diff --git a/src/mx_bluesky/beamlines/i02_1/__init__.py b/src/mx_bluesky/beamlines/i02_1/__init__.py new file mode 100644 index 0000000000..413892bc30 --- /dev/null +++ b/src/mx_bluesky/beamlines/i02_1/__init__.py @@ -0,0 +1,5 @@ +from mx_bluesky.beamlines.i02_1.i02_1_flyscan_xray_centre_plan import ( + i02_1_flyscan_xray_centre, +) + +__all__ = ["i02_1_flyscan_xray_centre"] diff --git a/src/mx_bluesky/beamlines/i02_1/constants.py b/src/mx_bluesky/beamlines/i02_1/constants.py new file mode 100644 index 0000000000..a4f7aac8e3 --- /dev/null +++ b/src/mx_bluesky/beamlines/i02_1/constants.py @@ -0,0 +1,7 @@ +from pydantic.dataclasses import dataclass + + +@dataclass(frozen=True) +class I02_1_Constants: + GRAYLOG_PORT = 12232 + LOG_FILE_NAME = "i02_1_bluesky.log" diff --git a/src/mx_bluesky/beamlines/i02_1/device_setup_plans/setup_zebra.py b/src/mx_bluesky/beamlines/i02_1/device_setup_plans/setup_zebra.py new file mode 100644 index 0000000000..83513a241b --- /dev/null +++ b/src/mx_bluesky/beamlines/i02_1/device_setup_plans/setup_zebra.py @@ -0,0 +1,28 @@ +import bluesky.plan_stubs as bps +from dodal.devices.zebra.zebra import Zebra + +ZEBRA_STATUS_TIMEOUT = 30 + + +# Control Eiger from motion controller +def setup_zebra_for_xrc_flyscan(zebra: Zebra, group="setup_zebra_for_xrc", wait=True): + yield from bps.abs_set( + zebra.output.out_pvs[zebra.mapping.outputs.TTL_EIGER], + zebra.mapping.sources.IN1_TTL, + ) + if wait: + yield from bps.wait(group, timeout=ZEBRA_STATUS_TIMEOUT) + + +# State of zebra expected by GDA +def tidy_up_zebra_after_gridscan( + zebra: Zebra, group="tidy_up_vmxm_zebra_after_gridscan", wait=False +): + yield from bps.abs_set( + zebra.output.out_pvs[zebra.mapping.outputs.TTL_EIGER], + zebra.mapping.sources.OR1, + group=group, + ) + + if wait: + yield from bps.wait(group) diff --git a/src/mx_bluesky/beamlines/i02_1/i02_1_flyscan_xray_centre_plan.py b/src/mx_bluesky/beamlines/i02_1/i02_1_flyscan_xray_centre_plan.py new file mode 100644 index 0000000000..1f87299f49 --- /dev/null +++ b/src/mx_bluesky/beamlines/i02_1/i02_1_flyscan_xray_centre_plan.py @@ -0,0 +1,122 @@ +from functools import partial + +import bluesky.plan_stubs as bps +import bluesky.preprocessors as bpp +import pydantic +from bluesky.utils import MsgGenerator +from dodal.beamlines.i02_1 import TwoDFastGridScan +from dodal.common import inject +from dodal.devices.eiger import EigerDetector +from dodal.devices.fast_grid_scan import ( + set_fast_grid_scan_params as set_flyscan_params_plan, +) +from dodal.devices.i02_1.sample_motors import SampleMotors +from dodal.devices.synchrotron import Synchrotron +from dodal.devices.zebra.zebra import Zebra +from dodal.devices.zocalo.zocalo_results import ( + ZocaloResults, +) + +from mx_bluesky.beamlines.i02_1.constants import I02_1_Constants +from mx_bluesky.beamlines.i02_1.device_setup_plans.setup_zebra import ( + setup_zebra_for_xrc_flyscan, + tidy_up_zebra_after_gridscan, +) +from mx_bluesky.common.experiment_plans.common_flyscan_xray_centre_plan import ( + CALLBACKS_FOR_SUBS_DECORATOR, + BeamlineSpecificFGSFeatures, + FlyScanEssentialDevices, + common_flyscan_xray_centre, + construct_beamline_specific_FGS_features, +) +from mx_bluesky.common.parameters.gridscan import SpecifiedThreeDGridScan +from mx_bluesky.common.utils.log import LOGGER, do_default_logging_setup + + +@pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True}) +class FlyScanXRayCentreComposite(FlyScanEssentialDevices): + """All devices which are directly or indirectly required by this plan""" + + # todo add fast grid scan device to essentials + zebra_fast_grid_scan: TwoDFastGridScan + zebra: Zebra + sample_stages: SampleMotors + + +def construct_i02_1_specific_features( + fgs_composite: FlyScanXRayCentreComposite, + parameters: SpecifiedThreeDGridScan, +) -> BeamlineSpecificFGSFeatures: + signals_to_read_pre_flyscan = [ + fgs_composite.synchrotron.synchrotron_mode, + fgs_composite.sample_stages, + ] + signals_to_read_during_collection = [ + fgs_composite.eiger.bit_depth, + ] + + return construct_beamline_specific_FGS_features( + _zebra_triggering_setup, + partial(_tidy_plan, group="flyscan_zebra_tidy", wait=True), + partial( + set_flyscan_params_plan, + fgs_composite.zebra_fast_grid_scan, + parameters.FGS_params, + ), + fgs_composite.zebra_fast_grid_scan, + signals_to_read_pre_flyscan, + signals_to_read_during_collection, # type: ignore # See : https://github.com/bluesky/bluesky/issues/1809 + ) + + +def _zebra_triggering_setup( + fgs_composite: FlyScanXRayCentreComposite, +): + yield from setup_zebra_for_xrc_flyscan(fgs_composite.zebra) + + +def _tidy_plan( + fgs_composite: FlyScanXRayCentreComposite, group, wait=True +) -> MsgGenerator: + LOGGER.info("Tidying up Zebra") + yield from tidy_up_zebra_after_gridscan(fgs_composite.zebra) + LOGGER.info("Tidying up Zocalo") + # make sure we don't consume any other results + yield from bps.unstage(fgs_composite.zocalo, group=group, wait=wait) + + +def i02_1_flyscan_xray_centre( + parameters: SpecifiedThreeDGridScan, + eiger: EigerDetector = inject("eiger"), + zebra_fast_grid_scan: TwoDFastGridScan = inject("TwoDFastGridScan"), + synchrotron: Synchrotron = inject("synchrotron"), + zebra: Zebra = inject("zebra"), + zocalo: ZocaloResults = inject("zocalo"), + sample_motors: SampleMotors = inject("sample_motors"), +) -> MsgGenerator: + """BlueAPI entry point for XRC grid scans""" + + do_default_logging_setup( + I02_1_Constants.LOG_FILE_NAME, + I02_1_Constants.GRAYLOG_PORT, + ) + + # Composites have to be made this way until https://github.com/DiamondLightSource/dodal/issues/874 + # is done and we can properly use composite devices in BlueAPI + composite = FlyScanXRayCentreComposite( + eiger, + synchrotron, + zocalo, + sample_motors, + zebra_fast_grid_scan, + zebra, + sample_motors, + ) + + beamline_specific = construct_i02_1_specific_features(composite, parameters) + + @bpp.subs_decorator(CALLBACKS_FOR_SUBS_DECORATOR) + def decorated_flyscan_plan(): + yield from common_flyscan_xray_centre(composite, parameters, beamline_specific) + + yield from decorated_flyscan_plan() diff --git a/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py b/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py index d80fe1f307..0fe86dcad9 100644 --- a/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +++ b/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py @@ -14,19 +14,32 @@ ) from dodal.devices.zocalo import ZocaloResults from dodal.devices.zocalo.zocalo_results import ( + ZOCALO_STAGE_GROUP, XrcResult, get_full_processing_results, ) from mx_bluesky.common.experiment_plans.inner_plans.do_fgs import ( - ZOCALO_STAGE_GROUP, kickoff_and_complete_gridscan, ) from mx_bluesky.common.experiment_plans.inner_plans.read_hardware import ( read_hardware_plan, ) +from mx_bluesky.common.external_interaction.callbacks.common.log_uid_tag_callback import ( + LogUidTaggingCallback, +) +from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import ( + ZocaloCallback, +) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( + GridscanISPyBCallback, +) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( + GridscanNexusFileCallback, +) from mx_bluesky.common.parameters.constants import ( DocDescriptorNames, + EnvironmentConstants, GridscanParamConstants, PlanGroupCheckpointConstants, PlanNameConstants, @@ -41,6 +54,17 @@ from mx_bluesky.common.utils.tracing import TRACER from mx_bluesky.common.xrc_result import XRayCentreResult +# Hyperion handles its own callbacks via an external process. Other beamlines using this plan should wrap their entry point with +# @bpp.subs_decorator(CALLBACKS_FOR_SUBS_DECORATOR) +CALLBACKS_FOR_SUBS_DECORATOR = [ + GridscanNexusFileCallback(param_type=SpecifiedThreeDGridScan), + GridscanISPyBCallback( + param_type=SpecifiedThreeDGridScan, + emit=ZocaloCallback(PlanNameConstants.DO_FGS, EnvironmentConstants.ZOCALO_ENV), + ), + LogUidTaggingCallback(), +] + @dataclasses.dataclass class BeamlineSpecificFGSFeatures: @@ -240,7 +264,7 @@ def run_gridscan( ): # Currently gridscan only works for omega 0, see https://github.com/DiamondLightSource/mx-bluesky/issues/410 with TRACER.start_span("moving_omega_to_0"): - yield from bps.abs_set(fgs_composite.smargon.omega, 0) + yield from bps.abs_set(fgs_composite.sample_stage.omega, 0) with TRACER.start_span("ispyb_hardware_readings"): yield from beamline_specific.read_pre_flyscan_plan() diff --git a/src/mx_bluesky/common/external_interaction/callbacks/common/callback_util.py b/src/mx_bluesky/common/external_interaction/callbacks/common/callback_util.py new file mode 100644 index 0000000000..c63f4ee4c3 --- /dev/null +++ b/src/mx_bluesky/common/external_interaction/callbacks/common/callback_util.py @@ -0,0 +1,28 @@ +from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import ( + ZocaloCallback, +) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( + GridscanISPyBCallback, +) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( + GridscanNexusFileCallback, +) +from mx_bluesky.common.parameters.constants import ( + EnvironmentConstants, + PlanNameConstants, +) +from mx_bluesky.common.parameters.gridscan import SpecifiedThreeDGridScan + + +def create_gridscan_callbacks() -> tuple[ + GridscanNexusFileCallback, GridscanISPyBCallback +]: + return ( + GridscanNexusFileCallback(param_type=SpecifiedThreeDGridScan), + GridscanISPyBCallback( + param_type=SpecifiedThreeDGridScan, + emit=ZocaloCallback( + PlanNameConstants.DO_FGS, EnvironmentConstants.ZOCALO_ENV + ), + ), + ) diff --git a/src/mx_bluesky/common/parameters/device_composites.py b/src/mx_bluesky/common/parameters/device_composites.py index ff964498f1..54fd5414dd 100644 --- a/src/mx_bluesky/common/parameters/device_composites.py +++ b/src/mx_bluesky/common/parameters/device_composites.py @@ -1,3 +1,5 @@ +from typing import Protocol + import pydantic from dodal.devices.aperturescatterguard import ( ApertureScatterguard, @@ -23,6 +25,12 @@ from dodal.devices.zebra.zebra import Zebra from dodal.devices.zebra.zebra_controlled_shutter import ZebraShutter from dodal.devices.zocalo import ZocaloResults +from ophyd_async.epics.motor import Motor + + +# FGS plan only uses the gonio to set omega to 0, no need to constrain to a more complex device +class SampleStageWithOmega(Protocol): + omega: Motor @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True}) @@ -30,7 +38,8 @@ class FlyScanEssentialDevices: eiger: EigerDetector synchrotron: Synchrotron zocalo: ZocaloResults - smargon: Smargon + sample_stage: SampleStageWithOmega + # TODO add fgs device @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True}) @@ -63,3 +72,4 @@ class GridDetectThenXRayCentreComposite(FlyScanEssentialDevices): zebra: Zebra robot: BartRobot sample_shutter: ZebraShutter + smargon: Smargon diff --git a/src/mx_bluesky/hyperion/parameters/device_composites.py b/src/mx_bluesky/hyperion/parameters/device_composites.py index 49fdae6a10..c8854fab05 100644 --- a/src/mx_bluesky/hyperion/parameters/device_composites.py +++ b/src/mx_bluesky/hyperion/parameters/device_composites.py @@ -15,6 +15,7 @@ from dodal.devices.flux import Flux from dodal.devices.robot import BartRobot from dodal.devices.s4_slit_gaps import S4SlitGaps +from dodal.devices.smargon import Smargon from dodal.devices.synchrotron import Synchrotron from dodal.devices.undulator import Undulator from dodal.devices.xbpm_feedback import XBPMFeedback @@ -52,6 +53,7 @@ class HyperionFlyScanXRayCentreComposite(FlyScanEssentialDevices): backlight: Backlight xbpm_feedback: XBPMFeedback zebra_fast_grid_scan: ZebraFastGridScan + smargon: Smargon @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True}) diff --git a/tests/conftest.py b/tests/conftest.py index a5d6be02d8..4e017dbccf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -859,6 +859,11 @@ def fake_create_rotation_devices( ) +@pytest.fixture +def oav_parameters_for_rotation(test_config_files) -> OAVParameters: + return OAVParameters(oav_config_json=test_config_files["oav_config_json"]) + + @pytest.fixture def zocalo(done_status, RE: RunEngine): zoc = i03.zocalo(connect_immediately=True, mock=True) @@ -925,11 +930,6 @@ async def create_mock_signals(devices_and_signals: dict[Device, dict[str, Any]]) return panda -@pytest.fixture -def oav_parameters_for_rotation(test_config_files) -> OAVParameters: - return OAVParameters(oav_config_json=test_config_files["oav_config_json"]) - - async def async_status_done(): await asyncio.sleep(0) @@ -1671,6 +1671,47 @@ class TestData(OavGridSnapshotTestEvents): } ] + test_result_large = [ + { + "centre_of_mass": [1, 2, 3], + "max_voxel": [1, 2, 3], + "max_count": 105062, + "n_voxels": 35, + "total_count": 2387574, + "bounding_box": [[2, 2, 2], [8, 8, 7]], + } + ] + test_result_medium = [ + { + "centre_of_mass": [1, 2, 3], + "max_voxel": [2, 4, 5], + "max_count": 50000, + "n_voxels": 35, + "total_count": 100000, + "bounding_box": [[1, 2, 3], [3, 4, 4]], + } + ] + test_result_small = [ + { + "centre_of_mass": [1, 2, 3], + "max_voxel": [1, 2, 3], + "max_count": 1000, + "n_voxels": 35, + "total_count": 1000, + "bounding_box": [[2, 2, 2], [3, 3, 3]], + } + ] + test_result_below_threshold = [ + { + "centre_of_mass": [2, 3, 4], + "max_voxel": [2, 3, 4], + "max_count": 2, + "n_voxels": 1, + "total_count": 2, + "bounding_box": [[1, 2, 3], [2, 3, 4]], + } + ] + def _mock_ispyb_conn(base_ispyb_conn, position_id, dcgid, dcids, giids): def upsert_data_collection(values):