diff --git a/pyproject.toml b/pyproject.toml index 6ade793d98..cefafbeb94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ dev = [ "tox-uv", "types-mock", "types-requests", - "sphinxcontrib.mermaid", # To build dodal docs + "sphinxcontrib.mermaid", # To build dodal docs ] [project.scripts] diff --git a/src/mx_bluesky/beamlines/i02_1/__init__.py b/src/mx_bluesky/beamlines/i02_1/__init__.py index e69de29bb2..daf4f10de7 100644 --- a/src/mx_bluesky/beamlines/i02_1/__init__.py +++ b/src/mx_bluesky/beamlines/i02_1/__init__.py @@ -0,0 +1,3 @@ +from mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan import i02_1_gridscan_plan + +__all__ = ["i02_1_gridscan_plan"] diff --git a/src/mx_bluesky/beamlines/i02_1/composites.py b/src/mx_bluesky/beamlines/i02_1/composites.py new file mode 100644 index 0000000000..795757bb55 --- /dev/null +++ b/src/mx_bluesky/beamlines/i02_1/composites.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from mx_bluesky.beamlines.i02_1.parameters.gridscan import SpecifiedTwoDGridScan + + +class I02_1FgsParams(SpecifiedTwoDGridScan): # noqa: N801 + """For VMXm gridscans, GDA currently takes the snapshots and provides bluesky with a path, and + sends over the grid parameters""" + + path_to_xtal_snapshot: Path + beam_size_x: float + beam_size_y: float + microns_per_pixel_x: float + microns_per_pixel_y: float + upper_left_x: int # position of X,Y for the top left of the grid, in pixels + upper_left_y: int diff --git a/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/__init__.py b/src/mx_bluesky/beamlines/i02_1/external_interaction/__init__.py similarity index 100% rename from src/mx_bluesky/common/external_interaction/callbacks/xray_centre/__init__.py rename to src/mx_bluesky/beamlines/i02_1/external_interaction/__init__.py diff --git a/src/mx_bluesky/beamlines/i02_1/external_interaction/callbacks/__init__.py b/src/mx_bluesky/beamlines/i02_1/external_interaction/callbacks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/mx_bluesky/beamlines/i02_1/external_interaction/callbacks/gridscan/__init__.py b/src/mx_bluesky/beamlines/i02_1/external_interaction/callbacks/gridscan/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/mx_bluesky/beamlines/i02_1/external_interaction/callbacks/gridscan/ispyb_callback.py b/src/mx_bluesky/beamlines/i02_1/external_interaction/callbacks/gridscan/ispyb_callback.py new file mode 100644 index 0000000000..3455847000 --- /dev/null +++ b/src/mx_bluesky/beamlines/i02_1/external_interaction/callbacks/gridscan/ispyb_callback.py @@ -0,0 +1,88 @@ +from collections.abc import Sequence + +from mx_bluesky.beamlines.i02_1.composites import I02_1FgsParams +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_mapping import ( + construct_comment_for_gridscan, +) +from mx_bluesky.common.external_interaction.callbacks.grid.gridscan.ispyb_callback import ( + GridscanISPyBCallback as CommonGridscanISPyBCallback, +) +from mx_bluesky.common.external_interaction.ispyb.data_model import ( + DataCollectionGridInfo, + DataCollectionInfo, + Orientation, + ScanDataInfo, +) +from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER + + +def _make_comment(x_steps: int, y_steps: int) -> str: + return f"Diffraction grid scan of {x_steps} by {y_steps}." + + +class GridscanISPyBCallback(CommonGridscanISPyBCallback): + def _get_scan_infos(self, doc) -> Sequence[ScanDataInfo]: + """ + For VMXm, grid information is available immediately after the plan is triggered. + """ + assert isinstance(self.params, I02_1FgsParams) + assert self.ispyb_ids.data_collection_ids, "No current data collection" + assert self.data_collection_group_info, "No data collection group" + data = doc["data"] + scan_data_infos = [] + + for grid_num in range(self.params.num_grids): + omega = data.get("gonio-omega", self.params.omega_starts_deg[grid_num]) + + ISPYB_ZOCALO_CALLBACK_LOGGER.info( + f"Generating dc info for gridplane XY, omega {omega}" + ) + data_collection_number = self.params.detector_params.run_number + file_template = f"{self.params.detector_params.prefix}_{data_collection_number}_master.h5" + # Snapshots have already been taken in GDA + + data_collection_info = DataCollectionInfo( + xtal_snapshot1=str(self.params.path_to_xtal_snapshot), + xtal_snapshot2=str(self.params.path_to_xtal_snapshot), + xtal_snapshot3=str(self.params.path_to_xtal_snapshot), + n_images=self.params.num_images, + data_collection_number=data_collection_number, + file_template=file_template, + ) + data_collection_grid_info = DataCollectionGridInfo( + dx_in_mm=self.params.x_step_size_um / 1000, + dy_in_mm=self.params.y_step_sizes_um[grid_num] / 1000, + steps_x=self.params.x_steps, + steps_y=self.params.y_steps[grid_num], + microns_per_pixel_x=self.params.microns_per_pixel_x, + microns_per_pixel_y=self.params.microns_per_pixel_y, + snapshot_offset_x_pixel=self.params.upper_left_x, + snapshot_offset_y_pixel=self.params.upper_left_y, + orientation=Orientation.HORIZONTAL, + snaked=True, + ) + data_collection_info.comments = construct_comment_for_gridscan( + data_collection_grid_info + ) + + data_collection_id = self.ispyb_ids.data_collection_ids[0] + + self.data_collection_group_info.comments = _make_comment( + self.params.x_steps, self.params.y_steps[0] + ) + + self._populate_axis_info(data_collection_info, doc["data"]) + + scan_data_info = ScanDataInfo( + data_collection_info=data_collection_info, + data_collection_id=data_collection_id, + data_collection_grid_info=data_collection_grid_info, + ) + + scan_data_infos.append(scan_data_info) + + ISPYB_ZOCALO_CALLBACK_LOGGER.info( + "Updating ispyb data collection after loading grid params" + ) + + return scan_data_infos diff --git a/src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py b/src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py index 3c7626e911..cbeaaab8c2 100644 --- a/src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py +++ b/src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py @@ -1,4 +1,5 @@ from functools import partial +from pathlib import Path import bluesky.preprocessors as bpp import pydantic @@ -6,19 +7,27 @@ from dodal.beamlines.i02_1 import ZebraFastGridScanTwoD from dodal.common import inject from dodal.devices.attenuator.attenuator import ReadOnlyAttenuator +from dodal.devices.beamlines.i02_1.flux import Flux from dodal.devices.common_dcm import DoubleCrystalMonochromatorBase from dodal.devices.fast_grid_scan import ( set_fast_grid_scan_params as set_flyscan_params_plan, ) -from dodal.devices.flux import Flux -from dodal.devices.s4_slit_gaps import S4SlitGaps +from dodal.devices.motors import XYZWrappedOmegaStage +from dodal.devices.slits import Slits from dodal.devices.undulator import BaseUndulator from dodal.devices.zebra.zebra import Zebra +from pydantic import BaseModel +from pydantic_extra_types.semantic_version import SemanticVersion +from semver import Version +from mx_bluesky.beamlines.i02_1.composites import I02_1FgsParams from mx_bluesky.beamlines.i02_1.device_setup_plans.setup_zebra import ( setup_zebra_for_gridscan, tidy_up_zebra_after_gridscan, ) +from mx_bluesky.beamlines.i02_1.external_interaction.callbacks.gridscan.ispyb_callback import ( + GridscanISPyBCallback, +) from mx_bluesky.beamlines.i02_1.parameters.gridscan import SpecifiedTwoDGridScan from mx_bluesky.common.experiment_plans.common_flyscan_xray_centre_plan import ( BeamlineSpecificFGSFeatures, @@ -28,43 +37,50 @@ 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, - generate_start_info_from_omega_map, -) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback import ( GridscanNexusFileCallback, ) +from mx_bluesky.common.external_interaction.callbacks.grid.gridscan.ispyb_callback import ( + ispyb_activation_decorator, +) +from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( + generate_start_info_from_num_grids, +) +from mx_bluesky.common.parameters.components import ( + IspybExperimentType, + get_param_version, +) from mx_bluesky.common.parameters.constants import ( EnvironmentConstants, PlanNameConstants, ) from mx_bluesky.common.parameters.device_composites import ( FlyScanEssentialDevices, - GonioWithOmegaType, ) -from mx_bluesky.common.parameters.gridscan import GenericGrid +from mx_bluesky.common.parameters.gridscan import PositiveFloat from mx_bluesky.common.utils.log import LOGGER +DEFAULT_BOX_SIZE_UM = 2 -def create_gridscan_callbacks() -> tuple[ - GridscanNexusFileCallback, GridscanISPyBCallback -]: + +def create_gridscan_callbacks( + params: I02_1FgsParams, +) -> tuple[GridscanNexusFileCallback, GridscanISPyBCallback]: return ( GridscanNexusFileCallback(param_type=SpecifiedTwoDGridScan), GridscanISPyBCallback( - param_type=GenericGrid, + param_type=I02_1FgsParams, emit=ZocaloCallback( PlanNameConstants.DO_FGS, EnvironmentConstants.ZOCALO_ENV, - generate_start_info_from_omega_map, + lambda: generate_start_info_from_num_grids(params), ), ), ) @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True}) -class I021FlyScanXRayCentreComposite(FlyScanEssentialDevices[GonioWithOmegaType]): +class I021FlyScanXRayCentreComposite(FlyScanEssentialDevices[XYZWrappedOmegaStage]): """All devices which are directly or indirectly required by this plan""" zebra: Zebra @@ -73,7 +89,7 @@ class I021FlyScanXRayCentreComposite(FlyScanEssentialDevices[GonioWithOmegaType] attenuator: ReadOnlyAttenuator flux: Flux undulator: BaseUndulator - s4_slit_gaps: S4SlitGaps + s4_slit_gaps: Slits def construct_i02_1_specific_features( @@ -122,17 +138,82 @@ def _tidy_plan( yield from tidy_up_zebra_after_gridscan(fgs_composite.zebra) +PARAMETER_VERSION = Version.parse("1.0.0") + + +def get_internal_param_version() -> SemanticVersion: + return SemanticVersion.validate_from_str(str(PARAMETER_VERSION)) + + +class ExternalGridScanParams(BaseModel): + visit: str + file_name: str + storage_directory: str + exposure_time_s: float + snapshot_directory: Path + x_start_um: float + y_start_um: float + z_start_um: float + x_steps: int + y_steps: int + beam_size_x: float + beam_size_y: float + microns_per_pixel_x: float + microns_per_pixel_y: float + upper_left_x: int + upper_left_y: int + detector_distance_mm: float + sample_id: int + + # GDA branch needs to update for these params + x_step_size_um: PositiveFloat + y_step_sizes_um: list[PositiveFloat] + omega_start_deg: int + + +def get_internal_params(params: ExternalGridScanParams) -> I02_1FgsParams: + return I02_1FgsParams( + y_starts_um=[params.y_start_um], + x_start_um=params.x_start_um, + z_starts_um=[params.z_start_um], + omega_starts_deg=[params.omega_start_deg], + sample_id=params.sample_id, + visit=params.visit, + parameter_model_version=get_param_version(), + file_name=params.file_name, + storage_directory=params.storage_directory, + x_steps=params.x_steps, + y_steps=[params.y_steps], + path_to_xtal_snapshot=params.snapshot_directory, + beam_size_x=params.beam_size_x, + beam_size_y=params.beam_size_y, + microns_per_pixel_x=params.microns_per_pixel_x, + microns_per_pixel_y=params.microns_per_pixel_y, + upper_left_x=params.upper_left_x, + upper_left_y=params.upper_left_y, + detector_distance_mm=params.detector_distance_mm, + ispyb_experiment_type=IspybExperimentType.SAD, + x_step_size_um=params.x_step_size_um, + y_step_sizes_um=params.y_step_sizes_um, + use_roi_mode=False, + box_size_um=DEFAULT_BOX_SIZE_UM, + ) + + def i02_1_gridscan_plan( - parameters: SpecifiedTwoDGridScan, + parameters: ExternalGridScanParams, composite: I021FlyScanXRayCentreComposite = inject(""), ) -> MsgGenerator: """BlueAPI entry point for i02-1 grid scans""" - beamline_specific = construct_i02_1_specific_features(composite, parameters) - callbacks = create_gridscan_callbacks() + params = get_internal_params(parameters) + + beamline_specific = construct_i02_1_specific_features(composite, params) + callbacks = create_gridscan_callbacks(params) @bpp.subs_decorator(callbacks) + @ispyb_activation_decorator(params) def decorated_flyscan_plan(): - yield from common_flyscan_xray_centre(composite, parameters, beamline_specific) + yield from common_flyscan_xray_centre(composite, params, beamline_specific) yield from decorated_flyscan_plan() diff --git a/src/mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py b/src/mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py index 34cb748fc8..8fa837ae3b 100644 --- a/src/mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +++ b/src/mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py @@ -62,16 +62,19 @@ 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, - generate_start_info_from_omega_map, +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( + GridDetectAndScanISPyBCallback, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback import ( GridscanNexusFileCallback, ) +from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( + generate_start_info_from_omega_map, +) from mx_bluesky.common.parameters.components import get_param_version from mx_bluesky.common.parameters.constants import ( EnvironmentConstants, + GridscanParamConstants, OavConstants, PlanGroupCheckpointConstants, PlanNameConstants, @@ -208,7 +211,10 @@ def _inner_grid_detect_then_xrc(): # Hyperion handles its callbacks differently to BlueAPI-managed plans, see # https://github.com/DiamondLightSource/mx-bluesky/issues/1117 flyscan_event_handler = XRayCentreEventHandler() - callbacks = *create_gridscan_callbacks(), flyscan_event_handler + callbacks = ( + *create_gridscan_callbacks(), + flyscan_event_handler, + ) @bpp.subs_decorator(callbacks) @verify_undulator_gap_before_run_decorator(composite) @@ -271,16 +277,18 @@ def get_ready_for_oav_and_close_shutter( def create_gridscan_callbacks() -> tuple[ - GridscanNexusFileCallback, GridscanISPyBCallback + GridscanNexusFileCallback, GridDetectAndScanISPyBCallback ]: return ( GridscanNexusFileCallback(param_type=SpecifiedThreeDGridScan), - GridscanISPyBCallback( + GridDetectAndScanISPyBCallback( param_type=GenericGrid, emit=ZocaloCallback( PlanNameConstants.DO_FGS, EnvironmentConstants.ZOCALO_ENV, - generate_start_info_from_omega_map, + lambda: generate_start_info_from_omega_map( + [GridscanParamConstants.OMEGA_1, GridscanParamConstants.OMEGA_2] + ), ), ), ) diff --git a/src/mx_bluesky/common/experiment_plans/common_grid_detect_then_xray_centre_plan.py b/src/mx_bluesky/common/experiment_plans/common_grid_detect_then_xray_centre_plan.py index ce2508c1b2..46d77f7ead 100644 --- a/src/mx_bluesky/common/experiment_plans/common_grid_detect_then_xray_centre_plan.py +++ b/src/mx_bluesky/common/experiment_plans/common_grid_detect_then_xray_centre_plan.py @@ -32,7 +32,7 @@ GridDetectionCallback, GridParamUpdate, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( ispyb_activation_wrapper, ) from mx_bluesky.common.parameters.constants import ( diff --git a/src/mx_bluesky/common/experiment_plans/inner_plans/do_fgs.py b/src/mx_bluesky/common/experiment_plans/inner_plans/do_fgs.py index d7e973a926..452cd61f45 100644 --- a/src/mx_bluesky/common/experiment_plans/inner_plans/do_fgs.py +++ b/src/mx_bluesky/common/experiment_plans/inner_plans/do_fgs.py @@ -1,4 +1,4 @@ -from collections.abc import Callable +from collections.abc import Callable, Sequence from time import time import bluesky.plan_stubs as bps @@ -65,7 +65,7 @@ def kickoff_and_complete_gridscan( detector: EigerDetector, # Once Eiger inherits from StandardDetector, use that type instead synchrotron: Synchrotron, scan_points: list[AxesPoints[Axis]], - omega_starts_deg: list[float], + omega_starts_deg: Sequence[float], plan_during_collection: Callable[[], MsgGenerator] | None = None, ): """Triggers a grid scan motion program and waits for completion, accounting for synchrotron topup. diff --git a/src/mx_bluesky/common/external_interaction/callbacks/__init__.py b/src/mx_bluesky/common/external_interaction/callbacks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py b/src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py index eedd0ba85f..bb25b9f68d 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py @@ -128,24 +128,44 @@ def activity_gated_event(self, doc: Event) -> Event: return self.tag_doc(doc) def _handle_ispyb_hardware_read(self, doc) -> Sequence[ScanDataInfo]: + _data = doc["data"] + assert self.params, "Event handled before activity_gated_start received params" ISPYB_ZOCALO_CALLBACK_LOGGER.info( f"ISPyB handler received event from read hardware: {doc}" ) - synchrotron_mode = doc["data"]["synchrotron-synchrotron_mode"] - hwscan_data_collection_info = DataCollectionInfo( - undulator_gap1=doc["data"]["undulator-current_gap"], - synchrotron_mode=str(synchrotron_mode), - slitgap_horizontal=doc["data"]["s4_slit_gaps-xgap"], - slitgap_vertical=doc["data"]["s4_slit_gaps-ygap"], - ) + synchrotron_mode = _data["synchrotron-synchrotron_mode"] + + # TODO remove this abomination + # https://github.com/DiamondLightSource/mx-bluesky/issues/1555 + if "s4_slit_gaps-xgap" in _data: + hwscan_data_collection_info = DataCollectionInfo( + undulator_gap1=_data["undulator-current_gap"], + synchrotron_mode=synchrotron_mode.value, + slitgap_horizontal=_data["s4_slit_gaps-xgap"], + slitgap_vertical=_data["s4_slit_gaps-ygap"], + ) + + elif "s4_slit_gaps-x_gap" in _data: + hwscan_data_collection_info = DataCollectionInfo( + undulator_gap1=_data["undulator-current_gap"], + synchrotron_mode=str(synchrotron_mode), + slitgap_horizontal=_data["s4_slit_gaps-x_gap"], + slitgap_vertical=_data["s4_slit_gaps-y_gap"], + ) + else: + raise ValueError( + f"Couldn't read slits from {doc=} and so couldn't update ispyb data collection info." + ) + hwscan_data_collection_info = _update_based_on_energy( doc, self.params.detector_params, hwscan_data_collection_info ) + hwscan_position_info = DataCollectionPositionInfo( - pos_x=float(doc["data"]["gonio-x"]), - pos_y=float(doc["data"]["gonio-y"]), - pos_z=float(doc["data"]["gonio-z"]), + pos_x=float(_data["gonio-x"]), + pos_y=float(_data.get("gonio-y")), + pos_z=float(_data["gonio-z"]), ) scan_data_infos = self.populate_info_for_update( hwscan_data_collection_info, hwscan_position_info, self.params @@ -158,18 +178,36 @@ def _handle_ispyb_hardware_read(self, doc) -> Sequence[ScanDataInfo]: def _handle_ispyb_transmission_flux_read( self, doc: Event ) -> Sequence[ScanDataInfo]: + _data = doc["data"] + assert self.params - aperture = doc["data"]["aperture_scatterguard-selected_aperture"] - beamsize_x_mm = doc["data"]["beamsize-x_um"] / 1000 - beamsize_y_mm = doc["data"]["beamsize-y_um"] / 1000 + aperture = _data.get( + "aperture_scatterguard-selected_aperture", "Not implemented" + ) + beamsize_x_mm = _data.get("beamsize-x_um", None) + if beamsize_x_mm: + beamsize_x_mm = beamsize_x_mm / 1000 + beamsize_y_mm = _data.get("beamsize-y_um", None) + if beamsize_y_mm: + beamsize_y_mm = beamsize_y_mm / 1000 + if not (beamsize_x_mm and beamsize_y_mm): + # VMXm don't have a beamsize device in dodal yet, they get beamsize sent in from GDA + try: + beamsize_x_mm = self.params.beam_size_x # type: ignore + beamsize_y_mm = self.params.beam_size_y # type: ignore + except Exception: + ISPYB_ZOCALO_CALLBACK_LOGGER.warning( + "ISPyB callbacks couldn't get beamsize" + ) + hwscan_data_collection_info = DataCollectionInfo( beamsize_at_samplex=beamsize_x_mm, beamsize_at_sampley=beamsize_y_mm, - flux=doc["data"]["flux-flux_reading"], - detector_mode="ROI" if doc["data"]["eiger_cam_roi_mode"] else "FULL", - ispyb_detector_id=doc["data"]["eiger-ispyb_detector_id"], + flux=_data["flux-flux_reading"], + detector_mode="ROI" if _data["eiger_cam_roi_mode"] else "FULL", + ispyb_detector_id=_data["eiger-ispyb_detector_id"], ) - if transmission := doc["data"]["attenuator-actual_transmission"]: + if transmission := _data["attenuator-actual_transmission"]: # Ispyb wants the transmission in a percentage, we use fractions hwscan_data_collection_info.transmission = transmission * 100 hwscan_data_collection_info = _update_based_on_energy( diff --git a/src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py b/src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py index 7df7c2a176..33cbeea8a5 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py @@ -48,7 +48,7 @@ def populate_remaining_data_collection_info( data_collection_info.ybeam = beam_position[1] data_collection_info.start_time = get_current_time_string() if data_collection_info.data_collection_number is not None: - # Do not write the file template if we don't have sufficient information - for gridscans we may not + # Do not write the file template if we don't have sufficient information - for gridscans we may not # know the data collection number until later data_collection_info.file_template = f"{params.detector_params.prefix}_{data_collection_info.data_collection_number}_master.h5" return data_collection_info diff --git a/src/mx_bluesky/common/external_interaction/callbacks/grid/__init__.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/__init__.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py similarity index 84% rename from src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py rename to src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py index 69736c8037..d36c040881 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py @@ -8,8 +8,6 @@ from bluesky import preprocessors as bpp from bluesky.utils import MsgGenerator, make_decorator -from dodal.common.maths import reflect_phase -from dodal.devices.zocalo import ZocaloStartInfo from mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base import ( BaseISPyBCallback, @@ -19,12 +17,14 @@ populate_data_collection_group, populate_remaining_data_collection_info, ) -from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import ( - ZocaloInfoGenerator, -) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_mapping import ( construct_comment_for_gridscan, ) +from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( + ASSERT_START_BEFORE_EVENT_DOC_MESSAGE, + common_add_processing_time_to_comment, + common_populate_axis_info, +) from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionGridInfo, DataCollectionGroupInfo, @@ -45,7 +45,6 @@ SampleError, ) from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, set_dcgid_tag -from mx_bluesky.common.utils.utils import number_of_frames_from_scan_spec OMEGA_TOLERANCE = 1 @@ -59,7 +58,6 @@ class GridscanPlane(StrEnum): from event_model import Event, RunStart, RunStop T = TypeVar("T", bound="GenericGrid") -ASSERT_START_BEFORE_EVENT_DOC_MESSAGE = f"No data collection group info - event document has been emitted before a {PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN} start document" def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters): @@ -67,7 +65,7 @@ def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters): bpp.run_wrapper( plan_generator, md={ - "activate_callbacks": ["GridscanISPyBCallback"], + "activate_callbacks": ["GridDetectAndScanISPyBCallback"], "subplan_name": PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN, "mx_bluesky_parameters": parameters.model_dump_json(), }, @@ -79,12 +77,15 @@ def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters): ispyb_activation_decorator = make_decorator(ispyb_activation_wrapper) -class GridscanISPyBCallback(BaseISPyBCallback): +class GridDetectAndScanISPyBCallback(BaseISPyBCallback): """Callback class to handle the deposition of experiment parameters into the ISPyB database. Listens for 'event' and 'descriptor' documents. Creates the ISpyB entry on receiving an 'event' document for the 'ispyb_reading_hardware' event, and updates the deposition on receiving its final 'stop' document. + This callback is specifically for detecting and scanning two grids. In the future + this callback should be made compatible to a generic number of grids + To use, subscribe the Bluesky RunEngine to an instance of this class. E.g.: ispyb_handler_callback = FGSISPyBCallback(parameters) @@ -171,16 +172,8 @@ def activity_gated_event(self, doc: Event): return doc def _add_processing_time_to_comment(self, processing_start_time: float): - assert self.data_collection_group_info, ASSERT_START_BEFORE_EVENT_DOC_MESSAGE - proc_time = time() - processing_start_time - crystal_summary = f"Zocalo processing took {proc_time:.2f} s." - - self.data_collection_group_info.comments = ( - self.data_collection_group_info.comments or "" - ) + crystal_summary - - self.ispyb.append_to_comment( - self.ispyb_ids.data_collection_ids[0], crystal_summary + common_add_processing_time_to_comment( + self, processing_start_time, self.data_collection_group_info ) def _handle_oav_grid_snapshot_triggered(self, doc) -> Sequence[ScanDataInfo]: @@ -256,14 +249,7 @@ def _handle_oav_grid_snapshot_triggered(self, doc) -> Sequence[ScanDataInfo]: return [scan_data_info] def _populate_axis_info(self, data_collection_info: DataCollectionInfo, doc: dict): - if (phase := doc.get("gonio-wrapped_omega-phase")) is not None: - omega_in_gda_space = reflect_phase(phase) - data_collection_info.omega_start = omega_in_gda_space - data_collection_info.axis_start = omega_in_gda_space - data_collection_info.axis_end = omega_in_gda_space - data_collection_info.axis_range = 0 - if (chi_start := doc.get("gonio-chi")) is not None: - data_collection_info.chi_start = chi_start + common_populate_axis_info(data_collection_info, doc) def populate_info_for_update( self, @@ -338,27 +324,6 @@ def data_collection_number_from_gridplane(self, plane) -> int: return base_number if plane == GridscanPlane.OMEGA_XY else base_number + 1 -def generate_start_info_from_omega_map() -> ZocaloInfoGenerator: - """ - Generate the zocalo trigger info from bluesky runs where the frame number is - computed using metadata added to the document by the ISPyB callback and the - run start which together can be used to determine the correct frame numbering. - """ - doc = yield [] - omega_to_scan_spec = doc["omega_to_scan_spec"] - start_frame = 0 - infos = [] - for i, omega in enumerate([GridscanPlane.OMEGA_XY, GridscanPlane.OMEGA_XZ]): - frames = number_of_frames_from_scan_spec(omega_to_scan_spec[omega]) - infos.append( - ZocaloStartInfo( - doc["grid_plane_to_id_map"][omega], None, start_frame, frames, i - ) - ) - start_frame += frames - yield infos - - def _smargon_omega_to_xyxz_plane(smargon_omega: float) -> GridscanPlane: modulo_180 = abs(smargon_omega) % 180 is_xy = isclose(modulo_180, 0, abs_tol=OMEGA_TOLERANCE) or isclose( diff --git a/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_mapping.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_mapping.py similarity index 100% rename from src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_mapping.py rename to src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_mapping.py diff --git a/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/nexus_callback.py similarity index 100% rename from src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py rename to src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/nexus_callback.py diff --git a/src/mx_bluesky/common/external_interaction/callbacks/grid/gridscan/__init__.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/gridscan/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/mx_bluesky/common/external_interaction/callbacks/grid/gridscan/ispyb_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/gridscan/ispyb_callback.py new file mode 100644 index 0000000000..82ed95e03b --- /dev/null +++ b/src/mx_bluesky/common/external_interaction/callbacks/grid/gridscan/ispyb_callback.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +from abc import abstractmethod +from collections.abc import Callable, Sequence +from time import time +from typing import TYPE_CHECKING, Any, TypeVar + +from bluesky import preprocessors as bpp +from bluesky.utils import MsgGenerator, make_decorator + +from mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base import ( + BaseISPyBCallback, +) +from mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping import ( + populate_data_collection_group, + populate_remaining_data_collection_info, +) +from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( + common_add_processing_time_to_comment, + common_populate_axis_info, +) +from mx_bluesky.common.external_interaction.ispyb.data_model import ( + DataCollectionGroupInfo, + DataCollectionInfo, + DataCollectionPositionInfo, + ScanDataInfo, +) +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( + IspybIds, + StoreInIspyb, +) +from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample +from mx_bluesky.common.parameters.constants import PlanNameConstants +from mx_bluesky.common.parameters.gridscan import SpecifiedGrids +from mx_bluesky.common.utils.exceptions import ( + ISPyBDepositionNotMadeError, + SampleError, +) +from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, set_dcgid_tag + +if TYPE_CHECKING: + from event_model import RunStart, RunStop + +T = TypeVar("T", bound="SpecifiedGrids") +D = TypeVar("D") + + +def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters): + return bpp.set_run_key_wrapper( + bpp.run_wrapper( + plan_generator, + md={ + "activate_callbacks": ["GridscanISPyBCallback"], + "subplan_name": PlanNameConstants.TRIGGER_GRIDSCAN_ISPYB_CALLBACK, + "mx_bluesky_parameters": parameters.model_dump_json(), + }, + ), + PlanNameConstants.TRIGGER_GRIDSCAN_ISPYB_CALLBACK, + ) + + +ispyb_activation_decorator = make_decorator(ispyb_activation_wrapper) + + +class GridscanISPyBCallback(BaseISPyBCallback): + """Callback class to handle the deposition of experiment parameters into the ISPyB + database. Listens for 'event' and 'descriptor' documents. Creates the ISpyB entry on + receiving an 'event' document for the 'ispyb_reading_hardware' event, and updates the + deposition on receiving its final 'stop' document. + + This callback should be used when grid parameters have been sent in to BlueAPI as part + of entry parameters. + + To use, subscribe the Bluesky RunEngine to an instance of this class. + E.g.: + ispyb_handler_callback = FGSISPyBCallback(parameters) + run_engine.subscribe(ispyb_handler_callback) + Or decorate a plan using bluesky.preprocessors.subs_decorator. + + See: https://blueskyproject.io/bluesky/callbacks.html#ways-to-invoke-callbacks + """ + + def __init__( + self, + param_type: type[T], + *, + emit: Callable[..., Any] | None = None, + ) -> None: + super().__init__(emit=emit) + self.ispyb: StoreInIspyb + self.param_type = param_type + self._start_of_fgs_uid: str | None = None + self._processing_start_time: float | None = None + self._grid_num_to_id_map: dict[int, int] = {} + self.data_collection_group_info: DataCollectionGroupInfo | None + + def activity_gated_start(self, doc: RunStart): + if doc.get("subplan_name") == PlanNameConstants.TRIGGER_GRIDSCAN_ISPYB_CALLBACK: + self._start_of_fgs_uid = doc.get("uid") + ISPYB_ZOCALO_CALLBACK_LOGGER.info( + "ISPyB callback received start document with experiment parameters and " + f"uid: {self._start_of_fgs_uid}" + ) + mx_bluesky_parameters = doc.get("mx_bluesky_parameters") + assert isinstance(mx_bluesky_parameters, str) + self.params = self.param_type.model_validate_json(mx_bluesky_parameters) + + # Fill ispyb deposition with all relevant info, including grid info + self.fill_gridscan_deposition_and_store(lambda: self._get_scan_infos(doc)) + + set_dcgid_tag(self.ispyb_ids.data_collection_group_id) + return super().activity_gated_start(doc) + + def _add_processing_time_to_comment(self, processing_start_time: float): + common_add_processing_time_to_comment( + self, processing_start_time, self.data_collection_group_info + ) + + @abstractmethod + def _get_scan_infos(self, doc) -> Sequence[ScanDataInfo]: ... + + """ + Use grid parameters to create a sequence of ScanDataInfos. See + i02-1's gridscan ispyb callback for example implementation. + """ + + def _populate_axis_info(self, data_collection_info: DataCollectionInfo, doc: dict): + common_populate_axis_info(data_collection_info, doc) + + def populate_info_for_update( + self, + event_sourced_data_collection_info: DataCollectionInfo, + event_sourced_position_info: DataCollectionPositionInfo | None, + params: DiffractionExperimentWithSample, + ) -> Sequence[ScanDataInfo]: + assert self.ispyb_ids.data_collection_ids, ( + "Expect at least one valid data collection to record scan data" + ) + assert isinstance(self.params, SpecifiedGrids) + scan_data_infos = [] + for grid_num in range(self.params.num_grids): + id = self.ispyb_ids.data_collection_ids[grid_num] + self._grid_num_to_id_map[grid_num] = id + scan_data_info = ScanDataInfo( + data_collection_info=event_sourced_data_collection_info, + data_collection_id=id, + ) + scan_data_infos.append(scan_data_info) + return scan_data_infos + + def activity_gated_stop(self, doc: RunStop) -> RunStop: + assert self.data_collection_group_info, ( + f"No data collection group info - stop document has been emitted before a {PlanNameConstants.DO_FGS} start document" + ) + if doc.get("run_start") == self._start_of_fgs_uid: + self._processing_start_time = time() + if doc.get("run_start") == self._start_of_fgs_uid: + ISPYB_ZOCALO_CALLBACK_LOGGER.info( + "ISPyB callback received stop document corresponding to start document " + f"with uid: {self._start_of_fgs_uid}." + ) + if self.ispyb_ids == IspybIds(): + raise ISPyBDepositionNotMadeError( + "ispyb was not initialised at run start" + ) + exception_type, message = SampleError.type_and_message_from_reason( + doc.get("reason", "") + ) + if exception_type: + doc["reason"] = message + self.data_collection_group_info.comments = message + elif self._processing_start_time: + self._add_processing_time_to_comment(self._processing_start_time) + self.ispyb.update_data_collection_group_table( + self.data_collection_group_info, + self.ispyb_ids.data_collection_group_id, + ) + self.data_collection_group_info = None + return super().activity_gated_stop(doc) + return self.tag_doc(doc) + + def tag_doc(self, doc: D) -> D: + doc = super().tag_doc(doc) + assert isinstance(doc, dict) + if self._grid_num_to_id_map: + doc["_grid_num_to_id_map"] = self._grid_num_to_id_map + return doc # type: ignore + + def fill_gridscan_deposition_and_store( + self, make_scan_infos_with_grid_info: Callable[..., Sequence[ScanDataInfo]] + ): + assert isinstance(self.params, SpecifiedGrids) + + # Do initial deposition using all info except grid info + self.ispyb = StoreInIspyb(self.ispyb_config) + self.data_collection_group_info = populate_data_collection_group(self.params) + scan_data_infos = [] + assert self.params.num_grids > 0 + for grid in range(self.params.num_grids): + scan_data_infos.append( + ScanDataInfo( + data_collection_info=populate_remaining_data_collection_info( + f"MX-Bluesky: Xray centring {grid + 1}/{self.params.num_grids} -", + None, + DataCollectionInfo(), + self.params, + ) + ) + ) + self.ispyb_ids = self.ispyb.begin_deposition( + self.data_collection_group_info, scan_data_infos + ) + # Now use grid information to complete deposition + scan_data_infos_list: list[ScanDataInfo] = list( + make_scan_infos_with_grid_info() + ) + self.ispyb_ids = self.ispyb.update_deposition( + self.ispyb_ids, scan_data_infos_list + ) + self.ispyb.update_data_collection_group_table( + self.data_collection_group_info, self.ispyb_ids.data_collection_group_id + ) diff --git a/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py new file mode 100644 index 0000000000..4f94d461d4 --- /dev/null +++ b/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from time import time + +from dodal.common.maths import reflect_phase +from dodal.devices.zocalo import ZocaloStartInfo + +from mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base import ( + BaseISPyBCallback, +) +from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import ( + ZocaloInfoGenerator, +) +from mx_bluesky.common.external_interaction.ispyb.data_model import ( + DataCollectionGroupInfo, + DataCollectionInfo, +) +from mx_bluesky.common.parameters.constants import PlanNameConstants +from mx_bluesky.common.parameters.gridscan import SpecifiedGrids +from mx_bluesky.common.utils.utils import number_of_frames_from_scan_spec + +ASSERT_START_BEFORE_EVENT_DOC_MESSAGE = f"No data collection group info - event document has been emitted before a {PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN} start document" + + +def generate_start_info_from_omega_map( + omega_positions: list[int], +) -> ZocaloInfoGenerator: + """ + Generate the zocalo trigger info from bluesky runs where the frame number is + computed using metadata added to the document by the ISPyB callback and the + run start which together can be used to determine the correct frame numbering. + """ + doc = yield [] + omega_to_scan_spec = doc["omega_to_scan_spec"] + start_frame = 0 + infos = [] + omegas_str = [str(omega) for omega in omega_positions] + for i, omega in enumerate(omegas_str): + frames = number_of_frames_from_scan_spec(omega_to_scan_spec[omega]) + infos.append( + ZocaloStartInfo( + doc["grid_plane_to_id_map"][omega], None, start_frame, frames, i + ) + ) + start_frame += frames + yield infos + + +def generate_start_info_from_num_grids( + params: SpecifiedGrids, +) -> ZocaloInfoGenerator: + """ + Generate the zocalo trigger info from bluesky runs where the grid specs + are immediately known from entry parameters. Metadata added to the document + by the ispyb callback maps the data collection id to the grid number. + """ + + doc = yield [] + + infos = [] + for grid_num in range(params.num_grids): + start_frame = params.scan_indices[grid_num] + frames = len(params.scan_points[grid_num]) + infos.append( + ZocaloStartInfo( + doc["_grid_num_to_id_map"][grid_num], + None, + start_frame, + frames, + grid_num, + ) + ) + start_frame += frames + yield infos + + +def common_populate_axis_info(data_collection_info: DataCollectionInfo, doc: dict): + omega_in_gda_space = None + if (phase := doc.get("gonio-wrapped_omega-phase")) is not None: + omega_in_gda_space = reflect_phase(phase) + elif (omega := doc.get("gonio-omega")) is not None: + # Gonio does not support wrapped omega - use absolute coordinates + omega_in_gda_space = -omega + if omega_in_gda_space is not None: + data_collection_info.omega_start = omega_in_gda_space + data_collection_info.axis_start = omega_in_gda_space + data_collection_info.axis_end = omega_in_gda_space + data_collection_info.axis_range = 0 + + if (chi_start := doc.get("gonio-chi")) is not None: + data_collection_info.chi_start = chi_start + + +def common_add_processing_time_to_comment( + callback: BaseISPyBCallback, + processing_start_time: float, + data_collection_group_info: DataCollectionGroupInfo | None, +): + assert data_collection_group_info, ASSERT_START_BEFORE_EVENT_DOC_MESSAGE + proc_time = time() - processing_start_time + crystal_summary = f"Zocalo processing took {proc_time:.2f} s." + + data_collection_group_info.comments = ( + data_collection_group_info.comments or "" + ) + crystal_summary + + callback.ispyb.append_to_comment( + callback.ispyb_ids.data_collection_ids[0], crystal_summary + ) diff --git a/src/mx_bluesky/common/parameters/components.py b/src/mx_bluesky/common/parameters/components.py index ca7aebf68e..8fb57e2b03 100644 --- a/src/mx_bluesky/common/parameters/components.py +++ b/src/mx_bluesky/common/parameters/components.py @@ -271,7 +271,9 @@ def _start_for_axis(self, axis: XyzAxis, grid: int) -> float: class OptionalGonioAngleStarts(BaseModel): # Gridscans have different omega starts - omega_starts_deg: list[float] = [0, 90] + # See https://github.com/DiamondLightSource/mx-bluesky/issues/1631 for why + # we use int + omega_starts_deg: list[int] = [0, 90] phi_start_deg: float | None = None chi_start_deg: float | None = None diff --git a/src/mx_bluesky/common/parameters/constants.py b/src/mx_bluesky/common/parameters/constants.py index a67d03924f..53ed763c73 100644 --- a/src/mx_bluesky/common/parameters/constants.py +++ b/src/mx_bluesky/common/parameters/constants.py @@ -76,6 +76,7 @@ class PlanNameConstants: DO_FGS = "do_fgs" FLYSCAN_RESULTS = "xray_centre_results" PIN_TIP_CENTRE_THEN_XRC = "pin_tip_centre_then_xray_centre" + TRIGGER_GRIDSCAN_ISPYB_CALLBACK = "trigger gridscan ispyb callback" # Rotation scan ROTATION_MULTI = "multi_rotation_wrapper" ROTATION_MULTI_OUTER = "multi_rotation_outer" diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 184217687e..675aa1a8c8 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -124,7 +124,7 @@ class SpecifiedGrids(GenericGrid, XyzStarts, WithScan, Generic[GridScanParamType # See https://github.com/DiamondLightSource/mx-bluesky/issues/1634 for a better structure for this # class - omega_starts_deg: list[float] = Field( + omega_starts_deg: list[int] = Field( default=[GridscanParamConstants.OMEGA_1, GridscanParamConstants.OMEGA_2] ) x_step_size_um: PositiveFloat = Field( diff --git a/src/mx_bluesky/hyperion/experiment_plans/pin_centre_then_gridscan_plan.py b/src/mx_bluesky/hyperion/experiment_plans/pin_centre_then_gridscan_plan.py index b6841ae173..22edfd77eb 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/pin_centre_then_gridscan_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/pin_centre_then_gridscan_plan.py @@ -20,7 +20,7 @@ PinTipCentringComposite, pin_tip_centre_plan, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( ispyb_activation_wrapper, ) from mx_bluesky.common.parameters.constants import OavConstants, PlanNameConstants diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py index 325e922f07..729e1a95dc 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py @@ -29,16 +29,19 @@ from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import ( ZocaloCallback, ) -from mx_bluesky.common.external_interaction.callbacks.sample_handling.sample_handling_callback import ( - SampleHandlingCallback, +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( + GridDetectAndScanISPyBCallback, +) +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback import ( + GridscanNexusFileCallback, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, +from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( generate_start_info_from_omega_map, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( - GridscanNexusFileCallback, +from mx_bluesky.common.external_interaction.callbacks.sample_handling.sample_handling_callback import ( + SampleHandlingCallback, ) +from mx_bluesky.common.parameters.constants import GridscanParamConstants from mx_bluesky.common.utils.log import ( ISPYB_ZOCALO_CALLBACK_LOGGER, NEXUS_LOGGER, @@ -67,7 +70,6 @@ from mx_bluesky.hyperion.parameters.cli import CallbackArgs, parse_callback_args from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.gridscan import ( - GenericGridWithHyperionDetectorParams, HyperionSpecifiedThreeDGridScan, ) @@ -79,14 +81,18 @@ def create_gridscan_callbacks() -> tuple[ - GridscanNexusFileCallback, GridscanISPyBCallback + GridscanNexusFileCallback, GridDetectAndScanISPyBCallback ]: return ( GridscanNexusFileCallback(param_type=HyperionSpecifiedThreeDGridScan), - GridscanISPyBCallback( - param_type=GenericGridWithHyperionDetectorParams, + GridDetectAndScanISPyBCallback( + param_type=HyperionSpecifiedThreeDGridScan, emit=ZocaloCallback( - CONST.PLAN.DO_FGS, CONST.ZOCALO_ENV, generate_start_info_from_omega_map + CONST.PLAN.DO_FGS, + CONST.ZOCALO_ENV, + lambda: generate_start_info_from_omega_map( + [GridscanParamConstants.OMEGA_1, GridscanParamConstants.OMEGA_2] + ), ), ), ) diff --git a/tests/conftest.py b/tests/conftest.py index 7ad6b0f767..b3a7864481 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -85,7 +85,7 @@ from scanspec.core import Path as ScanPath from scanspec.specs import Line -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( GridscanPlane, ) from mx_bluesky.common.parameters.constants import ( diff --git a/tests/system_tests/hyperion/external_interaction/callbacks/test_external_callbacks.py b/tests/system_tests/hyperion/external_interaction/callbacks/test_external_callbacks.py index 1a8cf6a5f0..f7a28afbdd 100644 --- a/tests/system_tests/hyperion/external_interaction/callbacks/test_external_callbacks.py +++ b/tests/system_tests/hyperion/external_interaction/callbacks/test_external_callbacks.py @@ -38,7 +38,7 @@ fetch_xrc_results_from_zocalo, zocalo_stage_decorator, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( ispyb_activation_decorator, ) from mx_bluesky.common.parameters.components import WithSnapshot diff --git a/tests/system_tests/hyperion/external_interaction/test_ispyb_dev_connection.py b/tests/system_tests/hyperion/external_interaction/test_ispyb_dev_connection.py index aafcf3932c..abd93a289b 100644 --- a/tests/system_tests/hyperion/external_interaction/test_ispyb_dev_connection.py +++ b/tests/system_tests/hyperion/external_interaction/test_ispyb_dev_connection.py @@ -21,10 +21,10 @@ populate_data_collection_group, populate_remaining_data_collection_info, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( + GridDetectAndScanISPyBCallback, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_mapping import ( construct_comment_for_gridscan, ) from mx_bluesky.common.external_interaction.ispyb.data_model import ( @@ -416,7 +416,9 @@ def test_ispyb_deposition_in_gridscan( set_mock_value( grid_detect_then_xray_centre_composite.s4_slit_gaps.ygap.user_readback, 0.1 ) - ispyb_callback = GridscanISPyBCallback(GenericGridWithHyperionDetectorParams) + ispyb_callback = GridDetectAndScanISPyBCallback( + GenericGridWithHyperionDetectorParams + ) run_engine.subscribe(ispyb_callback) run_engine( grid_detect_then_xray_centre( diff --git a/tests/system_tests/hyperion/external_interaction/test_load_centre_collect_full_plan.py b/tests/system_tests/hyperion/external_interaction/test_load_centre_collect_full_plan.py index c9ea83cf4b..81487faf97 100644 --- a/tests/system_tests/hyperion/external_interaction/test_load_centre_collect_full_plan.py +++ b/tests/system_tests/hyperion/external_interaction/test_load_centre_collect_full_plan.py @@ -28,12 +28,12 @@ from mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping import ( get_proposal_and_session_from_visit_string, ) +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( + GridDetectAndScanISPyBCallback, +) from mx_bluesky.common.external_interaction.callbacks.sample_handling.sample_handling_callback import ( SampleHandlingCallback, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, -) from mx_bluesky.common.utils.exceptions import ( CrystalNotFoundError, WarningError, @@ -305,7 +305,7 @@ def test_execute_load_centre_collect_full( tmp_path, robot_load_cb: RobotLoadISPyBCallback, ): - ispyb_gridscan_cb = GridscanISPyBCallback( + ispyb_gridscan_cb = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() @@ -487,7 +487,7 @@ def move_to_initial_omega(): yield from bps.mv(load_centre_collect_composite.gonio.omega, initial_omega) run_engine(move_to_initial_omega()) - ispyb_gridscan_cb = GridscanISPyBCallback( + ispyb_gridscan_cb = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() @@ -579,7 +579,7 @@ def test_load_centre_collect_updates_bl_sample_status_pin_tip_detection_fail( fetch_blsample: Callable[..., Any], ): robot_load_cb = RobotLoadISPyBCallback() - ispyb_gridscan_cb = GridscanISPyBCallback( + ispyb_gridscan_cb = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) sample_handling_cb = SampleHandlingCallback() @@ -613,7 +613,7 @@ def test_load_centre_collect_updates_bl_sample_status_grid_detection_fail_tip_no fetch_blsample: Callable[..., Any], ): robot_load_cb = RobotLoadISPyBCallback() - ispyb_gridscan_cb = GridscanISPyBCallback( + ispyb_gridscan_cb = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) sample_handling_cb = SampleHandlingCallback() @@ -665,7 +665,7 @@ def test_load_centre_collect_updates_bl_sample_status_gridscan_no_diffraction( fetch_blsample: Callable[..., Any], ): robot_load_cb = RobotLoadISPyBCallback() - ispyb_gridscan_cb = GridscanISPyBCallback( + ispyb_gridscan_cb = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) sample_handling_cb = SampleHandlingCallback() @@ -697,7 +697,7 @@ def test_load_centre_collect_updates_bl_sample_status_rotation_failure( fetch_blsample: Callable[..., Any], ): robot_load_cb = RobotLoadISPyBCallback() - ispyb_gridscan_cb = GridscanISPyBCallback( + ispyb_gridscan_cb = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) sample_handling_cb = SampleHandlingCallback() @@ -755,7 +755,7 @@ def test_load_centre_collect_gridscan_result_at_edge_of_grid( load_centre_collect_composite.zocalo.my_zocalo_result = _with_sample_ids( zocalo_result, [SimConstants.ST_SAMPLE_ID] ) - ispyb_gridscan_cb = GridscanISPyBCallback( + ispyb_gridscan_cb = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() @@ -789,7 +789,7 @@ def test_execute_load_centre_collect_capture_rotation_snapshots( ): load_centre_collect_params.multi_rotation_scan.snapshot_directory = tmp_path - ispyb_gridscan_cb = GridscanISPyBCallback( + ispyb_gridscan_cb = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() @@ -871,7 +871,7 @@ def test_load_centre_collect_multisample_pin_reports_correct_sample_ids_in_ispyb fetch_datacollection_attribute: Callable[..., Any], ): load_centre_collect_composite.zocalo.my_zocalo_result = zocalo_result - ispyb_gridscan_cb = GridscanISPyBCallback( + ispyb_gridscan_cb = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() @@ -924,7 +924,7 @@ def test_load_centre_collect_multisample_pin_reports_correct_sample_ids_in_ispyb fetch_datacollection_ids_for_group_id: Callable[..., Any], ): load_centre_collect_composite.zocalo.my_zocalo_result = zocalo_result - ispyb_gridscan_cb = GridscanISPyBCallback( + ispyb_gridscan_cb = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() @@ -990,7 +990,7 @@ def test_load_centre_collect_multisample_pin_reports_correct_sample_ids_robot_lo robot_load_cb: RobotLoadISPyBCallback, ): load_centre_collect_composite.zocalo.my_zocalo_result = zocalo_result - ispyb_gridscan_cb = GridscanISPyBCallback( + ispyb_gridscan_cb = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() @@ -1047,7 +1047,7 @@ def test_load_centre_collect_multisample_pin_updates_sample_status_for_parent_sa fetch_blsample: Callable[..., Any], ): load_centre_collect_composite.zocalo.my_zocalo_result = zocalo_result - ispyb_gridscan_cb = GridscanISPyBCallback( + ispyb_gridscan_cb = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() @@ -1175,7 +1175,7 @@ def test_load_centre_collect_generate_rotation_snapshots( SNAPSHOT_GENERATION_ZOCALO_RESULT ) - ispyb_gridscan_cb = GridscanISPyBCallback( + ispyb_gridscan_cb = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() diff --git a/tests/system_tests/hyperion/external_interaction/test_zocalo_system.py b/tests/system_tests/hyperion/external_interaction/test_zocalo_system.py index 29f70abc86..9b5972e807 100644 --- a/tests/system_tests/hyperion/external_interaction/test_zocalo_system.py +++ b/tests/system_tests/hyperion/external_interaction/test_zocalo_system.py @@ -13,7 +13,7 @@ from mx_bluesky.common.experiment_plans.inner_plans.read_hardware import ( read_hardware_for_zocalo, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( GridscanPlane, ispyb_activation_wrapper, ) diff --git a/tests/unit_tests/beamlines/i02_1/conftest.py b/tests/unit_tests/beamlines/i02_1/conftest.py new file mode 100644 index 0000000000..eb4f4e059f --- /dev/null +++ b/tests/unit_tests/beamlines/i02_1/conftest.py @@ -0,0 +1,41 @@ +import pytest +from dodal.beamlines import i02_1 + +from mx_bluesky.beamlines.i02_1.composites import I02_1FgsParams +from mx_bluesky.common.parameters.components import get_param_version + + +@pytest.fixture +def fgs_params_two_d(tmp_path) -> I02_1FgsParams: + return I02_1FgsParams( + x_start_um=0, + y_starts_um=[0], + z_starts_um=[0], + y_step_sizes_um=[10], + omega_starts_deg=[0], + parameter_model_version=get_param_version(), + sample_id=0, + visit="cm0000-0", + file_name="test_file", + storage_directory=str(tmp_path), + x_steps=5, + y_steps=[3], + path_to_xtal_snapshot=tmp_path, + beam_size_x=0, + beam_size_y=0, + microns_per_pixel_x=1, + microns_per_pixel_y=1, + upper_left_x=0, + upper_left_y=0, + detector_distance_mm=100, + ) + + +@pytest.fixture(autouse=True) +def always_use_i02_1_beamline(monkeypatch, patch_beamline_env_variable): + monkeypatch.setenv("BEAMLINE", "i02-1") + + +@pytest.fixture() +def goniometer(): + return i02_1.goniometer.build(connect_immediately=True, mock=True) diff --git a/tests/unit_tests/beamlines/i02_1/test_gridscan_ispyb_callback.py b/tests/unit_tests/beamlines/i02_1/test_gridscan_ispyb_callback.py new file mode 100644 index 0000000000..0ad3121a8e --- /dev/null +++ b/tests/unit_tests/beamlines/i02_1/test_gridscan_ispyb_callback.py @@ -0,0 +1,76 @@ +from mx_bluesky.beamlines.i02_1.composites import I02_1FgsParams +from mx_bluesky.beamlines.i02_1.external_interaction.callbacks.gridscan.ispyb_callback import ( + GridscanISPyBCallback, + _make_comment, +) +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_mapping import ( + construct_comment_for_gridscan, +) +from mx_bluesky.common.external_interaction.ispyb.data_model import ( + DataCollectionGridInfo, + DataCollectionGroupInfo, + DataCollectionInfo, + Orientation, + ScanDataInfo, +) +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import IspybIds + + +def _get_expected_scan_info(params: I02_1FgsParams, dcid: int): + dc_grid_info = DataCollectionGridInfo( + params.x_step_size_um / 1000, + params.y_step_sizes_um[0] / 1000, + params.x_steps, + params.y_steps[0], + params.microns_per_pixel_x, + params.microns_per_pixel_y, + params.upper_left_x, + params.upper_left_y, + Orientation.HORIZONTAL, + True, + ) + xtal = str(params.path_to_xtal_snapshot) + dc_info = DataCollectionInfo( + omega_start=0, + data_collection_number=1, + xtal_snapshot1=xtal, + xtal_snapshot2=xtal, + xtal_snapshot3=xtal, + n_images=params.x_steps * params.y_steps[0], + axis_end=0, + axis_range=0, + axis_start=0, + file_template=f"{params.file_name}_1_master.h5", + comments=construct_comment_for_gridscan(dc_grid_info), + ) + return [ + ScanDataInfo( + data_collection_info=dc_info, + data_collection_id=dcid, + data_collection_grid_info=dc_grid_info, + ) + ] + + +def test_get_scan_infos_gives_expected_output( + fgs_params_two_d: I02_1FgsParams, +): + callback = GridscanISPyBCallback(param_type=I02_1FgsParams) + callback.params = fgs_params_two_d + doc = {} + doc["data"] = { + "gonio-omega": 0, + } + callback.ispyb_ids = IspybIds() + callback.ispyb_ids.data_collection_group_id = 0 + callback.ispyb_ids.data_collection_ids = ((0),) + callback.data_collection_group_info = DataCollectionGroupInfo( + "0", + "SAD", + None, + comments=_make_comment(fgs_params_two_d.x_steps, fgs_params_two_d.y_steps[0]), + ) + scan_info = callback._get_scan_infos(doc) + assert scan_info[0].data_collection_grid_info + + assert scan_info == _get_expected_scan_info(fgs_params_two_d, 0) diff --git a/tests/unit_tests/beamlines/i02_1/test_i02_1_gridscan_plan.py b/tests/unit_tests/beamlines/i02_1/test_i02_1_gridscan_plan.py index fb6dff13c5..6ce0c06f9f 100644 --- a/tests/unit_tests/beamlines/i02_1/test_i02_1_gridscan_plan.py +++ b/tests/unit_tests/beamlines/i02_1/test_i02_1_gridscan_plan.py @@ -5,48 +5,82 @@ from dodal.beamlines import i02_1 from dodal.beamlines.i02_1 import ZebraFastGridScanTwoD from dodal.devices.attenuator.attenuator import ReadOnlyAttenuator +from dodal.devices.beamlines.i02_1.flux import Flux from dodal.devices.common_dcm import DoubleCrystalMonochromatorBase from dodal.devices.eiger import EigerDetector -from dodal.devices.flux import Flux -from dodal.devices.s4_slit_gaps import S4SlitGaps +from dodal.devices.motors import XYZWrappedOmegaStage +from dodal.devices.slits import Slits from dodal.devices.synchrotron import Synchrotron from dodal.devices.undulator import BaseUndulator from dodal.devices.zebra.zebra import Zebra from pydantic import ValidationError +from mx_bluesky.beamlines.i02_1.composites import I02_1FgsParams from mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan import ( + ExternalGridScanParams, I021FlyScanXRayCentreComposite, construct_i02_1_specific_features, i02_1_gridscan_plan, ) from mx_bluesky.beamlines.i02_1.parameters.gridscan import SpecifiedTwoDGridScan -from mx_bluesky.common.parameters.components import get_param_version -from mx_bluesky.common.parameters.device_composites import ( - GonioWithOmega, +from mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping import ( + populate_data_collection_group, + populate_remaining_data_collection_info, +) +from mx_bluesky.common.external_interaction.ispyb.data_model import ( + DataCollectionInfo, + ScanDataInfo, +) +from mx_bluesky.common.parameters.components import ( + IspybExperimentType, + get_param_version, ) @pytest.fixture -def fgs_params_two_d(tmp_path) -> SpecifiedTwoDGridScan: - return SpecifiedTwoDGridScan( - x_start_um=0, - y_starts_um=[0], - z_starts_um=[0], - y_step_sizes_um=[10], - omega_starts_deg=[0], - parameter_model_version=get_param_version(), - sample_id=0, - visit="visit", +def zebra_fgs_two_d() -> ZebraFastGridScanTwoD: + device = i02_1.zebra_fast_grid_scan.build(connect_immediately=True, mock=True) + + return device + + +@pytest.fixture +def entry_params(tmp_path) -> ExternalGridScanParams: + return ExternalGridScanParams( + visit="cm0000-0", file_name="test_file", storage_directory=str(tmp_path), + exposure_time_s=0.004, + snapshot_directory=tmp_path, + x_start_um=0, + y_start_um=0, + z_start_um=0, x_steps=5, - y_steps=[3], + y_steps=3, + beam_size_x=5, + beam_size_y=5, + microns_per_pixel_x=1, + microns_per_pixel_y=1, + upper_left_x=1, + upper_left_y=2, + detector_distance_mm=100, + sample_id=0, + x_step_size_um=20, + y_step_sizes_um=[10], + omega_start_deg=10, ) @pytest.fixture -def zebra_fgs_two_d() -> ZebraFastGridScanTwoD: - device = i02_1.zebra_fast_grid_scan.build(connect_immediately=True, mock=True) +def slits() -> Slits: + device = i02_1.s4_slit_gaps.build(connect_immediately=True, mock=True) + + return device + + +@pytest.fixture +def flux() -> Flux: + device = i02_1.flux.build(connect_immediately=True, mock=True) return device @@ -55,26 +89,26 @@ def zebra_fgs_two_d() -> ZebraFastGridScanTwoD: def fgs_composite( eiger: EigerDetector, synchrotron: Synchrotron, - smargon: GonioWithOmega, + goniometer: XYZWrappedOmegaStage, zebra_fgs_two_d: ZebraFastGridScanTwoD, dcm: DoubleCrystalMonochromatorBase, attenuator: ReadOnlyAttenuator, flux: Flux, undulator: BaseUndulator, - s4_slit_gaps: S4SlitGaps, + slits: Slits, zebra: Zebra, ) -> I021FlyScanXRayCentreComposite: return I021FlyScanXRayCentreComposite( eiger, synchrotron, - smargon, + goniometer, zebra, zebra_fgs_two_d, dcm, attenuator, flux, undulator, - s4_slit_gaps, + slits, ) @@ -96,7 +130,7 @@ def _check_lengths_are_same(self): # type: ignore def test_two_d_grid_scan_validation( y_starts_um: list[float], z_starts_um: list[float], - omega_starts_deg: list[float], + omega_starts_deg: list[int], y_step_sizes_um: list[float], y_steps: list[int], should_raise: bool, @@ -125,6 +159,10 @@ def create_params(): create_params() +@patch( + "mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan.create_gridscan_callbacks", + new=MagicMock(), +) @patch( "mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan.construct_i02_1_specific_features", ) @@ -135,15 +173,75 @@ def test_i02_1_flyscan_xray_centre_in_re( mock_common_scan: MagicMock, mock_create_features: MagicMock, run_engine: RunEngine, - fgs_params_two_d: SpecifiedTwoDGridScan, + fgs_params_two_d: I02_1FgsParams, + fgs_composite: I021FlyScanXRayCentreComposite, + entry_params: ExternalGridScanParams, +): + expected_fgs_params = fgs_params_two_d + expected_fgs_params.omega_starts_deg = [10] + expected_fgs_params.ispyb_experiment_type = IspybExperimentType.SAD + expected_fgs_params.use_roi_mode = False + expected_fgs_params.beam_size_x = 5 + expected_fgs_params.beam_size_y = 5 + expected_fgs_params.upper_left_x = 1 + expected_fgs_params.upper_left_y = 2 + expected_fgs_params.box_size_um = 2 + specific_features = construct_i02_1_specific_features( + fgs_composite, expected_fgs_params + ) + mock_create_features.return_value = specific_features + + run_engine(i02_1_gridscan_plan(entry_params, fgs_composite)) + + mock_common_scan.assert_called_once_with( + fgs_composite, expected_fgs_params, specific_features + ) + + +@patch( + "mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan.common_flyscan_xray_centre", + new=MagicMock(), +) +@patch( + "mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan.get_internal_params", +) +@patch( + "mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan.construct_i02_1_specific_features", +) +@patch( + "mx_bluesky.common.external_interaction.callbacks.grid.gridscan.ispyb_callback.StoreInIspyb" +) +def test_ispyb_activated_correct_params( + mock_store_ispyb: MagicMock, + mock_create_features: MagicMock, + mock_get_internal_params: MagicMock, + run_engine: RunEngine, + fgs_params_two_d: I02_1FgsParams, fgs_composite: I021FlyScanXRayCentreComposite, + entry_params: ExternalGridScanParams, ): + mock_ispyb = MagicMock() + mock_get_internal_params.return_value = fgs_params_two_d + + mock_store_ispyb.return_value = mock_ispyb expected_features = construct_i02_1_specific_features( fgs_composite, fgs_params_two_d ) + run_engine.md["data"] = {} mock_create_features.return_value = expected_features - run_engine(i02_1_gridscan_plan(fgs_params_two_d, fgs_composite)) - mock_common_scan.assert_called_once_with( - fgs_composite, fgs_params_two_d, expected_features + + run_engine(i02_1_gridscan_plan(entry_params, fgs_composite)) + expected_group_info = populate_data_collection_group(fgs_params_two_d) + expected_group_info.comments = f"Diffraction grid scan of {fgs_params_two_d.x_steps} by {fgs_params_two_d.y_steps[0]}.Zocalo processing took 0.00 s." + expected_scan_info = ScanDataInfo( + data_collection_info=populate_remaining_data_collection_info( + "MX-Bluesky: Xray centring 1/1 -", + None, + DataCollectionInfo(), + fgs_params_two_d, + ) + ) + mock_ispyb.begin_deposition.assert_called_once_with( + expected_group_info, [expected_scan_info] ) diff --git a/tests/unit_tests/common/experiment_plans/inner_plans/test_do_fgs.py b/tests/unit_tests/common/experiment_plans/inner_plans/test_do_fgs.py index 3375b850e3..cc4f0914e7 100644 --- a/tests/unit_tests/common/experiment_plans/inner_plans/test_do_fgs.py +++ b/tests/unit_tests/common/experiment_plans/inner_plans/test_do_fgs.py @@ -18,7 +18,7 @@ from mx_bluesky.common.experiment_plans.inner_plans.do_fgs import ( kickoff_and_complete_gridscan, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( GridscanPlane, ) from mx_bluesky.common.parameters.constants import ( diff --git a/tests/unit_tests/common/experiment_plans/test_common_flyscan_xray_centre_plan.py b/tests/unit_tests/common/experiment_plans/test_common_flyscan_xray_centre_plan.py index f04092662a..a5c367cd46 100644 --- a/tests/unit_tests/common/experiment_plans/test_common_flyscan_xray_centre_plan.py +++ b/tests/unit_tests/common/experiment_plans/test_common_flyscan_xray_centre_plan.py @@ -37,11 +37,11 @@ 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.grid.grid_detect_and_scan.ispyb_callback import ( + GridDetectAndScanISPyBCallback, ispyb_activation_wrapper, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback import ( GridscanNexusFileCallback, ) from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( @@ -78,7 +78,7 @@ def mock_plan(): def run_engine_with_subs_snapshots_already_taken(run_engine_with_subs, test_event_data): run_engine, subscriptions = run_engine_with_subs ispyb_gridscan_callback = [ - sub for sub in subscriptions if isinstance(sub, GridscanISPyBCallback) + sub for sub in subscriptions if isinstance(sub, GridDetectAndScanISPyBCallback) ][0] ispyb_gridscan_callback.active = True ispyb_gridscan_callback.start( @@ -94,7 +94,7 @@ def run_engine_with_subs_snapshots_already_taken(run_engine_with_subs, test_even @patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb", + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback.StoreInIspyb", modified_store_grid_scan_mock, ) class TestFlyscanXrayCentrePlan: @@ -122,7 +122,9 @@ def test_when_run_gridscan_called_ispyb_deposition_made_and_records_errors( test_three_d_grid_params: SpecifiedThreeDGridScan, beamline_specific: BeamlineSpecificFGSFeatures, ): - ispyb_callback = GridscanISPyBCallback(param_type=SpecifiedThreeDGridScan) + ispyb_callback = GridDetectAndScanISPyBCallback( + param_type=SpecifiedThreeDGridScan + ) run_engine.subscribe(ispyb_callback) error = None @@ -323,7 +325,7 @@ def test_when_grid_scan_ran_then_eiger_disarmed_before_zocalo_end( test_three_d_grid_params: SpecifiedThreeDGridScan, run_engine_with_subs_snapshots_already_taken: tuple[ RunEngine, - tuple[GridscanNexusFileCallback, GridscanISPyBCallback], + tuple[GridscanNexusFileCallback, GridDetectAndScanISPyBCallback], ], beamline_specific: BeamlineSpecificFGSFeatures, ): @@ -346,7 +348,7 @@ def test_when_grid_scan_ran_then_eiger_disarmed_before_zocalo_end( ) with patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter.create_nexus_file", + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback.NexusWriter.create_nexus_file", autospec=True, ): [run_engine.subscribe(cb) for cb in (nexus_cb, ispyb_cb)] @@ -586,7 +588,7 @@ def test_when_gridscan_succeeds_and_results_fetched_ispyb_comment_appended_to( run_gridscan: MagicMock, run_engine_with_subs: tuple[ RunEngine, - tuple[GridscanNexusFileCallback, GridscanISPyBCallback], + tuple[GridscanNexusFileCallback, GridDetectAndScanISPyBCallback], ], test_three_d_grid_params: SpecifiedThreeDGridScan, fake_fgs_composite: FlyScanEssentialDevices, diff --git a/tests/unit_tests/common/experiment_plans/test_common_grid_detect_then_xray_centre_plan.py b/tests/unit_tests/common/experiment_plans/test_common_grid_detect_then_xray_centre_plan.py index 8c39c374b0..ed020c32ad 100644 --- a/tests/unit_tests/common/experiment_plans/test_common_grid_detect_then_xray_centre_plan.py +++ b/tests/unit_tests/common/experiment_plans/test_common_grid_detect_then_xray_centre_plan.py @@ -25,7 +25,7 @@ from mx_bluesky.common.experiment_plans.inner_plans.xrc_results_utils import ( _fire_xray_centre_result_event, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( ispyb_activation_wrapper, ) from mx_bluesky.common.parameters.constants import ( @@ -274,7 +274,7 @@ def test_detect_grid_and_do_gridscan_does_not_activate_ispyb_callback( msg for msg in msgs if msg.command == "open_run" - and "GridscanISPyBCallback" in msg.kwargs["activate_callbacks"] + and "GridDetectAndScanISPyBCallback" in msg.kwargs["activate_callbacks"] ] assert not activations @@ -348,7 +348,7 @@ def test_grid_detect_then_xray_centre_activates_ispyb_callback( msgs_from_simulated_grid_detect_then_xray_centre, lambda msg: ( msg.command == "open_run" - and "GridscanISPyBCallback" in msg.kwargs["activate_callbacks"] + and "GridDetectAndScanISPyBCallback" in msg.kwargs["activate_callbacks"] ), ) diff --git a/tests/unit_tests/common/experiment_plans/test_grid_detection_plan.py b/tests/unit_tests/common/experiment_plans/test_grid_detection_plan.py index 2b26600875..d4c9394d59 100644 --- a/tests/unit_tests/common/experiment_plans/test_grid_detection_plan.py +++ b/tests/unit_tests/common/experiment_plans/test_grid_detection_plan.py @@ -28,8 +28,8 @@ from mx_bluesky.common.external_interaction.callbacks.common.grid_detection_callback import ( GridDetectionCallback, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( + GridDetectAndScanISPyBCallback, ispyb_activation_wrapper, ) from mx_bluesky.common.parameters.gridscan import GenericGrid, SpecifiedThreeDGridScan @@ -214,7 +214,7 @@ async def test_when_grid_detection_plan_run_then_ispyb_callback_gets_correct_val ConfigClient(""), "loopCentring", test_config_files["oav_config_json"] ) composite, _ = fake_devices - cb = GridscanISPyBCallback(param_type=GenericGrid) + cb = GridDetectAndScanISPyBCallback(param_type=GenericGrid) cb.data_collection_group_info = dummy_rotation_data_collection_group_info run_engine.subscribe(cb) @@ -228,7 +228,7 @@ async def test_when_grid_detection_plan_run_then_ispyb_callback_gets_correct_val assert_event( cb.activity_gated_start.mock_calls[0], # pyright:ignore - {"activate_callbacks": ["GridscanISPyBCallback"]}, + {"activate_callbacks": ["GridDetectAndScanISPyBCallback"]}, ) assert_event( cb.activity_gated_event.mock_calls[0], # pyright: ignore diff --git a/tests/unit_tests/common/external_interaction/callbacks/common/test_ispyb_callback_base.py b/tests/unit_tests/common/external_interaction/callbacks/common/test_ispyb_callback_base.py index b09ad0c957..b58e01755e 100644 --- a/tests/unit_tests/common/external_interaction/callbacks/common/test_ispyb_callback_base.py +++ b/tests/unit_tests/common/external_interaction/callbacks/common/test_ispyb_callback_base.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, call, patch import bluesky.preprocessors as bpp import pytest @@ -58,3 +58,51 @@ def test_plan(): with pytest.raises(ValueError): run_engine(test_plan()) + + +def _get_working_doc(): + return { + "data": { + "flux-flux_reading": 0, + "eiger-ispyb_detector_id": 0, + "eiger_cam_roi_mode": None, + "attenuator-actual_transmission": None, + } + } + + +@patch( + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base.ISPYB_ZOCALO_CALLBACK_LOGGER" +) +def test_handle_ispyb_transmission_flux_read_if_no_beamsize_warning( + mock_logger: MagicMock, + test_three_d_grid_params: SpecifiedThreeDGridScan, +): + callback = BaseISPyBCallback() + callback.params = test_three_d_grid_params + doc = _get_working_doc() + callback._handle_ispyb_transmission_flux_read(doc) # type: ignore + mock_logger.warning.assert_has_calls( + [call("ISPyB callbacks couldn't get beamsize")] + ) + + +@patch( + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base.ISPYB_ZOCALO_CALLBACK_LOGGER" +) +def test_handle_ispyb_transmission_flux_read_if_params_specify_beamsize( + mock_logger: MagicMock, + test_three_d_grid_params: SpecifiedThreeDGridScan, +): + test_three_d_grid_params.beam_size_x = 0 # type: ignore + test_three_d_grid_params.beam_size_y = 1 # type: ignore + + callback = BaseISPyBCallback() + callback.params = test_three_d_grid_params + doc = _get_working_doc() + callback._handle_ispyb_transmission_flux_read(doc) # type: ignore + + assert ( + call("ISPyB callbacks couldn't get beamsize") + not in mock_logger.warning.call_args_list + ) diff --git a/tests/unit_tests/common/external_interaction/callbacks/grid/__init__.py b/tests/unit_tests/common/external_interaction/callbacks/grid/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/common/external_interaction/callbacks/grid/test_ispyb_gridscan_callback.py b/tests/unit_tests/common/external_interaction/callbacks/grid/test_ispyb_gridscan_callback.py new file mode 100644 index 0000000000..10b15e7aa5 --- /dev/null +++ b/tests/unit_tests/common/external_interaction/callbacks/grid/test_ispyb_gridscan_callback.py @@ -0,0 +1,112 @@ +from collections.abc import Sequence +from unittest.mock import MagicMock, patch + +import pytest +from event_model import RunStop + +from mx_bluesky.common.external_interaction.callbacks.grid.gridscan.ispyb_callback import ( + GridscanISPyBCallback, +) +from mx_bluesky.common.external_interaction.ispyb.data_model import ( + DataCollectionGroupInfo, + DataCollectionInfo, + ScanDataInfo, +) +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( + IspybIds, + StoreInIspyb, +) +from mx_bluesky.common.parameters.constants import PlanNameConstants +from mx_bluesky.common.parameters.gridscan import ( + SpecifiedThreeDGridScan, +) +from mx_bluesky.common.utils.exceptions import ISPyBDepositionNotMadeError + + +class Callback(GridscanISPyBCallback): + def _get_scan_infos(self, doc) -> Sequence[ScanDataInfo]: + return [ScanDataInfo(data_collection_info=DataCollectionInfo())] + + +@patch( + "mx_bluesky.common.external_interaction.callbacks.grid.gridscan.ispyb_callback.BaseISPyBCallback.activity_gated_start" +) +def test_gridscan_callback_start_calls_correct_funcs( + mock_start: MagicMock, test_three_d_grid_params: SpecifiedThreeDGridScan +): + cb = Callback(SpecifiedThreeDGridScan) + cb.fill_gridscan_deposition_and_store = MagicMock() + doc = { + "subplan_name": PlanNameConstants.TRIGGER_GRIDSCAN_ISPYB_CALLBACK, + "mx_bluesky_parameters": test_three_d_grid_params.model_dump_json(), + } + cb.activity_gated_start(doc) # type: ignore + cb.fill_gridscan_deposition_and_store.assert_called_once() + mock_start.assert_called_once() + + +def test_populate_info_for_update(test_three_d_grid_params: SpecifiedThreeDGridScan): + cb = Callback(SpecifiedThreeDGridScan) + cb.params = test_three_d_grid_params + cb.ispyb_ids = IspybIds(data_collection_ids=(0, 1)) + cb._grid_num_to_id_map = {0: 0, 1: 1} + es_dcid = DataCollectionInfo() + infos = cb.populate_info_for_update(es_dcid, None, test_three_d_grid_params) + assert infos == [ + ScanDataInfo(data_collection_id=0, data_collection_info=DataCollectionInfo()), + ScanDataInfo(data_collection_id=1, data_collection_info=DataCollectionInfo()), + ] + + +def test_stop_errors_if_empty_ispyb_id(): + cb = Callback(SpecifiedThreeDGridScan) + cb.ispyb_ids = IspybIds() + cb.data_collection_group_info = DataCollectionGroupInfo("", "", None) + doc: RunStop = { + "time": 0, + "uid": "0", + "exit_status": "success", + "run_start": None, # type: ignore + } + with pytest.raises(ISPyBDepositionNotMadeError): + cb.activity_gated_stop(doc) + + +def test_exception_added_onto_comments(): + cb = Callback(SpecifiedThreeDGridScan) + cb.ispyb = StoreInIspyb("") + cb.ispyb.update_data_collection_group_table = MagicMock() + cb.ispyb_ids = IspybIds(data_collection_ids=(0,)) + cb.data_collection_group_info = DataCollectionGroupInfo("", "", None) + reason = "test reason" + doc: RunStop = { + "time": 0, + "uid": "0", + "exit_status": "success", + "run_start": None, # type: ignore + "reason": f"[test]: {reason}", + } + cb.activity_gated_stop(doc) + cb.ispyb.update_data_collection_group_table.assert_called_once_with( + DataCollectionGroupInfo("", "", None, comments=reason), None + ) + + +@patch( + "mx_bluesky.common.external_interaction.callbacks.grid.gridscan.ispyb_callback.StoreInIspyb" +) +def test_fill_gridscan_deposition_and_store( + mock_store: MagicMock, + test_three_d_grid_params: SpecifiedThreeDGridScan, +): + cb = Callback(SpecifiedThreeDGridScan) + cb.params = test_three_d_grid_params + ispyb = StoreInIspyb("") + ispyb.begin_deposition = MagicMock() + ispyb.update_deposition = MagicMock() + ispyb.update_data_collection_group_table = MagicMock() + mock_store.return_value = ispyb + cb.fill_gridscan_deposition_and_store(MagicMock()) + ispyb.begin_deposition.assert_called_once() + ispyb.update_deposition.assert_called_once() + ispyb.update_data_collection_group_table.assert_called_once() diff --git a/tests/unit_tests/common/external_interaction/callbacks/grid/test_utils.py b/tests/unit_tests/common/external_interaction/callbacks/grid/test_utils.py new file mode 100644 index 0000000000..9cb8718504 --- /dev/null +++ b/tests/unit_tests/common/external_interaction/callbacks/grid/test_utils.py @@ -0,0 +1,28 @@ +from bluesky.run_engine import RunEngine +from dodal.devices.zocalo import ZocaloStartInfo + +from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( + generate_start_info_from_num_grids, +) +from mx_bluesky.common.parameters.gridscan import SpecifiedThreeDGridScan + + +def test_generate_start_info_from_num_grids( + test_three_d_grid_params: SpecifiedThreeDGridScan, run_engine: RunEngine +): + zocalo_info_gen = generate_start_info_from_num_grids(test_three_d_grid_params) + next(zocalo_info_gen) + infos = zocalo_info_gen.send({"_grid_num_to_id_map": {0: 0, 1: 1, 2: 2}}) + + expected_infos = [ + ZocaloStartInfo( + ispyb_dcid=num, + filename=None, + start_frame_index=test_three_d_grid_params.scan_indices[num], + number_of_frames=len(test_three_d_grid_params.scan_points[num]), + message_index=num, + ) + for num in range(test_three_d_grid_params.num_grids) + ] + + assert infos == expected_infos diff --git a/tests/unit_tests/common/external_interaction/callbacks/test_zocalo_handler.py b/tests/unit_tests/common/external_interaction/callbacks/test_zocalo_handler.py index aca1a0eb62..705e95ae9d 100644 --- a/tests/unit_tests/common/external_interaction/callbacks/test_zocalo_handler.py +++ b/tests/unit_tests/common/external_interaction/callbacks/test_zocalo_handler.py @@ -86,10 +86,10 @@ def test_handler_stores_collection_ispyb_ids_come_in_as_subplan( autospec=True, ) @patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter", + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback.NexusWriter", ) @patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb", + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback.StoreInIspyb", ) def test_execution_of_do_fgs_triggers_zocalo_calls( self, @@ -156,10 +156,10 @@ def test_execution_of_do_fgs_triggers_zocalo_calls( autospec=True, ) @patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter", + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback.NexusWriter", ) @patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb", + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback.StoreInIspyb", ) def test_do_fgs_triggers_zocalo_calls_when_snapshots_in_reverse_order( self, diff --git a/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_callback.py b/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_callback.py index 95115e82fa..ecd459c8d9 100644 --- a/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_callback.py +++ b/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_callback.py @@ -10,8 +10,8 @@ from mx_bluesky.common.experiment_plans.inner_plans.read_hardware import ( read_hardware_plan, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( + GridDetectAndScanISPyBCallback, GridscanPlane, _smargon_omega_to_xyxz_plane, ) @@ -73,7 +73,7 @@ ) class TestXrayCentreISPyBCallback: def test_activity_gated_start_3d(self, mock_ispyb_conn, test_event_data, tmp_path): - callback = GridscanISPyBCallback( + callback = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) callback.activity_gated_start( @@ -116,7 +116,7 @@ def test_activity_gated_start_3d(self, mock_ispyb_conn, test_event_data, tmp_pat def test_reason_provided_if_crystal_not_found_error( self, mock_update_data_collection_group_table, mock_ispyb_conn, test_event_data ): - callback = GridscanISPyBCallback( + callback = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) callback.activity_gated_start( @@ -137,7 +137,7 @@ def test_reason_provided_if_crystal_not_found_error( ) def test_hardware_read_event_3d(self, mock_ispyb_conn, test_event_data): - callback = GridscanISPyBCallback( + callback = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) callback.activity_gated_start( @@ -166,7 +166,7 @@ def test_hardware_read_event_3d(self, mock_ispyb_conn, test_event_data): assert update_dc_requests[1].body == expected_upsert def test_flux_read_events_3d(self, mock_ispyb_conn, test_event_data): - callback = GridscanISPyBCallback( + callback = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) callback.activity_gated_start( @@ -231,7 +231,7 @@ def test_activity_gated_event_oav_snapshot_triggered( snapshot_events: list[str], first_comment: str, ): - callback = GridscanISPyBCallback( + callback = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) callback.activity_gated_start( @@ -334,7 +334,7 @@ def test_activity_gated_event_oav_snapshot_triggered( async def test_ispyb_callback_handles_read_hardware_in_run_engine( self, run_engine, mock_ispyb_conn, dummy_rotation_data_collection_group_info ): - callback = GridscanISPyBCallback( + callback = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) callback._handle_ispyb_hardware_read = MagicMock() @@ -349,7 +349,7 @@ async def test_ispyb_callback_handles_read_hardware_in_run_engine( @subs_decorator(callback) @run_decorator( md={ - "activate_callbacks": ["GridscanISPyBCallback"], + "activate_callbacks": ["GridDetectAndScanISPyBCallback"], }, ) def test_plan(): @@ -366,7 +366,7 @@ def test_plan(): callback._handle_ispyb_transmission_flux_read.assert_called_once() @patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.GridscanISPyBCallback._handle_oav_grid_snapshot_triggered", + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback.GridDetectAndScanISPyBCallback._handle_oav_grid_snapshot_triggered", ) @patch( "mx_bluesky.common.external_interaction.ispyb.ispyb_store.StoreInIspyb.update_deposition", @@ -381,7 +381,7 @@ def test_given_event_doc_before_start_doc_received_then_exception_raised( mock__handle_oav_grid_snapshot_triggered, test_event_data, ): - callback = GridscanISPyBCallback( + callback = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) callback.activity_gated_descriptor( @@ -400,7 +400,7 @@ def test_given_event_doc_before_start_doc_received_then_exception_raised( def test_ispyb_callback_clears_state_after_run_stop( self, test_event_data, mock_ispyb_conn ): - callback = GridscanISPyBCallback( + callback = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) callback.active = True diff --git a/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_handler.py b/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_handler.py index 1abe4d71a5..9e6359da6b 100644 --- a/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_handler.py +++ b/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_handler.py @@ -3,8 +3,8 @@ import pytest from graypy import GELFTCPHandler -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( + GridDetectAndScanISPyBCallback, ) from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( IspybIds, @@ -48,12 +48,12 @@ def mock_store_in_ispyb(config, *args, **kwargs) -> StoreInIspyb: MagicMock(return_value=td.DUMMY_TIME_STRING), ) @patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb", + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback.StoreInIspyb", mock_store_in_ispyb, ) class TestXrayCentreIspybHandler: def test_fgs_failing_results_in_bad_run_status_in_ispyb(self, test_event_data): - ispyb_handler = GridscanISPyBCallback( + ispyb_handler = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) ispyb_handler.activity_gated_start( @@ -88,7 +88,7 @@ def test_fgs_failing_results_in_bad_run_status_in_ispyb(self, test_event_data): def test_fgs_raising_no_exception_results_in_good_run_status_in_ispyb( self, test_event_data ): - ispyb_handler = GridscanISPyBCallback( + ispyb_handler = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) ispyb_handler.activity_gated_start( @@ -134,7 +134,7 @@ def test_given_ispyb_callback_started_writing_to_ispyb_when_messages_logged_then ) gelf_handler.emit = MagicMock() - ispyb_handler = GridscanISPyBCallback( + ispyb_handler = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) ispyb_handler.activity_gated_start( @@ -170,7 +170,7 @@ def test_given_ispyb_callback_finished_writing_to_ispyb_when_messages_logged_the ) gelf_handler.emit = MagicMock() - ispyb_handler = GridscanISPyBCallback( + ispyb_handler = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams ) ispyb_handler.activity_gated_start( @@ -197,13 +197,17 @@ def test_given_ispyb_callback_finished_writing_to_ispyb_when_messages_logged_the assert not hasattr(latest_record, "dc_group_id") @patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.time", - side_effect=[2, 100], + "mx_bluesky.common.external_interaction.callbacks.grid.utils.time", + new=MagicMock(side_effect=[100]), + ) + @patch( + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback.time", + new=MagicMock(side_effect=[2]), ) def test_given_fgs_plan_finished_when_zocalo_results_event_then_expected_comment_deposited( - self, mock_time, dummy_rotation_data_collection_group_info, test_event_data + self, dummy_rotation_data_collection_group_info, test_event_data ): - ispyb_handler = GridscanISPyBCallback( + ispyb_handler = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams, ) diff --git a/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_mapping.py b/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_mapping.py index 93d5fc510c..1ab24436f2 100644 --- a/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_mapping.py +++ b/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_mapping.py @@ -2,7 +2,7 @@ import pytest -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_mapping import ( construct_comment_for_gridscan, ) from mx_bluesky.common.external_interaction.ispyb.data_model import ( @@ -64,7 +64,7 @@ def test_ispyb_deposition_rounds_position_to_int( ], ) @patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping.oav_utils.bottom_right_from_top_left", + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_mapping.oav_utils.bottom_right_from_top_left", autospec=True, ) def test_ispyb_deposition_rounds_box_size_int( diff --git a/tests/unit_tests/common/external_interaction/xray_centre/test_nexus_handler.py b/tests/unit_tests/common/external_interaction/xray_centre/test_nexus_handler.py index c5f970bb05..a48bf30fd6 100644 --- a/tests/unit_tests/common/external_interaction/xray_centre/test_nexus_handler.py +++ b/tests/unit_tests/common/external_interaction/xray_centre/test_nexus_handler.py @@ -5,7 +5,7 @@ import pytest from numpy.typing import DTypeLike -from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback import ( GridscanNexusFileCallback, ) from mx_bluesky.hyperion.parameters.gridscan import HyperionSpecifiedThreeDGridScan @@ -34,7 +34,7 @@ def test_writers_not_called_on_plan_start_doc( @patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter" + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback.NexusWriter" ) def test_writers_dont_create_on_init_but_do_on_during_collection_read_event( mock_nexus_writer: MagicMock, @@ -73,7 +73,7 @@ def test_writers_dont_create_on_init_but_do_on_during_collection_read_event( ], ) @patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter" + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback.NexusWriter" ) def test_given_different_bit_depths_then_writers_created_wth_correct_virtual_dataset_size( mock_nexus_writer: MagicMock, @@ -108,7 +108,7 @@ def test_given_different_bit_depths_then_writers_created_wth_correct_virtual_dat @patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter" + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback.NexusWriter" ) def test_beam_and_attenuator_set_on_ispyb_transmission_event( mock_nexus_writer: MagicMock, diff --git a/tests/unit_tests/common/parameters/test_gridscan.py b/tests/unit_tests/common/parameters/test_gridscan.py index 60e592e15d..8eb612f4a1 100644 --- a/tests/unit_tests/common/parameters/test_gridscan.py +++ b/tests/unit_tests/common/parameters/test_gridscan.py @@ -34,7 +34,7 @@ def fast_gridscan_params(): ... # type: ignore def test_specified_grids_validation_error( y_starts_um: list[float], z_starts_um: list[float], - omega_starts_deg: list[float], + omega_starts_deg: list[int], y_step_sizes_um: list[float], y_steps: list[int], should_raise: bool, @@ -82,7 +82,7 @@ def _check_lengths_are_same(self): # type: ignore def test_three_d_grid_scan_validation( y_starts_um: list[float], z_starts_um: list[float], - omega_starts_deg: list[float], + omega_starts_deg: list[int], y_step_sizes_um: list[float], y_steps: list[int], should_raise: bool, diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 9c8e659909..b69810847d 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -50,13 +50,15 @@ 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, - generate_start_info_from_omega_map, +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( + GridDetectAndScanISPyBCallback, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback import ( GridscanNexusFileCallback, ) +from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( + generate_start_info_from_omega_map, +) from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionGroupInfo, ) @@ -67,6 +69,7 @@ from mx_bluesky.common.parameters.constants import ( DocDescriptorNames, EnvironmentConstants, + GridscanParamConstants, PlanNameConstants, ) from mx_bluesky.common.parameters.device_composites import ( @@ -185,16 +188,18 @@ def assert_event(mock_call, expected): def create_gridscan_callbacks() -> tuple[ - GridscanNexusFileCallback, GridscanISPyBCallback + GridscanNexusFileCallback, GridDetectAndScanISPyBCallback ]: return ( GridscanNexusFileCallback(param_type=SpecifiedThreeDGridScan), - GridscanISPyBCallback( + GridDetectAndScanISPyBCallback( param_type=SpecifiedThreeDGridScan, emit=ZocaloCallback( PlanNameConstants.DO_FGS, EnvironmentConstants.ZOCALO_ENV, - generate_start_info_from_omega_map, + lambda: generate_start_info_from_omega_map( + [GridscanParamConstants.OMEGA_1, GridscanParamConstants.OMEGA_2] + ), ), ), ) @@ -225,7 +230,7 @@ def mock_subscriptions(test_three_d_grid_params): "mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback.ZocaloTrigger", ), patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb" + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback.StoreInIspyb" ) as mock_store_in_ispyb, ): mock_store_in_ispyb.return_value.begin_deposition.return_value = IspybIds( @@ -242,13 +247,17 @@ def mock_subscriptions(test_three_d_grid_params): yield (nexus_callback, ispyb_callback) -ReWithSubs = tuple[RunEngine, tuple[GridscanNexusFileCallback | GridscanISPyBCallback]] +ReWithSubs = tuple[ + RunEngine, tuple[GridscanNexusFileCallback | GridDetectAndScanISPyBCallback] +] @pytest.fixture def run_engine_with_subs( run_engine: RunEngine, - mock_subscriptions: tuple[GridscanNexusFileCallback | GridscanISPyBCallback], + mock_subscriptions: tuple[ + GridscanNexusFileCallback | GridDetectAndScanISPyBCallback + ], ) -> Generator[ReWithSubs, Any, None]: for cb in list(mock_subscriptions): run_engine.subscribe(cb) @@ -286,7 +295,7 @@ def make_event_doc(data, descriptor="abc123") -> Event: def run_generic_ispyb_handler_setup( - ispyb_handler: GridscanISPyBCallback, + ispyb_handler: GridDetectAndScanISPyBCallback, params: SpecifiedThreeDGridScan, ): """This is useful when testing 'run_gridscan_and_move(...)' because this stuff diff --git a/tests/unit_tests/hyperion/experiment_plans/conftest.py b/tests/unit_tests/hyperion/experiment_plans/conftest.py index 5fd785e3f0..9a9cd61fcc 100644 --- a/tests/unit_tests/hyperion/experiment_plans/conftest.py +++ b/tests/unit_tests/hyperion/experiment_plans/conftest.py @@ -114,10 +114,10 @@ def mock_subscriptions(): autospec=True, ), patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb.append_to_comment" + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback.StoreInIspyb.append_to_comment" ), patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb.begin_deposition", + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback.StoreInIspyb.begin_deposition", new=MagicMock( return_value=IspybIds( data_collection_ids=(0, 0), data_collection_group_id=0 @@ -125,7 +125,7 @@ def mock_subscriptions(): ), ), patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb.update_deposition", + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback.StoreInIspyb.update_deposition", new=MagicMock( return_value=IspybIds( data_collection_ids=(0, 0), diff --git a/tests/unit_tests/hyperion/experiment_plans/test_hyperion_flyscan_xray_centre_plan.py b/tests/unit_tests/hyperion/experiment_plans/test_hyperion_flyscan_xray_centre_plan.py index 70413e9b45..c4dcfeb5c4 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_hyperion_flyscan_xray_centre_plan.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_hyperion_flyscan_xray_centre_plan.py @@ -18,10 +18,10 @@ BeamlineSpecificFGSFeatures, common_flyscan_xray_centre, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( + GridDetectAndScanISPyBCallback, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback import ( GridscanNexusFileCallback, ) from mx_bluesky.common.parameters.constants import ( @@ -47,7 +47,9 @@ modified_store_grid_scan_mock, ) -ReWithSubs = tuple[RunEngine, tuple[GridscanNexusFileCallback, GridscanISPyBCallback]] +ReWithSubs = tuple[ + RunEngine, tuple[GridscanNexusFileCallback, GridDetectAndScanISPyBCallback] +] class CompleteError(Exception): @@ -70,7 +72,7 @@ def fgs_composite_with_panda_pcap( @patch( - "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb", + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback.StoreInIspyb", modified_store_grid_scan_mock, ) class TestFlyscanXrayCentrePlan: diff --git a/tests/unit_tests/hyperion/experiment_plans/test_pin_centre_then_xray_centre_plan.py b/tests/unit_tests/hyperion/experiment_plans/test_pin_centre_then_xray_centre_plan.py index 0331eea131..b63c61beb6 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_pin_centre_then_xray_centre_plan.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_pin_centre_then_xray_centre_plan.py @@ -159,7 +159,7 @@ def test_pin_centre_then_gridscan_plan_activates_ispyb_callback_before_pin_tip_c msgs, lambda msg: ( msg.command == "open_run" - and "GridscanISPyBCallback" in msg.kwargs["activate_callbacks"] + and "GridDetectAndScanISPyBCallback" in msg.kwargs["activate_callbacks"] ), ) msgs = assert_message_and_return_remaining( diff --git a/tests/unit_tests/hyperion/external_interaction/nexus/test_write_nexus.py b/tests/unit_tests/hyperion/external_interaction/nexus/test_write_nexus.py index 694f248fc3..5bb9f813b8 100644 --- a/tests/unit_tests/hyperion/external_interaction/nexus/test_write_nexus.py +++ b/tests/unit_tests/hyperion/external_interaction/nexus/test_write_nexus.py @@ -16,7 +16,7 @@ ZebraGridScanParamsThreeD, ) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback import ( _create_writers_from_params, ) from mx_bluesky.common.external_interaction.nexus.nexus_utils import ( diff --git a/uv.lock b/uv.lock index bdd785690d..a932ab5ee9 100644 --- a/uv.lock +++ b/uv.lock @@ -807,8 +807,8 @@ wheels = [ [[package]] name = "dls-dodal" -version = "2.3.1.dev8+gca163b99a" -source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#ca163b99a719b006e844067e724bf8c103615b83" } +version = "2.3.1.dev11+g0f2534ce9" +source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#0f2534ce926a173dba046352ea258a97b90176c2" } dependencies = [ { name = "aiofiles", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "aiohttp", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },