From a94ab9819b04919a006f27d9f35a8e3406e60137 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Mon, 9 Feb 2026 17:35:55 +0000 Subject: [PATCH 01/45] WIP begin making callbacks generic --- .../callbacks/xray_centre/nexus_callback.py | 15 +++-- .../common/parameters/components.py | 32 +++++----- src/mx_bluesky/common/parameters/gridscan.py | 63 ++++++++++++++----- 3 files changed, 73 insertions(+), 37 deletions(-) diff --git a/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py index 1a065c48d7..3752db323a 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py @@ -12,14 +12,14 @@ from mx_bluesky.common.external_interaction.nexus.write_nexus import NexusWriter from mx_bluesky.common.parameters.constants import DocDescriptorNames, PlanNameConstants from mx_bluesky.common.parameters.gridscan import ( - SpecifiedThreeDGridScan, + SpecifiedGrid, ) from mx_bluesky.common.utils.log import NEXUS_LOGGER if TYPE_CHECKING: from event_model.documents import Event, EventDescriptor, RunStart -T = TypeVar("T", bound="SpecifiedThreeDGridScan") +T = TypeVar("T", bound="SpecifiedGrid") class GridscanNexusFileCallback(PlanReactiveCallback): @@ -39,14 +39,13 @@ class GridscanNexusFileCallback(PlanReactiveCallback): See: https://blueskyproject.io/bluesky/callbacks.html#ways-to-invoke-callbacks """ - def __init__(self, param_type: type[T]) -> None: + def __init__(self, param_type: type[T], num_writers: int) -> None: super().__init__(NEXUS_LOGGER) self.param_type = param_type self.run_start_uid: str | None = None - self.nexus_writer_1: NexusWriter | None = None - self.nexus_writer_2: NexusWriter | None = None - self.descriptors: dict[str, EventDescriptor] = {} + self.num_writers: int = num_writers self.log = NEXUS_LOGGER + self.writers: list[NexusWriter] = [] def activity_gated_start(self, doc: RunStart): if doc.get("subplan_name") == PlanNameConstants.GRIDSCAN_OUTER: @@ -62,6 +61,10 @@ def activity_gated_start(self, doc: RunStart): data_shape_1 = (grid_n_img_1, d_size.width, d_size.height) data_shape_2 = (grid_n_img_2, d_size.width, d_size.height) run_number_2 = parameters.detector_params.run_number + 1 + + for writer in range(self.num_writers): + self._writers.append(NexusWriter) + self.nexus_writer_1 = NexusWriter( parameters, data_shape_1, parameters.scan_points_first_grid ) diff --git a/src/mx_bluesky/common/parameters/components.py b/src/mx_bluesky/common/parameters/components.py index e80f6ed8ee..7fda4a7833 100644 --- a/src/mx_bluesky/common/parameters/components.py +++ b/src/mx_bluesky/common/parameters/components.py @@ -96,6 +96,10 @@ class IspybExperimentType(StrEnum): GRIDSCAN_3D = "Mesh3D" +class WithNexusWriter(BaseModel): + indices_per_writer: tuple[int] + + class MxBlueskyParameters(BaseModel): model_config = ConfigDict( extra="allow", @@ -269,28 +273,28 @@ def selection_params(self) -> MultiXtalSelection: class OptionalXyzStarts(BaseModel): - x_start_um: float | None = None - y_start_um: float | None = None - z_start_um: float | None = None + x_starts_um: list[float | None] | None = None + y_starts_um: list[float | None] + z_starts_um: list[float | None] class XyzStarts(BaseModel): - x_start_um: float - y_start_um: float - z_start_um: float + x_starts_um: list[float] + y_starts_um: list[float] + z_starts_um: list[float] - def _start_for_axis(self, axis: XyzAxis) -> float: + def _start_for_axis(self, axis: XyzAxis, grid: int) -> float: match axis: case XyzAxis.X: - return self.x_start_um + return self.x_starts_um[grid] case XyzAxis.Y: - return self.y_start_um + return self.y_starts_um[grid] case XyzAxis.Z: - return self.z_start_um + return self.z_starts_um[grid] class OptionalGonioAngleStarts(BaseModel): - omega_start_deg: float | None = None - phi_start_deg: float | None = None - chi_start_deg: float | None = None - kappa_start_deg: float | None = None + omega_starts_deg: list[float | None] + phi_starts_deg: list[float | None] + chi_starts_deg: list[float | None] + kappa_starts_deg: list[float | None] diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 1b36482160..f6617e4d5a 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -107,17 +107,30 @@ def detector_params(self): ) -class SpecifiedGrid(GridCommon, XyzStarts, WithScan, Generic[GridScanParamType]): +class SpecifiedGrids(GridCommon, XyzStarts, WithScan, Generic[GridScanParamType]): """A specified grid is one which has defined values for the start position, grid and box sizes, etc., as opposed to parameters for a plan which will create those parameters at some point (e.g. through optical pin detection).""" - grid1_omega_deg: float = Field(default=GridscanParamConstants.OMEGA_1) - x_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM) - y_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM) - x_steps: int = Field(gt=0) - y_steps: int = Field(gt=0) + omega_deg_per_grid: list[float] = Field( + default=[GridscanParamConstants.OMEGA_1, GridscanParamConstants.OMEGA_2] + ) + x_step_sizes_um: list[float] = Field( + default=[ + GridscanParamConstants.BOX_WIDTH_UM, + GridscanParamConstants.BOX_WIDTH_UM, + ] + ) + y_step_sizes_um: list[float] = Field( + default=[ + GridscanParamConstants.BOX_WIDTH_UM, + GridscanParamConstants.BOX_WIDTH_UM, + ] + ) + x_steps: list[int] = Field(gt=0) + y_steps: list[int] = Field(gt=0) _set_stub_offsets: bool = PrivateAttr(default_factory=lambda: False) + # TODO validate that all the "per grid" things are the same length. _num_grids property can just print out length of this @property @abstractmethod @@ -126,22 +139,38 @@ def fast_gridscan_params(self) -> GridScanParamType: ... def do_set_stub_offsets(self, value: bool): self._set_stub_offsets = value + def _num_grids(self): + return len(self.x_steps) + @property - def grid_1_spec(self): - x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1) - y1_end = self.y_start_um + self.y_step_size_um * (self.y_steps - 1) - grid_1_x = Line("sam_x", self.x_start_um, x_end, self.x_steps) - grid_1_y = Line("sam_y", self.y_start_um, y1_end, self.y_steps) - grid_1_z = Static("sam_z", self.z_start_um) - return grid_1_y.zip(grid_1_z) * ~grid_1_x + def grid_specs(self) -> list[Product[str]]: + _grid_specs = [] + for idx in range(self._num_grids()): + x_end = self.x_starts_um[idx] + self.x_step_sizes_um[idx] * ( + self.x_step_sizes_um[idx] - 1 + ) + y_end = self.y_starts_um[idx] + self.y_step_sizes_um[idx] * ( + self.y_step_sizes_um[idx] - 1 + ) + grid_x = Line("sam_x", self.x_starts_um[idx], x_end, self.x_steps[idx]) + grid_y = Line("sam_y", self.y_starts_um[idx], y_end, self.y_steps[idx]) + grid_z = Static("sam_z", self.z_starts_um[idx]) + _grid_specs.append(grid_y.zip(grid_z) * ~grid_x) + return _grid_specs @property def scan_indices(self) -> list[int]: """The first index of each gridscan, useful for writing nexus files/VDS""" - return [ - 0, - len(ScanPath(self.grid_1_spec.calculate()).consume().midpoints["sam_x"]), - ] + _scan_indices = [0] + for idx in range(self._num_grids()): + _scan_indices.append( + len( + ScanPath(self.grid_specs[idx].calculate()) + .consume() + .midpoints["sam_x"] + ) + ) + return _scan_indices @property @abstractmethod From 5a2c51fb7e486e1a72d594bfaa67e85ea71fd38a Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Wed, 11 Feb 2026 11:16:41 +0000 Subject: [PATCH 02/45] wip --- .../hyperion/reference/gridscan.puml | 12 +- .../beamlines/i02_1/parameters/gridscan.py | 19 +- .../i04_grid_detect_then_xray_centre_plan.py | 14 +- .../common_flyscan_xray_centre_plan.py | 3 +- ...ommon_grid_detect_then_xray_centre_plan.py | 8 +- .../experiment_plans/inner_plans/do_fgs.py | 8 +- .../common/grid_detection_callback.py | 24 +- .../callbacks/xray_centre/ispyb_callback.py | 4 +- .../callbacks/xray_centre/nexus_callback.py | 48 ++-- .../common/parameters/components.py | 28 ++- src/mx_bluesky/common/parameters/gridscan.py | 226 ++++++++++++------ src/mx_bluesky/common/parameters/rotation.py | 9 +- .../load_centre_collect_full_plan.py | 7 +- .../callbacks/__main__.py | 4 +- .../callbacks/rotation/nexus_callback.py | 4 +- .../hyperion/parameters/gridscan.py | 46 ++-- .../hyperion/parameters/robot_load.py | 4 +- tests/conftest.py | 12 +- .../test_ispyb_dev_connection.py | 4 +- .../test_load_centre_collect_full_plan.py | 30 +-- ...d_test_specified_three_d_grid_params.json} | 24 +- .../test_gridscan_param_defaults.json | 24 +- ...t_i04_grid_detect_then_xray_centre_plan.py | 61 ++--- .../inner_plans/test_do_fgs.py | 2 + .../inner_plans/test_read_hardware.py | 4 +- .../test_common_flyscan_xray_centre_plan.py | 90 ++++--- .../test_grid_detection_plan.py | 18 +- .../common/test_ispyb_callback_base.py | 12 +- .../xray_centre/test_ispyb_callback.py | 20 +- .../xray_centre/test_ispyb_handler.py | 14 +- .../common/utils/test_xrc_result.py | 18 +- tests/unit_tests/conftest.py | 21 +- tests/unit_tests/hyperion/conftest.py | 11 +- .../test_load_centre_collect_full_plan.py | 8 +- .../test_pin_centre_then_xray_centre_plan.py | 13 +- .../test_robot_load_then_centre.py | 10 +- .../hyperion/external_interaction/conftest.py | 7 +- .../nexus/test_write_nexus.py | 73 +++--- tests/unit_tests/hyperion/test_main_system.py | 3 +- 39 files changed, 536 insertions(+), 411 deletions(-) rename tests/test_data/parameter_json_files/{good_test_parameters.json => good_test_specified_three_d_grid_params.json} (73%) diff --git a/docs/developer/hyperion/reference/gridscan.puml b/docs/developer/hyperion/reference/gridscan.puml index 3c22210486..3968f88cc0 100644 --- a/docs/developer/hyperion/reference/gridscan.puml +++ b/docs/developer/hyperion/reference/gridscan.puml @@ -3,14 +3,14 @@ title Gridscan Parameter Relationships class DiffractionExperiment class DiffractionExperimentWithSample -class GridCommon { +class GenericGrid { grid_width_um exposure_time_s } class GridScanWithEdgeDetect { box_size_um } -class HyperionGridCommon { +class HyperionGenericGrid { enable_dev_shm } class HyperionThreeDGridScan { @@ -47,10 +47,10 @@ class RotationScanPerSweep MxBlueSkyParameters <|-- DiffractionExperiment DiffractionExperiment <|-- DiffractionExperimentWithSample -DiffractionExperimentWithSample <|-- GridCommon -GridCommon <|-- GridScanWithEdgeDetect -GridCommon <|-- HyperionGridCommon -HyperionGridCommon <|-- HyperionThreeDGridScan +DiffractionExperimentWithSample <|-- GenericGrid +GenericGrid <|-- GridScanWithEdgeDetect +GenericGrid <|-- HyperionGenericGrid +HyperionGenericGrid <|-- HyperionThreeDGridScan SpecifiedGrid <|-- HyperionThreeDGridScan XyzStarts <|-- SpecifiedGrid OptionalXYZStarts <|-- RotationScanPerSweep diff --git a/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py b/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py index a3d568c0dd..7c31bc5830 100644 --- a/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py +++ b/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py @@ -1,34 +1,27 @@ from dodal.devices.beamlines.i02_1.fast_grid_scan import ZebraGridScanParamsTwoD -from scanspec.specs import Product from mx_bluesky.common.parameters.components import SplitScan, WithOptionalEnergyChange -from mx_bluesky.common.parameters.gridscan import SpecifiedGrid +from mx_bluesky.common.parameters.gridscan import SpecifiedGrids class SpecifiedTwoDGridScan( - SpecifiedGrid[ZebraGridScanParamsTwoD], + SpecifiedGrids[ZebraGridScanParamsTwoD], SplitScan, WithOptionalEnergyChange, ): """Parameters representing a so-called 2D grid scan, which consists of doing a gridscan in X and Y.""" - @property - def scan_spec(self) -> Product[str]: - """A fully specified ScanSpec object representing the grid, with x, y, z and - omega positions.""" - return self.grid_1_spec - @property def fast_gridscan_params(self) -> ZebraGridScanParamsTwoD: return ZebraGridScanParamsTwoD( x_steps=self.x_steps, - y_steps=self.y_steps, + y_steps=self.y_steps[0], x_step_size_mm=self.x_step_size_um / 1000, - y_step_size_mm=self.y_step_size_um / 1000, + y_step_size_mm=self.y_step_sizes_um[0] / 1000, x_start_mm=self.x_start_um / 1000, - y1_start_mm=self.y_start_um / 1000, - z1_start_mm=self.z_start_um / 1000, + y1_start_mm=self.y_starts_um[0] / 1000, + z1_start_mm=self.z_starts_um[0] / 1000, set_stub_offsets=self._set_stub_offsets, transmission_fraction=0.5, dwell_time_ms=self.exposure_time_s, 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 7d7421bcd3..48a50880f9 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 @@ -80,7 +80,7 @@ GridDetectThenXRayCentreComposite, ) from mx_bluesky.common.parameters.gridscan import ( - GridCommon, + GenericGrid, SpecifiedThreeDGridScan, ) from mx_bluesky.common.preprocessors.preprocessors import ( @@ -105,7 +105,7 @@ class I04AutoXrcParams(BaseModel): def _change_beamsize( - transfocator: Transfocator, beamsize: float, parameters: GridCommon + transfocator: Transfocator, beamsize: float, parameters: GenericGrid ): """i04 always uses the large aperture and changes beamsize with the transfocator. @@ -188,7 +188,7 @@ def i04_default_grid_detect_and_xray_centre( initial_z = yield from bps.rd(smargon.z.user_readback) _current_wavelength_a = yield from bps.rd(composite.dcm.wavelength_in_a) - grid_common_params = _get_grid_common_params(_current_wavelength_a, parameters) + grid_common_params = _get_generic_grid_params(_current_wavelength_a, parameters) def tidy_beamline(): yield from bps.mv(transfocator, initial_beamsize) @@ -275,7 +275,7 @@ def create_gridscan_callbacks() -> tuple[ return ( GridscanNexusFileCallback(param_type=SpecifiedThreeDGridScan), GridscanISPyBCallback( - param_type=GridCommon, + param_type=GenericGrid, emit=ZocaloCallback( PlanNameConstants.DO_FGS, EnvironmentConstants.ZOCALO_ENV, @@ -336,9 +336,9 @@ def construct_i04_specific_features( ) -def _get_grid_common_params( +def _get_generic_grid_params( _current_wavelength_a: float, parameters: I04AutoXrcParams -) -> GridCommon: +) -> GenericGrid: """Calculate scaled transmission and exposure by comparing current beamline energy to default energy""" _assumed_wavelength_a = ( get_i04_config_client().get_feature_flags().ASSUMED_WAVELENGTH_IN_A @@ -358,7 +358,7 @@ def _get_grid_common_params( ) ) - return GridCommon( + return GenericGrid( sample_id=parameters.sample_id, file_name=parameters.file_name, visit=parameters.visit, diff --git a/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py b/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py index 0c64076547..3bafca84a1 100644 --- a/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +++ b/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py @@ -201,7 +201,8 @@ def run_gridscan( beamline_specific.fgs_motors, fgs_composite.eiger, fgs_composite.synchrotron, - [parameters.scan_points_first_grid, parameters.scan_points_second_grid], + parameters.scan_points, + parameters.omega_starts_deg, plan_during_collection=beamline_specific.read_during_collection_plan, ) 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 79312c6184..46fd2cbe48 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 @@ -42,7 +42,7 @@ FlyScanEssentialDevices, GridDetectThenXRayCentreComposite, ) -from mx_bluesky.common.parameters.gridscan import GridCommon, SpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import GenericGrid, SpecifiedThreeDGridScan from mx_bluesky.common.utils.log import LOGGER TFlyScanEssentialDevices = TypeVar( @@ -55,7 +55,7 @@ def grid_detect_then_xray_centre( composite: GridDetectThenXRayCentreComposite, - parameters: GridCommon, + parameters: GenericGrid, xrc_params_type: type[SpecifiedThreeDGridScan], construct_beamline_specific: ConstructBeamlineSpecificFeatures, oav_config: str = OavConstants.OAV_CONFIG_JSON, @@ -96,7 +96,7 @@ def plan_to_perform(): # This function should be private but is currently called by Hyperion, see https://github.com/DiamondLightSource/mx-bluesky/issues/1148 def detect_grid_and_do_gridscan( composite: GridDetectThenXRayCentreComposite, - parameters: GridCommon, + parameters: GenericGrid, oav_params: OAVParameters, xrc_params_type: type[SpecifiedThreeDGridScan], construct_beamline_specific: ConstructBeamlineSpecificFeatures, @@ -180,7 +180,7 @@ def __call__( def create_parameters_for_flyscan_xray_centre( - parameters: GridCommon, + parameters: GenericGrid, grid_parameters: GridParamUpdate, xrc_params_type: type[SpecifiedThreeDGridScan], ) -> SpecifiedThreeDGridScan: 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 9257dba5ac..7a31ff79b6 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 @@ -17,9 +17,6 @@ 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 ( - GridscanPlane, -) from mx_bluesky.common.parameters.constants import ( PlanNameConstants, ) @@ -68,6 +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], plan_during_collection: Callable[[], MsgGenerator] | None = None, ): """Triggers a grid scan motion program and waits for completion, accounting for synchrotron topup. @@ -95,8 +93,8 @@ def kickoff_and_complete_gridscan( "omega_to_scan_spec": { # These have to be cast to strings due to a bug in orsjon. See # https://github.com/ijl/orjson/issues/414 - str(GridscanPlane.OMEGA_XY): scan_points[0], - str(GridscanPlane.OMEGA_XZ): scan_points[1], + str(omega_starts_deg[i]): scan_points[i] + for i in range(len(omega_starts_deg)) }, } ) diff --git a/src/mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py index 2407a584fa..272ffce3e1 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py @@ -30,16 +30,12 @@ class GridParamUpdate(TypedDict): """ x_start_um: float - y_start_um: float - y2_start_um: float - z_start_um: float - z2_start_um: float + y_start_um: list[float] + z_start_um: list[float] x_steps: int - y_steps: int - z_steps: int + y_steps: list[int] x_step_size_um: float - y_step_size_um: float - z_step_size_um: float + y_step_size_um: list[float] class XYZParams(TypedDict, Generic[T]): @@ -121,14 +117,10 @@ def event(self, doc: Event): def get_grid_parameters(self) -> GridParamUpdate: return { "x_start_um": self.start_positions_um["x"], - "y_start_um": self.start_positions_um["y"], - "y2_start_um": self.start_positions_um["y"], - "z_start_um": self.start_positions_um["z"], - "z2_start_um": self.start_positions_um["z"], + "y_start_um": [self.start_positions_um["y"], self.start_positions_um["y"]], + "z_start_um": [self.start_positions_um["z"], self.start_positions_um["z"]], "x_steps": self.box_numbers["x"], - "y_steps": self.box_numbers["y"], - "z_steps": self.box_numbers["z"], + "y_steps": [self.box_numbers["y"], self.box_numbers["z"]], "x_step_size_um": self.x_step_size_um, - "y_step_size_um": self.y_step_size_um, - "z_step_size_um": self.z_step_size_um, + "y_step_size_um": [self.y_step_size_um, self.z_step_size_um], } diff --git a/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py index 82190e7c92..830a71a3fc 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py @@ -38,7 +38,7 @@ ) from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample from mx_bluesky.common.parameters.constants import DocDescriptorNames, PlanNameConstants -from mx_bluesky.common.parameters.gridscan import GridCommon +from mx_bluesky.common.parameters.gridscan import GenericGrid from mx_bluesky.common.utils.exceptions import ( ISPyBDepositionNotMadeError, SampleError, @@ -57,7 +57,7 @@ class GridscanPlane(StrEnum): if TYPE_CHECKING: from event_model import Event, RunStart, RunStop -T = TypeVar("T", bound="GridCommon") +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" diff --git a/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py index 3752db323a..1d5b0dd9ef 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py @@ -12,21 +12,21 @@ from mx_bluesky.common.external_interaction.nexus.write_nexus import NexusWriter from mx_bluesky.common.parameters.constants import DocDescriptorNames, PlanNameConstants from mx_bluesky.common.parameters.gridscan import ( - SpecifiedGrid, + SpecifiedGrids, ) from mx_bluesky.common.utils.log import NEXUS_LOGGER if TYPE_CHECKING: from event_model.documents import Event, EventDescriptor, RunStart -T = TypeVar("T", bound="SpecifiedGrid") +T = TypeVar("T", bound="SpecifiedGrids") class GridscanNexusFileCallback(PlanReactiveCallback): """Callback class to handle the creation of Nexus files based on experiment \ parameters. Initialises on receiving a 'start' document for the \ 'run_gridscan_move_and_tidy' sub plan, which must also contain the run parameters, \ - as metadata under the 'hyperion_internal_parameters' key. Actually writes the \ + as metadata under the 'mx_bluesky_parameters' key. Actually writes the \ nexus files on updates the timestamps on receiving the 'ispyb_reading_hardware' event \ document, and finalises the files on getting a 'stop' document for the whole run. @@ -39,13 +39,13 @@ class GridscanNexusFileCallback(PlanReactiveCallback): See: https://blueskyproject.io/bluesky/callbacks.html#ways-to-invoke-callbacks """ - def __init__(self, param_type: type[T], num_writers: int) -> None: + def __init__(self, param_type: type[T]) -> None: super().__init__(NEXUS_LOGGER) self.param_type = param_type self.run_start_uid: str | None = None - self.num_writers: int = num_writers + self.descriptors: dict[str, EventDescriptor] = {} self.log = NEXUS_LOGGER - self.writers: list[NexusWriter] = [] + self._writers: list[NexusWriter] = [] def activity_gated_start(self, doc: RunStart): if doc.get("subplan_name") == PlanNameConstants.GRIDSCAN_OUTER: @@ -55,27 +55,25 @@ def activity_gated_start(self, doc: RunStart): f"Nexus writer received start document with experiment parameters {mx_bluesky_parameters}" ) parameters = self.param_type.model_validate_json(mx_bluesky_parameters) + num_writers = parameters.num_grids + d_size = parameters.detector_params.detector_size_constants.det_size_pixels - grid_n_img_1 = parameters.scan_indices[1] - grid_n_img_2 = parameters.num_images - grid_n_img_1 - data_shape_1 = (grid_n_img_1, d_size.width, d_size.height) - data_shape_2 = (grid_n_img_2, d_size.width, d_size.height) - run_number_2 = parameters.detector_params.run_number + 1 + for idx in range(0, num_writers - 1): + images_in_grid = parameters.scan_indices[idx + 1] + data_shape = (images_in_grid, d_size.width, d_size.height) + run_number = parameters.detector_params.run_number + idx - for writer in range(self.num_writers): - self._writers.append(NexusWriter) + self._writers.append( + NexusWriter( + parameters, + data_shape, + parameters.scan_points[idx], + run_number=run_number, + vds_start_index=parameters.scan_indices[idx], + omega_start_deg=parameters.omega_starts_deg[idx], + ) + ) - self.nexus_writer_1 = NexusWriter( - parameters, data_shape_1, parameters.scan_points_first_grid - ) - self.nexus_writer_2 = NexusWriter( - parameters, - data_shape_2, - parameters.scan_points_second_grid, - run_number=run_number_2, - vds_start_index=parameters.scan_indices[1], - omega_start_deg=90, - ) self.run_start_uid = doc.get("uid") def activity_gated_descriptor(self, doc: EventDescriptor): @@ -86,7 +84,7 @@ def activity_gated_event(self, doc: Event) -> Event | None: assert event_descriptor is not None if event_descriptor.get("name") == DocDescriptorNames.HARDWARE_READ_DURING: data = doc["data"] - for nexus_writer in [self.nexus_writer_1, self.nexus_writer_2]: + for nexus_writer in self._writers: assert nexus_writer, "Nexus callback did not receive start doc" ( nexus_writer.beam, diff --git a/src/mx_bluesky/common/parameters/components.py b/src/mx_bluesky/common/parameters/components.py index 7fda4a7833..6ee97801b0 100644 --- a/src/mx_bluesky/common/parameters/components.py +++ b/src/mx_bluesky/common/parameters/components.py @@ -212,12 +212,16 @@ class WithScan(BaseModel): @property @abstractmethod - def scan_points(self) -> AxesPoints: ... + def scan_points(self) -> list[AxesPoints]: ... + + """Per grid""" @property @abstractmethod def num_images(self) -> int: ... + """Must be same for each grid""" + class WithPandaGridScan(BaseModel): """For experiments which use a PandA for constant-motion grid scans""" @@ -273,20 +277,22 @@ def selection_params(self) -> MultiXtalSelection: class OptionalXyzStarts(BaseModel): - x_starts_um: list[float | None] | None = None - y_starts_um: list[float | None] - z_starts_um: list[float | None] + x_start_um: float = ( + 0 # todo link to issue about X params needing to be the same for each grid + ) + y_starts_um: list[float | None] | None = None + z_starts_um: list[float | None] | None = None class XyzStarts(BaseModel): - x_starts_um: list[float] + x_start_um: float y_starts_um: list[float] z_starts_um: list[float] def _start_for_axis(self, axis: XyzAxis, grid: int) -> float: match axis: case XyzAxis.X: - return self.x_starts_um[grid] + return self.x_start_um case XyzAxis.Y: return self.y_starts_um[grid] case XyzAxis.Z: @@ -294,7 +300,9 @@ def _start_for_axis(self, axis: XyzAxis, grid: int) -> float: class OptionalGonioAngleStarts(BaseModel): - omega_starts_deg: list[float | None] - phi_starts_deg: list[float | None] - chi_starts_deg: list[float | None] - kappa_starts_deg: list[float | None] + # Gridscans have different omega starts + omega_starts_deg: list[float] = [0, 90] + + phi_start_deg: float | None = None + chi_start_deg: float | None = None + kappa_start_deg: float | None = None diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index f6617e4d5a..7834270e96 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -1,7 +1,8 @@ from __future__ import annotations +import weakref from abc import abstractmethod -from typing import Generic, TypeVar +from typing import Annotated, Generic, TypeVar from dodal.devices.aperturescatterguard import ApertureValue from dodal.devices.detector.det_dim_constants import EIGER2_X_9M_SIZE, EIGER2_X_16M_SIZE @@ -11,7 +12,8 @@ ZebraGridScanParamsThreeD, ) from dodal.utils import get_beamline_name -from pydantic import Field, PrivateAttr +from pydantic import Field, PrivateAttr, model_validator +from scanspec.core import AxesPoints from scanspec.core import Path as ScanPath from scanspec.specs import Concat, Line, Product, Static @@ -42,14 +44,16 @@ ) -class GridCommon( +class GenericGrid( DiffractionExperimentWithSample, OptionalGonioAngleStarts, ): """ Parameters used in every MX diffraction experiment using grids. This model should be used by plans which have no knowledge of the grid specifications - i.e before - automatic grid detection has completed + automatic grid detection has completed. + + Params in GenericGrid currently must be the same for each grid in the gridscan. """ box_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM) @@ -66,15 +70,17 @@ class GridCommon( # Available after grid detection, used by entry point plans which need to # get the grid parameters to retrieve zocalo results # Can remove this after https://github.com/DiamondLightSource/python-dlstbx/issues/255 is done - _specified_grid_params: SpecifiedGrid | None = PrivateAttr(default=None) + _specified_grids_params: SpecifiedGrids | None = PrivateAttr(default=None) - def set_specified_grid_params(self, params: SpecifiedGrid): + def set_specified_grid_params(self, params: SpecifiedGrids): self._specified_grid_params = params @property - def specified_grid_params(self) -> SpecifiedGrid | None: + def specified_grid_params(self) -> SpecifiedGrids | None: return self._specified_grid_params + # We currently only arm the detector once, regardless of total grids. Detector params + # must be the same for each grid @property def detector_params(self): self.det_dist_to_beam_converter_path = ( @@ -96,7 +102,11 @@ def detector_params(self): directory=self.storage_directory, prefix=self.file_name, detector_distance=self.detector_distance_mm, - omega_start=self.omega_start_deg or 0, + omega_start=0 + if not self.omega_starts_deg + else self.omega_starts_deg[ + 0 + ], # This value is probably a lie after this PR... Could it be stored somewhere else? Should be an experiment param, not detector omega_increment=0, num_images_per_trigger=1, num_triggers=self.num_images, @@ -107,28 +117,31 @@ def detector_params(self): ) -class SpecifiedGrids(GridCommon, XyzStarts, WithScan, Generic[GridScanParamType]): +PositiveInt = Annotated[ + int, Field(gt=0) +] # todo test this actually validates as expected +PositiveFloat = Annotated[float, Field(gt=0)] + + +class SpecifiedGrids(GenericGrid, XyzStarts, WithScan, Generic[GridScanParamType]): """A specified grid is one which has defined values for the start position, grid and box sizes, etc., as opposed to parameters for a plan which will create those parameters at some point (e.g. through optical pin detection).""" - omega_deg_per_grid: list[float] = Field( + omega_starts_deg: list[float] = Field( default=[GridscanParamConstants.OMEGA_1, GridscanParamConstants.OMEGA_2] ) - x_step_sizes_um: list[float] = Field( - default=[ - GridscanParamConstants.BOX_WIDTH_UM, - GridscanParamConstants.BOX_WIDTH_UM, - ] + x_step_size_um: PositiveFloat = Field( + default=GridscanParamConstants.BOX_WIDTH_UM + ) # Think this needs to be the same for each grid too + + # In a 3D grid scan, n_steps[0] and n_steps[1] refers to Y and Z respectively. + # We do an omega rotation between scanning across N dimensions to make N different axes + y_step_sizes_um: list[PositiveFloat] = Field( + default=[GridscanParamConstants.BOX_WIDTH_UM] ) - y_step_sizes_um: list[float] = Field( - default=[ - GridscanParamConstants.BOX_WIDTH_UM, - GridscanParamConstants.BOX_WIDTH_UM, - ] - ) - x_steps: list[int] = Field(gt=0) - y_steps: list[int] = Field(gt=0) + x_steps: PositiveInt # Currently this must be the same for each grid for panda scan + y_steps: list[PositiveInt] _set_stub_offsets: bool = PrivateAttr(default_factory=lambda: False) # TODO validate that all the "per grid" things are the same length. _num_grids property can just print out length of this @@ -139,20 +152,22 @@ def fast_gridscan_params(self) -> GridScanParamType: ... def do_set_stub_offsets(self, value: bool): self._set_stub_offsets = value - def _num_grids(self): - return len(self.x_steps) + @property + def num_grids(self): + return len(self.y_steps) # TODO this should probably just be an input + + def __len__(self) -> int: + return self.num_grids @property def grid_specs(self) -> list[Product[str]]: _grid_specs = [] - for idx in range(self._num_grids()): - x_end = self.x_starts_um[idx] + self.x_step_sizes_um[idx] * ( - self.x_step_sizes_um[idx] - 1 - ) + for idx in range(self.num_grids): + x_end = self.x_start_um + self.x_step_size_um * (self.x_step_size_um - 1) y_end = self.y_starts_um[idx] + self.y_step_sizes_um[idx] * ( self.y_step_sizes_um[idx] - 1 ) - grid_x = Line("sam_x", self.x_starts_um[idx], x_end, self.x_steps[idx]) + grid_x = Line("sam_x", self.x_start_um, x_end, self.x_steps) grid_y = Line("sam_y", self.y_starts_um[idx], y_end, self.y_steps[idx]) grid_z = Static("sam_z", self.z_starts_um[idx]) _grid_specs.append(grid_y.zip(grid_z) * ~grid_x) @@ -162,7 +177,7 @@ def grid_specs(self) -> list[Product[str]]: def scan_indices(self) -> list[int]: """The first index of each gridscan, useful for writing nexus files/VDS""" _scan_indices = [0] - for idx in range(self._num_grids()): + for idx in range(self.num_grids): _scan_indices.append( len( ScanPath(self.grid_specs[idx].calculate()) @@ -173,75 +188,140 @@ def scan_indices(self) -> list[int]: return _scan_indices @property - @abstractmethod def scan_spec(self) -> Product[str] | Concat[str]: """A fully specified ScanSpec object representing all grids, with x, y, z and omega positions.""" + _scan_spec = self.grid_specs[0] + + for idx in range(1, self.num_grids - 1): + _scan_spec = _scan_spec.concat( + self.grid_specs[idx].concat(self.grid_specs[idx + 1]) + ) + return _scan_spec + + @property + def scan_points(self) -> list[AxesPoints[str]]: + """A list of all the points in the scan_spec for each grid.""" + _scan_points = [] + for grid in range(self.num_grids): + _scan_points.append( + ScanPath(self.grid_specs[grid].calculate()).consume().midpoints + ) + return _scan_points + + @property + def num_images(self) -> int: + """Total num images in entire scan""" + _num_images = 0 + for grid in range(len(self.scan_points)): + _num_images += len(self.scan_points[grid]["sam_x"]) + return _num_images + + def __getitem__(self, idx: int) -> _SingleGrid: + if idx < 0 or idx >= self.num_grids: + raise IndexError(idx) + return _SingleGrid(self, idx) + + +class _SingleGrid(Generic[GridScanParamType]): + """Helper class for plan code to not need to refer to an index constantly.""" + + def __init__(self, grids: SpecifiedGrids[GridScanParamType], idx: int): + self._grids_ref = weakref.ref(grids) + self._idx = idx + + @property + def _grids(self) -> SpecifiedGrids[GridScanParamType]: + grids = self._grids_ref() + if grids is None: + raise ReferenceError("Parent SpecifiedGrids object no longer exists") + return grids + + @property + def idx(self) -> int: + return self._idx + + # ---- Per-grid scalar accessors ---- + + @property + def x_steps(self) -> int: + return self._grids.x_steps + + @property + def y_steps(self) -> int: + return self._grids.y_steps[self._idx] + + @property + def x_step_size_um(self) -> float: + return self._grids.x_step_size_um + + @property + def y_step_size_um(self) -> float: + return self._grids.y_step_sizes_um[self._idx] + + @property + def omega_start_deg(self) -> float | None: + return self._grids.omega_starts_deg[self._idx] + + # ---- Derived scan objects ---- + @property - def scan_points(self): - """A list of all the points in the scan_spec.""" - return ScanPath(self.scan_spec.calculate()).consume().midpoints + def grid_spec(self) -> Product[str]: + return self._grids.grid_specs[self._idx] @property - def scan_points_first_grid(self): - """A list of all the points in the first grid scan.""" - return ScanPath(self.grid_1_spec.calculate()).consume().midpoints + def grid_points(self): + return ScanPath(self.grid_spec.calculate()).consume().midpoints @property def num_images(self) -> int: - return len(self.scan_points["sam_x"]) + return len(self.grid_points["sam_x"]) class SpecifiedThreeDGridScan( - SpecifiedGrid[ZebraGridScanParamsThreeD], + SpecifiedGrids[ZebraGridScanParamsThreeD], SplitScan, WithOptionalEnergyChange, ): """Parameters representing a so-called 3D grid scan, which consists of doing a gridscan in X and Y, followed by one in X and Z.""" - z_steps: int = Field(gt=0) - z_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM) - y2_start_um: float - z2_start_um: float - grid2_omega_deg: float = Field(default=GridscanParamConstants.OMEGA_2) + # TODO: specified grids just show X and Y. Here we should be using z_steps = specifiedgrid.y_steps[1] + # sort this out when dealing with external/internal params + + # TODO validate that grid is length 2 on creation here + + # For 3D scans, number of Z steps and Z step size is the same as y_steps[1] and + # y_step_sizes_um[1]. It can be helpful to think of it as both the Z axis and as + # the second 2D scan, so we put some validation logic to allow you to use either name. + # Maybe should just refer to one name instead of having this validation logic? + + # TODO make a better validator about all the lists being length 2 + + @model_validator(mode="after") + def validate_y_and_z_axes(self): + if len(self.y_steps) != 2: + raise ValueError(f"{self.y_steps=} must be length 2 for 3D scans") + if len(self.y_step_sizes_um) != 2: + raise ValueError(f"{self.y_step_sizes_um=} must be length 2 for 3D scans") + return self @property def fast_gridscan_params(self) -> ZebraGridScanParamsThreeD: return ZebraGridScanParamsThreeD( x_steps=self.x_steps, - y_steps=self.y_steps, - z_steps=self.z_steps, + y_steps=self.y_steps[0], + z_steps=self.y_steps[1], x_step_size_mm=self.x_step_size_um / 1000, - y_step_size_mm=self.y_step_size_um / 1000, - z_step_size_mm=self.z_step_size_um / 1000, + y_step_size_mm=self.y_step_sizes_um[0] / 1000, + z_step_size_mm=self.y_step_sizes_um[1] / 1000, x_start_mm=self.x_start_um / 1000, - y1_start_mm=self.y_start_um / 1000, - z1_start_mm=self.z_start_um / 1000, - y2_start_mm=self.y2_start_um / 1000, - z2_start_mm=self.z2_start_um / 1000, + y1_start_mm=self.y_starts_um[0] / 1000, + z1_start_mm=self.z_starts_um[0] / 1000, + y2_start_mm=self.y_starts_um[1] / 1000, + z2_start_mm=self.z_starts_um[1] / 1000, set_stub_offsets=self._set_stub_offsets, dwell_time_ms=self.exposure_time_s * 1000, transmission_fraction=self.transmission_frac, ) - - @property - def grid_2_spec(self): - x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1) - z2_end = self.z2_start_um + self.z_step_size_um * (self.z_steps - 1) - grid_2_x = Line("sam_x", self.x_start_um, x_end, self.x_steps) - grid_2_z = Line("sam_z", self.z2_start_um, z2_end, self.z_steps) - grid_2_y = Static("sam_y", self.y2_start_um) - return grid_2_z.zip(grid_2_y) * ~grid_2_x - - @property - def scan_spec(self): - """A fully specified ScanSpec object representing both grids, with x, y, z and - omega positions.""" - return self.grid_1_spec.concat(self.grid_2_spec) - - @property - def scan_points_second_grid(self): - """A list of all the points in the second grid scan.""" - return ScanPath(self.grid_2_spec.calculate()).consume().midpoints diff --git a/src/mx_bluesky/common/parameters/rotation.py b/src/mx_bluesky/common/parameters/rotation.py index 80a3965568..371de4bfff 100644 --- a/src/mx_bluesky/common/parameters/rotation.py +++ b/src/mx_bluesky/common/parameters/rotation.py @@ -22,11 +22,9 @@ DiffractionExperimentWithSample, IspybExperimentType, OptionalGonioAngleStarts, - OptionalXyzStarts, RotationAxis, SplitScan, WithSample, - WithScan, ) from mx_bluesky.common.parameters.constants import ( DetectorParamConstants, @@ -34,7 +32,7 @@ ) -class RotationScanPerSweep(OptionalGonioAngleStarts, OptionalXyzStarts, WithSample): +class RotationScanPerSweep(OptionalGonioAngleStarts, WithSample): """ Describes a rotation scan about the specified axis. @@ -48,6 +46,9 @@ class RotationScanPerSweep(OptionalGonioAngleStarts, OptionalXyzStarts, WithSamp nexus_vds_start_img: The frame number of the first frame captured during the rotation """ + x_start_um: float | None = None + y_start_um: float | None = None + z_start_um: float | None = None omega_start_deg: float = Field(default=0) # type: ignore rotation_axis: RotationAxis = Field(default=RotationAxis.OMEGA) scan_width_deg: float = Field(default=360, gt=0) @@ -109,7 +110,7 @@ def _set_default_aperture_position(cls, aperture_position: ApertureValue | None) class SingleRotationScan( - WithScan, RotationExperiment, RotationScanPerSweep, DiffractionExperimentWithSample + RotationExperiment, RotationScanPerSweep, DiffractionExperimentWithSample ): @property def detector_params(self): diff --git a/src/mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py b/src/mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py index d5a9798493..30c5b85d9e 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py @@ -177,6 +177,7 @@ def _x_coordinate(sample_and_location: tuple[int, np.ndarray]) -> float: return sample_and_location[1][0] # type: ignore +# todo rotation scan params shouldnt be tied to our grid scan params def rotation_scan_generator( is_alternating: bool, ) -> Generator[ @@ -186,10 +187,12 @@ def rotation_scan_generator( next_rotation_direction = scan_template.rotation_direction while True: scan = scan_template.model_copy() + if not scan.y_starts_um: + scan.y_starts_um = [0, 0] ( scan.x_start_um, - scan.y_start_um, - scan.z_start_um, + scan.y_starts_um[0], + scan.y_starts_um[1], ) = location scan.sample_id = sample_id if is_alternating: diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py index c93c746221..2f6696466e 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py @@ -63,7 +63,7 @@ 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 ( - GridCommonWithHyperionDetectorParams, + GenericGridWithHyperionDetectorParams, HyperionSpecifiedThreeDGridScan, ) @@ -80,7 +80,7 @@ def create_gridscan_callbacks() -> tuple[ return ( GridscanNexusFileCallback(param_type=HyperionSpecifiedThreeDGridScan), GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams, + param_type=GenericGridWithHyperionDetectorParams, emit=ZocaloCallback( CONST.PLAN.DO_FGS, CONST.ZOCALO_ENV, generate_start_info_from_omega_map ), diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py index de7cf2e3a8..60437c0e16 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py @@ -97,8 +97,8 @@ def activity_gated_start(self, doc: RunStart): shape, parameters.scan_points, omega_start_deg=parameters.omega_start_deg, - chi_start_deg=parameters.chi_start_deg or 0, - phi_start_deg=parameters.phi_start_deg or 0, + chi_start_deg=parameters.chi_start_deg, + phi_start_deg=parameters.phi_start_deg, vds_start_index=parameters.nexus_vds_start_img, full_num_of_images=self.full_num_of_images, meta_data_run_number=self.meta_data_run_number, diff --git a/src/mx_bluesky/hyperion/parameters/gridscan.py b/src/mx_bluesky/hyperion/parameters/gridscan.py index 757aa276ed..31f20e02fa 100644 --- a/src/mx_bluesky/hyperion/parameters/gridscan.py +++ b/src/mx_bluesky/hyperion/parameters/gridscan.py @@ -6,7 +6,7 @@ ) from mx_bluesky.common.parameters.gridscan import ( - GridCommon, + GenericGrid, SpecifiedThreeDGridScan, ) from mx_bluesky.hyperion.external_interaction.config_server import ( @@ -14,7 +14,7 @@ ) -class GridCommonWithHyperionDetectorParams(GridCommon): +class GenericGridWithHyperionDetectorParams(GenericGrid): """Used by models which require detector parameters but have no specifications of the grid""" # These detector params only exist so that we can properly select enable_dev_shm. Remove in @@ -45,18 +45,22 @@ def detector_params(self): # Relative to common grid scan, stub offsets are defined by config server @property def fast_gridscan_params(self) -> ZebraGridScanParamsThreeD: + """Currently, x steps must be the same for each grid. Validate this somewhere? + Same for x and y step sizes. TODO Better is probably to not make it a list at all... + + """ return ZebraGridScanParamsThreeD( x_steps=self.x_steps, - y_steps=self.y_steps, - z_steps=self.z_steps, + y_steps=self.y_steps[0], + z_steps=self.y_steps[1], x_step_size_mm=self.x_step_size_um / 1000, - y_step_size_mm=self.y_step_size_um / 1000, - z_step_size_mm=self.z_step_size_um / 1000, + y_step_size_mm=self.y_step_sizes_um[0] / 1000, + z_step_size_mm=self.y_step_sizes_um[1] / 1000, x_start_mm=self.x_start_um / 1000, - y1_start_mm=self.y_start_um / 1000, - z1_start_mm=self.z_start_um / 1000, - y2_start_mm=self.y2_start_um / 1000, - z2_start_mm=self.z2_start_um / 1000, + y1_start_mm=self.y_starts_um[0] / 1000, + z1_start_mm=self.z_starts_um[0] / 1000, + y2_start_mm=self.y_starts_um[1] / 1000, + z2_start_mm=self.z_starts_um[1] / 1000, set_stub_offsets=get_hyperion_config_client() .get_feature_flags() .SET_STUB_OFFSETS, @@ -66,23 +70,23 @@ def fast_gridscan_params(self) -> ZebraGridScanParamsThreeD: @property def panda_fast_gridscan_params(self) -> PandAGridScanParams: - if self.y_steps % 2 and self.z_steps > 0: + if self.y_steps[0] % 2 and self.y_steps[1] > 0: # See https://github.com/DiamondLightSource/hyperion/issues/1118 for explanation raise OddYStepsError( "The number of Y steps must be even for a PandA gridscan" ) return PandAGridScanParams( x_steps=self.x_steps, - y_steps=self.y_steps, - z_steps=self.z_steps, + y_steps=self.y_steps[0], + z_steps=self.y_steps[1], x_step_size_mm=self.x_step_size_um / 1000, - y_step_size_mm=self.y_step_size_um / 1000, - z_step_size_mm=self.z_step_size_um / 1000, + y_step_size_mm=self.y_step_sizes_um[0] / 1000, + z_step_size_mm=self.y_step_sizes_um[1] / 1000, x_start_mm=self.x_start_um / 1000, - y1_start_mm=self.y_start_um / 1000, - z1_start_mm=self.z_start_um / 1000, - y2_start_mm=self.y2_start_um / 1000, - z2_start_mm=self.z2_start_um / 1000, + y1_start_mm=self.y_starts_um[0] / 1000, + z1_start_mm=self.z_starts_um[0] / 1000, + y2_start_mm=self.y_starts_um[1] / 1000, + z2_start_mm=self.z_starts_um[1] / 1000, set_stub_offsets=get_hyperion_config_client() .get_feature_flags() .SET_STUB_OFFSETS, @@ -96,9 +100,9 @@ def panda_fast_gridscan_params(self) -> PandAGridScanParams: class OddYStepsError(Exception): ... -class PinTipCentreThenXrayCentre(GridCommonWithHyperionDetectorParams): +class PinTipCentreThenXrayCentre(GenericGridWithHyperionDetectorParams): tip_offset_um: float = 0 -class GridScanWithEdgeDetect(GridCommonWithHyperionDetectorParams): +class GridScanWithEdgeDetect(GenericGridWithHyperionDetectorParams): pass diff --git a/src/mx_bluesky/hyperion/parameters/robot_load.py b/src/mx_bluesky/hyperion/parameters/robot_load.py index 4342d487e9..43b07acbb5 100644 --- a/src/mx_bluesky/hyperion/parameters/robot_load.py +++ b/src/mx_bluesky/hyperion/parameters/robot_load.py @@ -6,7 +6,7 @@ WithVisit, ) from mx_bluesky.hyperion.parameters.gridscan import ( - GridCommonWithHyperionDetectorParams, + GenericGridWithHyperionDetectorParams, PinTipCentreThenXrayCentre, ) @@ -17,7 +17,7 @@ class RobotLoadAndEnergyChange( pass -class RobotLoadThenCentre(GridCommonWithHyperionDetectorParams): +class RobotLoadThenCentre(GenericGridWithHyperionDetectorParams): @property def robot_load_params(self) -> RobotLoadAndEnergyChange: my_params = self.model_dump() diff --git a/tests/conftest.py b/tests/conftest.py index 0efab4e664..416c3f7f54 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -389,13 +389,23 @@ def hyperion_fgs_params(tmp_path): return HyperionSpecifiedThreeDGridScan( **( raw_params_from_file( - "tests/test_data/parameter_json_files/good_test_parameters.json", + "tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json", tmp_path, ) ) ) +@pytest.fixture +def test_three_d_grid_params(tmp_path): + return SpecifiedThreeDGridScan( + **raw_params_from_file( + "tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json", + tmp_path, + ) + ) + + @pytest.fixture def eiger(): eiger = i03.eiger.build(mock=True) 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 026f3d0b50..c774e08de8 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 @@ -54,7 +54,7 @@ HyperionGridDetectThenXRayCentreComposite, ) from mx_bluesky.hyperion.parameters.gridscan import ( - GridCommonWithHyperionDetectorParams, + GenericGridWithHyperionDetectorParams, GridScanWithEdgeDetect, HyperionSpecifiedThreeDGridScan, ) @@ -415,7 +415,7 @@ 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(GridCommonWithHyperionDetectorParams) + ispyb_callback = GridscanISPyBCallback(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 8d0ad11f62..94450e0d24 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 @@ -56,7 +56,9 @@ from mx_bluesky.hyperion.parameters.device_composites import ( HyperionGridDetectThenXRayCentreComposite, ) -from mx_bluesky.hyperion.parameters.gridscan import GridCommonWithHyperionDetectorParams +from mx_bluesky.hyperion.parameters.gridscan import ( + GenericGridWithHyperionDetectorParams, +) from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect from ....conftest import ( @@ -291,7 +293,7 @@ def test_execute_load_centre_collect_full( robot_load_cb: RobotLoadISPyBCallback, ): ispyb_gridscan_cb = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() snapshot_cb = BeamDrawingCallback(emit=ispyb_rotation_cb) @@ -470,7 +472,7 @@ def move_to_initial_omega(): run_engine(move_to_initial_omega()) ispyb_gridscan_cb = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() snapshot_cb = BeamDrawingCallback(emit=ispyb_rotation_cb) @@ -562,7 +564,7 @@ def test_load_centre_collect_updates_bl_sample_status_pin_tip_detection_fail( ): robot_load_cb = RobotLoadISPyBCallback() ispyb_gridscan_cb = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) sample_handling_cb = SampleHandlingCallback() run_engine.subscribe(robot_load_cb) @@ -596,7 +598,7 @@ def test_load_centre_collect_updates_bl_sample_status_grid_detection_fail_tip_no ): robot_load_cb = RobotLoadISPyBCallback() ispyb_gridscan_cb = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) sample_handling_cb = SampleHandlingCallback() run_engine.subscribe(robot_load_cb) @@ -648,7 +650,7 @@ def test_load_centre_collect_updates_bl_sample_status_gridscan_no_diffraction( ): robot_load_cb = RobotLoadISPyBCallback() ispyb_gridscan_cb = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) sample_handling_cb = SampleHandlingCallback() run_engine.subscribe(robot_load_cb) @@ -680,7 +682,7 @@ def test_load_centre_collect_updates_bl_sample_status_rotation_failure( ): robot_load_cb = RobotLoadISPyBCallback() ispyb_gridscan_cb = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) sample_handling_cb = SampleHandlingCallback() run_engine.subscribe(robot_load_cb) @@ -738,7 +740,7 @@ def test_load_centre_collect_gridscan_result_at_edge_of_grid( zocalo_result, [SimConstants.ST_SAMPLE_ID] ) ispyb_gridscan_cb = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() set_mock_value( @@ -772,7 +774,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( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() snapshot_callback = BeamDrawingCallback(emit=ispyb_rotation_cb) @@ -854,7 +856,7 @@ def test_load_centre_collect_multisample_pin_reports_correct_sample_ids_in_ispyb ): load_centre_collect_composite.zocalo.my_zocalo_result = zocalo_result ispyb_gridscan_cb = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() snapshot_cb = BeamDrawingCallback(emit=ispyb_rotation_cb) @@ -907,7 +909,7 @@ def test_load_centre_collect_multisample_pin_reports_correct_sample_ids_in_ispyb ): load_centre_collect_composite.zocalo.my_zocalo_result = zocalo_result ispyb_gridscan_cb = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() snapshot_cb = BeamDrawingCallback(emit=ispyb_rotation_cb) @@ -973,7 +975,7 @@ def test_load_centre_collect_multisample_pin_reports_correct_sample_ids_robot_lo ): load_centre_collect_composite.zocalo.my_zocalo_result = zocalo_result ispyb_gridscan_cb = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() snapshot_cb = BeamDrawingCallback(emit=ispyb_rotation_cb) @@ -1030,7 +1032,7 @@ def test_load_centre_collect_multisample_pin_updates_sample_status_for_parent_sa ): load_centre_collect_composite.zocalo.my_zocalo_result = zocalo_result ispyb_gridscan_cb = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() snapshot_cb = BeamDrawingCallback(emit=ispyb_rotation_cb) @@ -1168,7 +1170,7 @@ def test_load_centre_collect_generate_rotation_snapshots( ) ispyb_gridscan_cb = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) ispyb_rotation_cb = RotationISPyBCallback() snapshot_callback = BeamDrawingCallback(emit=ispyb_rotation_cb) diff --git a/tests/test_data/parameter_json_files/good_test_parameters.json b/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json similarity index 73% rename from tests/test_data/parameter_json_files/good_test_parameters.json rename to tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json index 14b9b80900..8ae8d7fd1e 100644 --- a/tests/test_data/parameter_json_files/good_test_parameters.json +++ b/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json @@ -14,15 +14,23 @@ "use_roi_mode": false, "transmission_frac": 1.0, "x_steps": 40, - "y_steps": 20, - "z_steps": 10, + "y_steps": [ + 20, + 30 + ], "x_step_size_um": 100, - "y_step_size_um": 100, - "z_step_size_um": 100, + "y_step_sizes_um": [ + 100, + 100 + ], "x_start_um": 0.0, - "y_start_um": 0.0, - "y2_start_um": 0.0, - "z_start_um": 0.0, - "z2_start_um": 0.0, + "y_starts_um": [ + 0.0, + 0.0 + ], + "z_starts_um": [ + 0.0, + 0.0 + ], "storage_directory": "{tmp_data}/xraycentring/123456/" } diff --git a/tests/test_data/parameter_json_files/test_gridscan_param_defaults.json b/tests/test_data/parameter_json_files/test_gridscan_param_defaults.json index 5383c7bea8..c8d5c600c9 100644 --- a/tests/test_data/parameter_json_files/test_gridscan_param_defaults.json +++ b/tests/test_data/parameter_json_files/test_gridscan_param_defaults.json @@ -13,16 +13,24 @@ "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", "transmission_frac": 1.0, "x_steps": 40, - "y_steps": 20, - "z_steps": 10, + "y_steps": [ + 20, + 10 + ], "x_step_size_um": 0.1, - "y_step_size_um": 0.1, - "z_step_size_um": 0.1, + "y_step_sizes_um": [ + 0.1, + 0.1 + ], "x_start_um": 0.0, - "y_start_um": 0.0, - "y2_start_um": 0.0, - "z_start_um": 0.0, - "z2_start_um": 0.0, + "y_starts_um": [ + 0.0, + 0.0 + ], + "z_starts_um": [ + 0.0, + 0.0 + ], "detector_distance_mm": 100.0, "omega_start_deg": 0.0, "exposure_time_s": 0.1 diff --git a/tests/unit_tests/beamlines/i04/test_i04_grid_detect_then_xray_centre_plan.py b/tests/unit_tests/beamlines/i04/test_i04_grid_detect_then_xray_centre_plan.py index 728b957ae5..e85917ab3a 100644 --- a/tests/unit_tests/beamlines/i04/test_i04_grid_detect_then_xray_centre_plan.py +++ b/tests/unit_tests/beamlines/i04/test_i04_grid_detect_then_xray_centre_plan.py @@ -38,13 +38,13 @@ from mx_bluesky.beamlines.i04.experiment_plans.i04_grid_detect_then_xray_centre_plan import ( DEFAULT_XRC_BEAMSIZE_MICRONS, I04AutoXrcParams, - _get_grid_common_params, + _get_generic_grid_params, get_ready_for_oav_and_close_shutter, i04_default_grid_detect_and_xray_centre, ) from mx_bluesky.common.parameters.constants import PlanNameConstants from mx_bluesky.common.parameters.gridscan import ( - GridCommon, + GenericGrid, SpecifiedThreeDGridScan, ) from mx_bluesky.common.utils.exceptions import CrystalNotFoundError @@ -123,6 +123,21 @@ def i04_grid_detect_then_xrc_default_params( ) +@pytest.fixture() +def give_grid_common_specific_grid(test_three_d_grid_params: SpecifiedThreeDGridScan): + # Using this fixture means grid common always comes with its specific grid params + class GenericGridWithSpecificGrid(GenericGrid): + @property + def specified_grid_params(self): + return test_three_d_grid_params + + with patch( + "mx_bluesky.beamlines.i04.experiment_plans.i04_grid_detect_then_xray_centre_plan.GenericGrid", + GenericGridWithSpecificGrid, + ): + yield + + @patch( "mx_bluesky.beamlines.i04.experiment_plans.i04_grid_detect_then_xray_centre_plan.setup_beamline_for_oav", autospec=True, @@ -239,12 +254,12 @@ def test_i04_default_grid_detect_and_xray_centre_sets_transmission_and_triggers_ mock_wait: MagicMock, sim_run_engine: RunEngineSimulator, zocalo: ZocaloResults, - hyperion_fgs_params, + test_three_d_grid_params, i04_grid_detect_then_xrc_default_params: partial[MsgGenerator], ): desired_transmission = 0.4 mock_fix_transmission_and_exp_time.return_value = (desired_transmission, 1) - mock_create_parameters.return_value = hyperion_fgs_params + mock_create_parameters.return_value = test_three_d_grid_params simulate_xrc_result( sim_run_engine, zocalo, @@ -312,10 +327,10 @@ def test_i04_default_grid_detect_and_xray_centre_does_undulator_check_before_col mock_grid_detection_plan: MagicMock, mock_create_gridscan_callbacks: MagicMock, run_engine: RunEngine, - hyperion_fgs_params, + test_three_d_grid_params, i04_grid_detect_then_xrc_default_params: partial[MsgGenerator], ): - mock_create_parameters.return_value = hyperion_fgs_params + mock_create_parameters.return_value = test_three_d_grid_params mock_run_gridscan.side_effect = CompleteError with pytest.raises(CompleteError): run_engine(i04_grid_detect_then_xrc_default_params()) @@ -426,21 +441,6 @@ async def test_given_no_diffraction_found_i04_grid_detect_then_xrc_returns_sampl get_mock_put(smargon.z.user_setpoint).assert_has_calls([call(initial_z, wait=True)]) -@pytest.fixture() -def give_grid_common_specific_grid(test_fgs_params: SpecifiedThreeDGridScan): - # Using this fixture means grid common always comes with its specific grid params - class GridCommonWithSpecificGrid(GridCommon): - @property - def specified_grid_params(self): - return test_fgs_params - - with patch( - "mx_bluesky.beamlines.i04.experiment_plans.i04_grid_detect_then_xray_centre_plan.GridCommon", - GridCommonWithSpecificGrid, - ): - yield - - @patch( "mx_bluesky.beamlines.i04.experiment_plans.i04_grid_detect_then_xray_centre_plan.get_ready_for_oav_and_close_shutter", new=MagicMock(), @@ -472,8 +472,8 @@ def test_i04_grid_detect_then_xrc_calculates_exposure_and_transmission_then_uses mock_grid_detect_then_xray_centre: MagicMock, i04_grid_detect_then_xrc_default_params: partial[MsgGenerator], run_engine: RunEngine, - test_full_grid_scan_params: GridCommon, - test_fgs_params: SpecifiedThreeDGridScan, + test_full_grid_scan_params: GenericGrid, + test_three_d_grid_params: SpecifiedThreeDGridScan, give_grid_common_specific_grid, ): expected_trans_frac = 1 @@ -486,7 +486,7 @@ def test_i04_grid_detect_then_xrc_calculates_exposure_and_transmission_then_uses grid_common_params = mock_grid_detect_then_xray_centre.call_args.kwargs[ "parameters" ] - assert isinstance(grid_common_params, GridCommon) + assert isinstance(grid_common_params, GenericGrid) assert grid_common_params.exposure_time_s == expected_exposure_time assert grid_common_params.transmission_frac == expected_trans_frac @@ -494,7 +494,7 @@ def test_i04_grid_detect_then_xrc_calculates_exposure_and_transmission_then_uses @patch( "mx_bluesky.beamlines.i04.experiment_plans.i04_grid_detect_then_xray_centre_plan.fix_transmission_and_exposure_time_for_current_wavelength", ) -def test_get_grid_common_params( +def test_get_generic_grid_params( mock_fix_trans_and_exposure: MagicMock, tmp_path, ): @@ -511,7 +511,7 @@ def test_get_grid_common_params( detector_distance_mm=264.5, storage_directory=str(tmp_path), ) - grid_common_params = _get_grid_common_params(1, entry_params) + grid_common_params = _get_generic_grid_params(1, entry_params) assert grid_common_params.exposure_time_s == expected_exposure_time assert grid_common_params.transmission_frac == expected_trans_frac @@ -573,15 +573,18 @@ def test_grid_detect_then_xrc_stages_and_unstages_zocalo_and_gets_results( def test_detect_grid_and_do_gridscan_gives_params_specified_grid( mock_change_aperture_then_move_to_xtal: MagicMock, mock_create_flyscan_params: MagicMock, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, run_engine: RunEngine, i04_grid_detect_then_xrc_default_params: partial[MsgGenerator], ): - mock_create_flyscan_params.return_value = test_fgs_params + mock_create_flyscan_params.return_value = test_three_d_grid_params run_engine( i04_grid_detect_then_xrc_default_params( udc=False, ) ) mock_change_aperture_then_move_to_xtal.assert_called_once() - assert mock_change_aperture_then_move_to_xtal.call_args[0][1] == test_fgs_params + assert ( + mock_change_aperture_then_move_to_xtal.call_args[0][1] + == test_three_d_grid_params + ) 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 234cd56ff2..921ed85540 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 @@ -64,6 +64,7 @@ def null_plan() -> MsgGenerator: detector, synchrotron, scan_points=create_dummy_scan_spec(), + omega_starts_deg=[0, 90], plan_during_collection=null_plan, ) ) @@ -136,6 +137,7 @@ def event(self, doc: Event): detector, synchrotron, scan_points=expected_scan_points, + omega_starts_deg=[0, 90], ) ) diff --git a/tests/unit_tests/common/experiment_plans/inner_plans/test_read_hardware.py b/tests/unit_tests/common/experiment_plans/inner_plans/test_read_hardware.py index 15f835056d..34e09d705a 100644 --- a/tests/unit_tests/common/experiment_plans/inner_plans/test_read_hardware.py +++ b/tests/unit_tests/common/experiment_plans/inner_plans/test_read_hardware.py @@ -30,12 +30,12 @@ @pytest.fixture -def ispyb_plan(test_fgs_params: SpecifiedThreeDGridScan): +def ispyb_plan(test_three_d_grid_params: SpecifiedThreeDGridScan): @bpp.set_run_key_decorator(PlanNameConstants.GRIDSCAN_OUTER) @bpp.run_decorator( # attach experiment metadata to the start document md={ "subplan_name": PlanNameConstants.GRIDSCAN_OUTER, - "mx_bluesky_parameters": test_fgs_params.model_dump_json(), + "mx_bluesky_parameters": test_three_d_grid_params.model_dump_json(), } ) def standalone_read_hardware_for_ispyb(*args): 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 c1bf6d7b44..eae01e1e3f 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 @@ -102,10 +102,10 @@ class TestFlyscanXrayCentrePlan: def test_eiger2_x_16_detector_specified( self, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, ): assert ( - test_fgs_params.detector_params.detector_size_constants.det_type_string + test_three_d_grid_params.detector_params.detector_size_constants.det_type_string == EIGER_TYPE_EIGER2_X_16M ) @@ -119,7 +119,7 @@ def test_when_run_gridscan_called_ispyb_deposition_made_and_records_errors( self, run_engine: RunEngine, fake_fgs_composite: FlyScanEssentialDevices, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, beamline_specific: BeamlineSpecificFGSFeatures, ): ispyb_callback = GridscanISPyBCallback(param_type=SpecifiedThreeDGridScan) @@ -133,9 +133,11 @@ def test_when_run_gridscan_called_ispyb_deposition_made_and_records_errors( run_engine( ispyb_activation_wrapper( common_flyscan_xray_centre( - fake_fgs_composite, test_fgs_params, beamline_specific + fake_fgs_composite, + test_three_d_grid_params, + beamline_specific, ), - test_fgs_params, + test_three_d_grid_params, ), ) @@ -149,16 +151,14 @@ def test_when_run_gridscan_called_ispyb_deposition_made_and_records_errors( def test_results_passed_to_move_motors( self, bps_abs_set: MagicMock, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, fake_fgs_composite: FlyScanEssentialDevices, run_engine: RunEngine, ): from mx_bluesky.common.device_setup_plans.manipulate_sample import move_x_y_z - motor_position = ( - test_fgs_params.fast_gridscan_params.grid_position_to_motor_position( - np.array([1, 2, 3]) - ) + motor_position = test_three_d_grid_params.fast_gridscan_params.grid_position_to_motor_position( + np.array([1, 2, 3]) ) run_engine(move_x_y_z(fake_fgs_composite.smargon, *motor_position)) bps_abs_set.assert_called_with( @@ -179,7 +179,7 @@ def test_individual_plans_triggered_once_and_only_once_in_composite_run( run_gridscan: MagicMock, run_engine_with_subs: ReWithSubs, fake_fgs_composite: FlyScanEssentialDevices, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, beamline_specific: BeamlineSpecificFGSFeatures, ): run_engine, _ = run_engine_with_subs @@ -187,7 +187,7 @@ def test_individual_plans_triggered_once_and_only_once_in_composite_run( def wrapped_gridscan_and_move(): yield from common_flyscan_xray_centre( fake_fgs_composite, - test_fgs_params, + test_three_d_grid_params, beamline_specific, ) @@ -203,7 +203,7 @@ def test_waits_for_motion_program( self, check_topup_and_wait, run_engine: RunEngine, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, fake_fgs_composite: FlyScanEssentialDevices, ): fake_fgs_composite.eiger.unstage = MagicMock( @@ -219,10 +219,8 @@ def test_plan(): fgs, fake_fgs_composite.eiger, fake_fgs_composite.synchrotron, - [ - test_fgs_params.scan_points_first_grid, - test_fgs_params.scan_points_second_grid, - ], + test_three_d_grid_params.scan_points, + test_three_d_grid_params.omega_starts_deg, ) with pytest.raises(FailedStatus): @@ -240,19 +238,21 @@ def test_if_gridscan_prepare_fails_with_invalid_grid_then_sample_exception_raise run_engine: RunEngine, fake_fgs_composite: FlyScanEssentialDevices, beamline_specific: BeamlineSpecificFGSFeatures, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, ): beamline_specific.set_flyscan_params_plan = partial( set_fast_grid_scan_params, beamline_specific.fgs_motors, - test_fgs_params.fast_gridscan_params, + test_three_d_grid_params.fast_gridscan_params, ) set_mock_value(beamline_specific.fgs_motors.device_scan_invalid, 1.0) # type: ignore with pytest.raises(WarningError): run_engine( - run_gridscan(fake_fgs_composite, test_fgs_params, beamline_specific) + run_gridscan( + fake_fgs_composite, test_three_d_grid_params, beamline_specific + ) ) @patch( @@ -264,7 +264,7 @@ def test_if_gridscan_prepare_fails_with_other_exception_then_plan_re_raised( run_engine: RunEngine, fake_fgs_composite: FlyScanEssentialDevices, beamline_specific: BeamlineSpecificFGSFeatures, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, ): exception = FailedStatus() exception.__cause__ = Exception() @@ -273,7 +273,9 @@ def test_if_gridscan_prepare_fails_with_other_exception_then_plan_re_raised( with pytest.raises(FailedStatus) as e: run_engine( - run_gridscan(fake_fgs_composite, test_fgs_params, beamline_specific) + run_gridscan( + fake_fgs_composite, test_three_d_grid_params, beamline_specific + ) ) mock_kickoff_and_complete.assert_not_called() @@ -314,16 +316,15 @@ def test_when_grid_scan_ran_then_eiger_disarmed_before_zocalo_end( mock_kickoff, mock_abs_set, fake_fgs_composite: FlyScanEssentialDevices, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, run_engine_with_subs_snapshots_already_taken: tuple[ RunEngine, tuple[GridscanNexusFileCallback, GridscanISPyBCallback], ], beamline_specific: BeamlineSpecificFGSFeatures, ): - test_fgs_params.x_steps = 9 - test_fgs_params.y_steps = 10 - test_fgs_params.z_steps = 12 + test_three_d_grid_params.x_steps = 9 + test_three_d_grid_params.y_steps = [10, 12] run_engine, (nexus_cb, ispyb_cb) = run_engine_with_subs_snapshots_already_taken # Put both mocks in a parent to easily capture order mock_parent = MagicMock() @@ -348,9 +349,9 @@ def test_when_grid_scan_ran_then_eiger_disarmed_before_zocalo_end( run_engine( ispyb_activation_wrapper( common_flyscan_xray_centre( - fake_fgs_composite, test_fgs_params, beamline_specific + fake_fgs_composite, test_three_d_grid_params, beamline_specific ), - test_fgs_params, + test_three_d_grid_params, ) ) @@ -381,12 +382,16 @@ def test_fgs_arms_eiger_without_grid_detect( mock_complete, mock_wait, fake_fgs_composite: FlyScanEssentialDevices, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, run_engine: RunEngine, beamline_specific: BeamlineSpecificFGSFeatures, ): fake_fgs_composite.eiger.unstage = MagicMock(side_effect=completed_status) - run_engine(run_gridscan(fake_fgs_composite, test_fgs_params, beamline_specific)) + run_engine( + run_gridscan( + fake_fgs_composite, test_three_d_grid_params, beamline_specific + ) + ) fake_fgs_composite.eiger.stage.assert_called_once() # type: ignore fake_fgs_composite.eiger.unstage.assert_called_once() @@ -413,7 +418,7 @@ def test_when_grid_scan_fails_with_exception_then_detector_disarmed_and_correct_ mock_wait, mock_kickoff, fake_fgs_composite: FlyScanEssentialDevices, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, run_engine: RunEngine, beamline_specific: BeamlineSpecificFGSFeatures, ): @@ -439,7 +444,9 @@ def test_when_grid_scan_fails_with_exception_then_detector_disarmed_and_correct_ with pytest.raises(CompleteError): run_engine( bpp.run_wrapper( - run_gridscan(fake_fgs_composite, test_fgs_params, beamline_specific) + run_gridscan( + fake_fgs_composite, test_three_d_grid_params, beamline_specific + ) ) ) @@ -492,7 +499,8 @@ def test_kickoff_and_complete_gridscan_triggers_zocalo( zebra_fast_grid_scan, fake_fgs_composite.eiger, fake_fgs_composite.synchrotron, - scan_points=create_dummy_scan_spec(), + create_dummy_scan_spec(), + [0, 90], ) ) @@ -521,7 +529,7 @@ def test_kickoff_and_complete_gridscan_triggers_zocalo( def test_read_hardware_during_collection_occurs_after_eiger_arm( self, fake_fgs_composite: FlyScanEssentialDevices, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, sim_run_engine: RunEngineSimulator, beamline_specific: BeamlineSpecificFGSFeatures, ): @@ -536,7 +544,9 @@ def test_read_hardware_during_collection_occurs_after_eiger_arm( "synchrotron-synchrotron_mode", ) msgs = sim_run_engine.simulate_plan( - run_gridscan(fake_fgs_composite, test_fgs_params, beamline_specific) + run_gridscan( + fake_fgs_composite, test_three_d_grid_params, beamline_specific + ) ) msgs = assert_message_and_return_remaining( msgs, lambda msg: msg.command == "stage" and msg.obj.name == "eiger" @@ -568,22 +578,24 @@ def test_when_gridscan_succeeds_and_results_fetched_ispyb_comment_appended_to( RunEngine, tuple[GridscanNexusFileCallback, GridscanISPyBCallback], ], - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, fake_fgs_composite: FlyScanEssentialDevices, beamline_specific: BeamlineSpecificFGSFeatures, ): run_engine, (nexus_cb, ispyb_cb) = run_engine_with_subs def _wrapped_gridscan_and_move(): - run_generic_ispyb_handler_setup(ispyb_cb, test_fgs_params) + run_generic_ispyb_handler_setup(ispyb_cb, test_three_d_grid_params) yield from common_flyscan_xray_centre( fake_fgs_composite, - test_fgs_params, + test_three_d_grid_params, beamline_specific, ) run_engine( - ispyb_activation_wrapper(_wrapped_gridscan_and_move(), test_fgs_params) + ispyb_activation_wrapper( + _wrapped_gridscan_and_move(), test_three_d_grid_params + ) ) app_to_comment: MagicMock = ispyb_cb.ispyb.append_to_comment # type:ignore app_to_comment.assert_called() 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 88ee27981c..98ab4ba6d2 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 @@ -30,7 +30,7 @@ GridscanISPyBCallback, ispyb_activation_wrapper, ) -from mx_bluesky.common.parameters.gridscan import GridCommon, SpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import GenericGrid, SpecifiedThreeDGridScan from mx_bluesky.common.utils.exceptions import WarningError from ...conftest import assert_event @@ -208,13 +208,13 @@ async def test_when_grid_detection_plan_run_then_ispyb_callback_gets_correct_val fake_devices: tuple[OavGridDetectionComposite, MagicMock], run_engine: RunEngine, test_config_files: dict[str, str], - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, tmp_path: Path, dummy_rotation_data_collection_group_info, ): params = OAVParameters("loopCentring", test_config_files["oav_config_json"]) composite, _ = fake_devices - cb = GridscanISPyBCallback(param_type=GridCommon) + cb = GridscanISPyBCallback(param_type=GenericGrid) cb.data_collection_group_info = dummy_rotation_data_collection_group_info run_engine.subscribe(cb) @@ -222,7 +222,7 @@ async def test_when_grid_detection_plan_run_then_ispyb_callback_gets_correct_val run_engine( ispyb_activation_wrapper( do_grid_and_edge_detect(composite, params, tmp_path), - test_fgs_params, + test_three_d_grid_params, ) ) @@ -277,7 +277,7 @@ def test_when_grid_detection_plan_run_then_grid_detection_callback_gets_correct_ fake_devices: tuple[OavGridDetectionComposite, MagicMock], run_engine: RunEngine, test_config_files: dict[str, str], - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, tmp_path: Path, ): params = OAVParameters("loopCentring", test_config_files["oav_config_json"]) @@ -288,7 +288,8 @@ def test_when_grid_detection_plan_run_then_grid_detection_callback_gets_correct_ run_engine( ispyb_activation_wrapper( - do_grid_and_edge_detect(composite, params, tmp_path), test_fgs_params + do_grid_and_edge_detect(composite, params, tmp_path), + test_three_d_grid_params, ) ) @@ -317,7 +318,7 @@ def test_when_grid_detection_plan_run_with_different_omega_order_then_grid_detec fake_devices: tuple[OavGridDetectionComposite, MagicMock], run_engine: RunEngine, test_config_files: dict[str, str], - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, tmp_path: Path, ): params = OAVParameters("loopCentring", test_config_files["oav_config_json"]) @@ -335,7 +336,8 @@ def test_when_grid_detection_plan_run_with_different_omega_order_then_grid_detec run_engine( ispyb_activation_wrapper( - do_grid_and_edge_detect(composite, params, tmp_path), test_fgs_params + do_grid_and_edge_detect(composite, params, tmp_path), + test_three_d_grid_params, ) ) 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 30bd1fd9dc..b09ad0c957 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 @@ -13,7 +13,7 @@ def test_visit_extracted_from_numtracker( - run_engine: RunEngine, test_fgs_params: SpecifiedThreeDGridScan + run_engine: RunEngine, test_three_d_grid_params: SpecifiedThreeDGridScan ): test_visit = "test_visit" @@ -22,8 +22,8 @@ def test_visit_extracted_from_numtracker( callback = BaseISPyBCallback() callback.activity_gated_stop = MagicMock() - test_fgs_params.visit = USE_NUMTRACKER - callback.params = test_fgs_params + test_three_d_grid_params.visit = USE_NUMTRACKER + callback.params = test_three_d_grid_params run_engine.subscribe(callback) @bpp.run_decorator( @@ -40,12 +40,12 @@ def test_plan(): def test_exception_when_instrument_session_doesnt_exist( - run_engine: RunEngine, test_fgs_params: SpecifiedThreeDGridScan + run_engine: RunEngine, test_three_d_grid_params: SpecifiedThreeDGridScan ): callback = BaseISPyBCallback() callback.activity_gated_stop = MagicMock() - test_fgs_params.visit = USE_NUMTRACKER - callback.params = test_fgs_params + test_three_d_grid_params.visit = USE_NUMTRACKER + callback.params = test_three_d_grid_params run_engine.subscribe(callback) @bpp.run_decorator( 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 2f0a860cce..b1c9d67e69 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 @@ -16,7 +16,9 @@ _smargon_omega_to_xyxz_plane, ) from mx_bluesky.common.parameters.constants import DocDescriptorNames -from mx_bluesky.hyperion.parameters.gridscan import GridCommonWithHyperionDetectorParams +from mx_bluesky.hyperion.parameters.gridscan import ( + GenericGridWithHyperionDetectorParams, +) from .....conftest import ( EXPECTED_START_TIME, @@ -72,7 +74,7 @@ class TestXrayCentreISPyBCallback: def test_activity_gated_start_3d(self, mock_ispyb_conn, test_event_data, tmp_path): callback = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) callback.activity_gated_start( test_event_data.test_grid_detect_and_gridscan_start_document @@ -115,7 +117,7 @@ def test_reason_provided_if_crystal_not_found_error( self, mock_update_data_collection_group_table, mock_ispyb_conn, test_event_data ): callback = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) callback.activity_gated_start( test_event_data.test_grid_detect_and_gridscan_start_document @@ -136,7 +138,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( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) callback.activity_gated_start( test_event_data.test_grid_detect_and_gridscan_start_document @@ -165,7 +167,7 @@ def test_hardware_read_event_3d(self, mock_ispyb_conn, test_event_data): def test_flux_read_events_3d(self, mock_ispyb_conn, test_event_data): callback = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) callback.activity_gated_start( test_event_data.test_grid_detect_and_gridscan_start_document @@ -230,7 +232,7 @@ def test_activity_gated_event_oav_snapshot_triggered( first_comment: str, ): callback = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) callback.activity_gated_start( test_event_data.test_grid_detect_and_gridscan_start_document @@ -333,7 +335,7 @@ 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( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) callback._handle_ispyb_hardware_read = MagicMock() callback._handle_ispyb_transmission_flux_read = MagicMock() @@ -380,7 +382,7 @@ def test_given_event_doc_before_start_doc_received_then_exception_raised( test_event_data, ): callback = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) callback.activity_gated_descriptor( test_event_data.test_descriptor_document_oav_snapshot @@ -399,7 +401,7 @@ def test_ispyb_callback_clears_state_after_run_stop( self, test_event_data, mock_ispyb_conn ): callback = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) callback.active = True callback.start(test_event_data.test_grid_detect_and_gridscan_start_document) # type: ignore 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 38208065c7..1abe4d71a5 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 @@ -12,7 +12,9 @@ ) from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER from mx_bluesky.hyperion.external_interaction.callbacks.__main__ import setup_logging -from mx_bluesky.hyperion.parameters.gridscan import GridCommonWithHyperionDetectorParams +from mx_bluesky.hyperion.parameters.gridscan import ( + GenericGridWithHyperionDetectorParams, +) from .....conftest import TestData @@ -52,7 +54,7 @@ def mock_store_in_ispyb(config, *args, **kwargs) -> StoreInIspyb: class TestXrayCentreIspybHandler: def test_fgs_failing_results_in_bad_run_status_in_ispyb(self, test_event_data): ispyb_handler = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) ispyb_handler.activity_gated_start( test_event_data.test_grid_detect_and_gridscan_start_document @@ -87,7 +89,7 @@ def test_fgs_raising_no_exception_results_in_good_run_status_in_ispyb( self, test_event_data ): ispyb_handler = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) ispyb_handler.activity_gated_start( test_event_data.test_grid_detect_and_gridscan_start_document @@ -133,7 +135,7 @@ def test_given_ispyb_callback_started_writing_to_ispyb_when_messages_logged_then gelf_handler.emit = MagicMock() ispyb_handler = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) ispyb_handler.activity_gated_start( test_event_data.test_grid_detect_and_gridscan_start_document @@ -169,7 +171,7 @@ def test_given_ispyb_callback_finished_writing_to_ispyb_when_messages_logged_the gelf_handler.emit = MagicMock() ispyb_handler = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams + param_type=GenericGridWithHyperionDetectorParams ) ispyb_handler.activity_gated_start( test_event_data.test_grid_detect_and_gridscan_start_document @@ -202,7 +204,7 @@ def test_given_fgs_plan_finished_when_zocalo_results_event_then_expected_comment self, mock_time, dummy_rotation_data_collection_group_info, test_event_data ): ispyb_handler = GridscanISPyBCallback( - param_type=GridCommonWithHyperionDetectorParams, + param_type=GenericGridWithHyperionDetectorParams, ) ispyb_handler.activity_gated_start( diff --git a/tests/unit_tests/common/utils/test_xrc_result.py b/tests/unit_tests/common/utils/test_xrc_result.py index 274d70cf41..126a529c1d 100644 --- a/tests/unit_tests/common/utils/test_xrc_result.py +++ b/tests/unit_tests/common/utils/test_xrc_result.py @@ -33,14 +33,14 @@ async def test_results_adjusted_and_event_raised( - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, run_engine: RunEngine, zocalo: ZocaloResults, ): x_ray_centre_event_handler = XRayCentreEventHandler() run_engine.subscribe(x_ray_centre_event_handler) mock_zocalo_trigger(zocalo, TestData.test_result_large) - run_engine(fetch_xrc_results_from_zocalo(zocalo, test_fgs_params)) + run_engine(fetch_xrc_results_from_zocalo(zocalo, test_three_d_grid_params)) actual = x_ray_centre_event_handler.xray_centre_results expected = XRayCentreResult( @@ -60,7 +60,7 @@ async def test_results_adjusted_and_event_raised( def test_fetch_results_discards_results_below_threshold( - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, run_engine: RunEngine, zocalo: ZocaloResults, ): @@ -73,7 +73,7 @@ def test_fetch_results_discards_results_below_threshold( + TestData.test_result_below_threshold + TestData.test_result_small, ) - run_engine(fetch_xrc_results_from_zocalo(zocalo, test_fgs_params)) + run_engine(fetch_xrc_results_from_zocalo(zocalo, test_three_d_grid_params)) assert callback.xray_centre_results and len(callback.xray_centre_results) == 2 assert [r.max_count for r in callback.xray_centre_results] == [50000, 1000] @@ -81,18 +81,18 @@ def test_fetch_results_discards_results_below_threshold( def test_no_xtal_found_raises_exception( run_engine: RunEngine, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, zocalo: ZocaloResults, ): mock_zocalo_trigger(zocalo, []) with pytest.raises(CrystalNotFoundError): - run_engine(fetch_xrc_results_from_zocalo(zocalo, test_fgs_params)) + run_engine(fetch_xrc_results_from_zocalo(zocalo, test_three_d_grid_params)) def test_dummy_result_returned_when_no_xtal_and_commissioning_mode_enabled( run_engine: RunEngine, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, fake_fgs_composite: FlyScanEssentialDevices, beamline_specific: BeamlineSpecificFGSFeatures, zocalo: ZocaloResults, @@ -103,11 +103,11 @@ def test_dummy_result_returned_when_no_xtal_and_commissioning_mode_enabled( mock_zocalo_trigger(zocalo, []) - run_engine(fetch_xrc_results_from_zocalo(zocalo, test_fgs_params)) + run_engine(fetch_xrc_results_from_zocalo(zocalo, test_three_d_grid_params)) results = xrc_event_handler.xray_centre_results or [] assert len(results) == 1 result = results[0] - assert result.sample_id == test_fgs_params.sample_id + assert result.sample_id == test_three_d_grid_params.sample_id assert result.max_count == 10000 assert result.total_count == 100000 assert all(np.isclose(result.bounding_box_mm[0], [1.95, 0.95, 0.45])) diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index a409a0fee2..5f76ab8f44 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -72,7 +72,7 @@ from mx_bluesky.common.parameters.device_composites import ( GridDetectThenXRayCentreComposite, ) -from mx_bluesky.common.parameters.gridscan import GridCommon, SpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import GenericGrid, SpecifiedThreeDGridScan from mx_bluesky.hyperion.parameters.device_composites import ( HyperionGridDetectThenXRayCentreComposite, ) @@ -204,7 +204,7 @@ def use_beamline_t01(): @pytest.fixture -def mock_subscriptions(test_fgs_params): +def mock_subscriptions(test_three_d_grid_params): with ( patch( "mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback.ZocaloTrigger", @@ -240,15 +240,6 @@ def run_engine_with_subs( yield run_engine, mock_subscriptions -@pytest.fixture -def test_fgs_params(tmp_path): - return SpecifiedThreeDGridScan( - **raw_params_from_file( - "tests/test_data/parameter_json_files/good_test_parameters.json", tmp_path - ) - ) - - def mock_zocalo_trigger(zocalo: ZocaloResults, result): @AsyncStatus.wrap async def mock_complete(results): @@ -329,7 +320,7 @@ async def zebra_fast_grid_scan(): @pytest.fixture async def fake_fgs_composite( smargon: Smargon, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, attenuator, xbpm_feedback, synchrotron, @@ -347,7 +338,9 @@ async def fake_fgs_composite( fake_composite.eiger.stage = MagicMock(side_effect=lambda: completed_status()) # unstage should be mocked on a per-test basis because several rely on unstage - fake_composite.eiger.set_detector_parameters(test_fgs_params.detector_params) + fake_composite.eiger.set_detector_parameters( + test_three_d_grid_params.detector_params + ) fake_composite.eiger.stop_odin_when_all_frames_collected = MagicMock() fake_composite.eiger.odin.check_and_wait_for_odin_state = lambda timeout: True @@ -401,7 +394,7 @@ def test_full_grid_scan_params(tmp_path): "tests/test_data/parameter_json_files/good_test_grid_with_edge_detect_parameters.json", tmp_path, ) - return GridCommon(**params) + return GenericGrid(**params) @pytest.fixture diff --git a/tests/unit_tests/hyperion/conftest.py b/tests/unit_tests/hyperion/conftest.py index 437c5c86dd..4613783b5a 100644 --- a/tests/unit_tests/hyperion/conftest.py +++ b/tests/unit_tests/hyperion/conftest.py @@ -101,15 +101,6 @@ def test_multi_rotation_params(tmp_path): ) -@pytest.fixture -def test_fgs_params(tmp_path): - return HyperionSpecifiedThreeDGridScan( - **raw_params_from_file( - "tests/test_data/parameter_json_files/good_test_parameters.json", tmp_path - ) - ) - - @pytest.fixture(params=[False, True]) def test_omega_flip(request): with patch( @@ -126,7 +117,7 @@ def fgs_params_use_panda(tmp_path): new="tests/test_data/test_domain_properties_with_panda", ): params = raw_params_from_file( - "tests/test_data/parameter_json_files/good_test_parameters.json", + "tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json", tmp_path, ) yield HyperionSpecifiedThreeDGridScan(**params) diff --git a/tests/unit_tests/hyperion/experiment_plans/test_load_centre_collect_full_plan.py b/tests/unit_tests/hyperion/experiment_plans/test_load_centre_collect_full_plan.py index f5e2532235..0d26f86a28 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_load_centre_collect_full_plan.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_load_centre_collect_full_plan.py @@ -89,13 +89,13 @@ @pytest.fixture def load_centre_collect_params_with_patched_create_params( load_centre_collect_params: LoadCentreCollect, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, ): with patch( "mx_bluesky.hyperion.experiment_plans.pin_centre_then_xray_centre_plan.create_parameters_for_grid_detection" ) as mock_create_params: load_centre_collect_params.robot_load_then_centre.set_specified_grid_params( - test_fgs_params + test_three_d_grid_params ) mock_create_params.return_value = ( load_centre_collect_params.robot_load_then_centre @@ -1072,7 +1072,7 @@ def test_box_size_passed_through_to_gridscan( load_centre_collect_params: LoadCentreCollect, oav_parameters_for_rotation: OAVParameters, run_engine: RunEngine, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, load_centre_collect_params_with_patched_create_params, ): run_engine( @@ -1081,7 +1081,7 @@ def test_box_size_passed_through_to_gridscan( ) ) detect_grid_call = mock_detect_grid.mock_calls[0] - assert detect_grid_call.args[1].box_size_um == test_fgs_params.box_size_um + assert detect_grid_call.args[1].box_size_um == test_three_d_grid_params.box_size_um @patch( 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 5d206428cc..f9b33a73f2 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 @@ -72,14 +72,14 @@ def test_pin_centre_then_xray_centre_params( @pytest.fixture def pin_centre_then_xray_centre_params_with_patched_create_params( - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, test_pin_centre_then_xray_centre_params: PinTipCentreThenXrayCentre, ): with patch( "mx_bluesky.hyperion.experiment_plans.pin_centre_then_xray_centre_plan.create_parameters_for_grid_detection" ) as mock_create_params: test_pin_centre_then_xray_centre_params.set_specified_grid_params( - test_fgs_params + test_three_d_grid_params ) mock_create_params.return_value = test_pin_centre_then_xray_centre_params yield test_pin_centre_then_xray_centre_params @@ -569,11 +569,11 @@ def test_detect_grid_and_do_gridscan_gives_params_specified_grid( mock_create_flyscan_params: MagicMock, test_pin_centre_then_xray_centre_params: PinTipCentreThenXrayCentre, hyperion_grid_detect_xrc_devices: HyperionGridDetectThenXRayCentreComposite, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, test_config_files, run_engine: RunEngine, ): - mock_create_flyscan_params.return_value = test_fgs_params + mock_create_flyscan_params.return_value = test_three_d_grid_params run_engine( pin_centre_then_xray_centre_plan( hyperion_grid_detect_xrc_devices, @@ -582,4 +582,7 @@ def test_detect_grid_and_do_gridscan_gives_params_specified_grid( ) ) mock_change_aperture_then_move_to_xtal.assert_called_once() - assert mock_change_aperture_then_move_to_xtal.call_args[0][1] == test_fgs_params + assert ( + mock_change_aperture_then_move_to_xtal.call_args[0][1] + == test_three_d_grid_params + ) diff --git a/tests/unit_tests/hyperion/experiment_plans/test_robot_load_then_centre.py b/tests/unit_tests/hyperion/experiment_plans/test_robot_load_then_centre.py index e84521826e..b0feb358b2 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_robot_load_then_centre.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_robot_load_then_centre.py @@ -40,12 +40,14 @@ def robot_load_then_centre_params(tmp_path): @pytest.fixture def robot_load_then_centre_params_with_patched_create_params( robot_load_then_centre_params: RobotLoadThenCentre, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, ): with patch( "mx_bluesky.hyperion.experiment_plans.pin_centre_then_xray_centre_plan.create_parameters_for_grid_detection" ) as mock_create_params: - robot_load_then_centre_params.set_specified_grid_params(test_fgs_params) + robot_load_then_centre_params.set_specified_grid_params( + test_three_d_grid_params + ) mock_create_params.return_value = robot_load_then_centre_params yield @@ -495,7 +497,7 @@ def test_box_size_passed_through_to_gridscan( robot_load_then_centre_params: RobotLoadThenCentre, grid_detection_callback_with_detected_grid: MagicMock, run_engine: RunEngine, - test_fgs_params: SpecifiedThreeDGridScan, + test_three_d_grid_params: SpecifiedThreeDGridScan, robot_load_then_centre_params_with_patched_create_params, ): run_engine( @@ -505,7 +507,7 @@ def test_box_size_passed_through_to_gridscan( ) ) detect_grid_call = mock_detect_grid.mock_calls[0] - assert detect_grid_call.args[1].box_size_um == test_fgs_params.box_size_um + assert detect_grid_call.args[1].box_size_um == test_three_d_grid_params.box_size_um async def test_multiple_devices(dcm, undulator, undulator_dcm): diff --git a/tests/unit_tests/hyperion/external_interaction/conftest.py b/tests/unit_tests/hyperion/external_interaction/conftest.py index 94edecacba..c78ea94779 100644 --- a/tests/unit_tests/hyperion/external_interaction/conftest.py +++ b/tests/unit_tests/hyperion/external_interaction/conftest.py @@ -26,8 +26,7 @@ def test_rotation_params(tmp_path): param_dict["scan_width_deg"] = 360.0 params = SingleRotationScan(**param_dict) params.x_start_um = 0 - params.y_start_um = 0 - params.z_start_um = 0 + params.y_starts_um = [0, 0] params.exposure_time_s = 0.004 return params @@ -41,8 +40,8 @@ def test_fgs_params(request, tmp_path): first_scan_img = (request.param // 10) * 6 second_scan_img = (request.param // 10) * 4 params.x_steps = 5 - params.y_steps = first_scan_img // 5 - params.z_steps = second_scan_img // 5 + params.y_steps[0] = first_scan_img // 5 + params.y_steps[1] = second_scan_img // 5 params.storage_directory = ( os.path.dirname(os.path.realpath(__file__)) + "/test_data" ) 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 248c001f8c..961991714f 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 @@ -45,16 +45,14 @@ def create_nexus_writer(parameters: HyperionSpecifiedThreeDGridScan, writer_num) if writer_num == 1 else parameters.num_images - parameters.scan_indices[1] ) - points = ( - parameters.scan_points_first_grid - if writer_num == 1 - else parameters.scan_points_second_grid - ) + points = parameters.scan_points[0] if writer_num == 1 else parameters.scan_points[1] data_shape = (n_img, d_size.width, d_size.height) run_number = parameters.detector_params.run_number + writer_num - 1 vds_start = 0 if writer_num == 1 else parameters.scan_indices[1] omega_start = ( - parameters.grid1_omega_deg if writer_num == 1 else parameters.grid2_omega_deg + parameters.omega_starts_deg[0] + if writer_num == 1 + else parameters.omega_starts_deg[1] ) nexus_writer = NexusWriter( parameters, @@ -84,8 +82,8 @@ def create_nexus_writers(parameters: HyperionSpecifiedThreeDGridScan): @pytest.fixture -def dummy_nexus_writers(test_fgs_params: HyperionSpecifiedThreeDGridScan): - with create_nexus_writers(test_fgs_params) as ( +def dummy_nexus_writers(test_three_d_grid_params: HyperionSpecifiedThreeDGridScan): + with create_nexus_writers(test_three_d_grid_params) as ( nexus_writer_1, nexus_writer_2, ): @@ -94,13 +92,13 @@ def dummy_nexus_writers(test_fgs_params: HyperionSpecifiedThreeDGridScan): @pytest.fixture def dummy_nexus_writers_with_more_images( - test_fgs_params: HyperionSpecifiedThreeDGridScan, + test_three_d_grid_params: HyperionSpecifiedThreeDGridScan, ): x, y, z = 45, 35, 25 - test_fgs_params.x_steps = x - test_fgs_params.y_steps = y - test_fgs_params.z_steps = z - with create_nexus_writers(test_fgs_params) as ( + test_three_d_grid_params.x_steps = x + test_three_d_grid_params.y_steps[0] = y + test_three_d_grid_params.y_steps[1] = z + with create_nexus_writers(test_three_d_grid_params) as ( nexus_writer_1, nexus_writer_2, ): @@ -108,14 +106,16 @@ def dummy_nexus_writers_with_more_images( @pytest.fixture -def single_dummy_file(test_fgs_params: HyperionSpecifiedThreeDGridScan): - test_fgs_params.use_roi_mode = True - d_size = test_fgs_params.detector_params.detector_size_constants.det_size_pixels - data_shape = (test_fgs_params.scan_indices[1], d_size.width, d_size.height) +def single_dummy_file(test_three_d_grid_params: HyperionSpecifiedThreeDGridScan): + test_three_d_grid_params.use_roi_mode = True + d_size = ( + test_three_d_grid_params.detector_params.detector_size_constants.det_size_pixels + ) + data_shape = (test_three_d_grid_params.scan_indices[1], d_size.width, d_size.height) nexus_writer = NexusWriter( - test_fgs_params, + test_three_d_grid_params, data_shape, - scan_points=test_fgs_params.scan_points_first_grid, + scan_points=test_three_d_grid_params.scan_points[0], run_number=1, ) yield nexus_writer @@ -125,12 +125,12 @@ def single_dummy_file(test_fgs_params: HyperionSpecifiedThreeDGridScan): @pytest.mark.parametrize( - "test_fgs_params, expected_num_of_files", + "test_three_d_grid_params, expected_num_of_files", [(2550, 3), (4000, 4), (8975, 9)], - indirect=["test_fgs_params"], + indirect=["test_three_d_grid_params"], ) def test_given_number_of_images_above_1000_then_expected_datafiles_used( - test_fgs_params: HyperionSpecifiedThreeDGridScan, + test_three_d_grid_params: HyperionSpecifiedThreeDGridScan, expected_num_of_files: Literal[3, 4, 9], single_dummy_file: NexusWriter, ): @@ -148,11 +148,13 @@ def test_given_number_of_images_above_1000_then_expected_datafiles_used( def test_given_dummy_data_then_datafile_written_correctly( - test_fgs_params: HyperionSpecifiedThreeDGridScan, + test_three_d_grid_params: HyperionSpecifiedThreeDGridScan, dummy_nexus_writers: tuple[NexusWriter, NexusWriter], ): nexus_writer_1, nexus_writer_2 = dummy_nexus_writers - grid_scan_params: ZebraGridScanParamsThreeD = test_fgs_params.fast_gridscan_params + grid_scan_params: ZebraGridScanParamsThreeD = ( + test_three_d_grid_params.fast_gridscan_params + ) nexus_writer_1.create_nexus_file(np.uint16) for filename in [nexus_writer_1.nexus_file, nexus_writer_1.master_file]: @@ -324,11 +326,12 @@ def assert_contains_external_link(data_path, entry_name, file_name): def test_nexus_writer_files_are_formatted_as_expected( - test_fgs_params: HyperionSpecifiedThreeDGridScan, single_dummy_file: NexusWriter + test_three_d_grid_params: HyperionSpecifiedThreeDGridScan, + single_dummy_file: NexusWriter, ): for file in [single_dummy_file.nexus_file, single_dummy_file.master_file]: file_name = os.path.basename(file.name) - expected_file_name_prefix = test_fgs_params.file_name + "_1" + expected_file_name_prefix = test_three_d_grid_params.file_name + "_1" assert file_name.startswith(expected_file_name_prefix) @@ -344,11 +347,15 @@ def test_nexus_writer_writes_width_and_height_correctly(single_dummy_file: Nexus @patch.dict(os.environ, {"BEAMLINE": "i03"}) def test_nexus_writer_writes_beamline_name_correctly( - test_fgs_params: HyperionSpecifiedThreeDGridScan, + test_three_d_grid_params: HyperionSpecifiedThreeDGridScan, ): - d_size = test_fgs_params.detector_params.detector_size_constants.det_size_pixels - data_shape = (test_fgs_params.num_images, d_size.width, d_size.height) - nexus_writer = NexusWriter(test_fgs_params, data_shape, test_fgs_params.scan_points) + d_size = ( + test_three_d_grid_params.detector_params.detector_size_constants.det_size_pixels + ) + data_shape = (test_three_d_grid_params.num_images, d_size.width, d_size.height) + nexus_writer = NexusWriter( + test_three_d_grid_params, data_shape, test_three_d_grid_params.scan_points[0] + ) assert nexus_writer.source.beamline == "i03" @@ -414,10 +421,10 @@ def test_given_some_datafiles_outside_of_virtual_dataset_range_then_they_are_not def test_given_data_files_not_yet_written_when_nexus_files_created_then_nexus_files_still_written( - test_fgs_params: HyperionSpecifiedThreeDGridScan, + test_three_d_grid_params: HyperionSpecifiedThreeDGridScan, ): - test_fgs_params.file_name = "non_existant_file" - with create_nexus_writers(test_fgs_params) as ( + test_three_d_grid_params.file_name = "non_existant_file" + with create_nexus_writers(test_three_d_grid_params) as ( nexus_writer_1, nexus_writer_2, ): diff --git a/tests/unit_tests/hyperion/test_main_system.py b/tests/unit_tests/hyperion/test_main_system.py index 1a99886226..bd97a12502 100644 --- a/tests/unit_tests/hyperion/test_main_system.py +++ b/tests/unit_tests/hyperion/test_main_system.py @@ -385,7 +385,8 @@ def test_start_with_json_file_with_extras_gives_error( test_env.mock_run_engine.run_engine_takes_time = False params = raw_params_from_file( - "tests/test_data/parameter_json_files/good_test_parameters.json", tmp_path + "tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json", + tmp_path, ) params["extra_param"] = "test" response = test_env.client.put(START_ENDPOINT, json=params) From 0a84a71175df4b3676205af1d0c33b8c590a3013 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Wed, 11 Feb 2026 12:31:41 +0000 Subject: [PATCH 03/45] wip --- .../experiment_plans/inner_plans/do_fgs.py | 3 +- .../common/grid_detection_callback.py | 12 +++---- .../common/parameters/components.py | 2 +- src/mx_bluesky/common/parameters/gridscan.py | 2 +- .../external_interaction/agamemnon.py | 2 +- ...test_grid_with_edge_detect_parameters.json | 7 ++-- .../good_test_load_centre_collect_params.json | 2 +- ..._centre_collect_params_multi_rotation.json | 9 ++++-- ...d_test_multi_rotation_scan_parameters.json | 2 +- ...st_one_multi_rotation_scan_parameters.json | 2 +- ...multi_rotation_scan_parameters_nomove.json | 2 +- ...in_centre_then_xray_centre_parameters.json | 7 ++-- ...ood_test_robot_load_and_centre_params.json | 7 ++-- .../good_test_robot_load_params.json | 4 +-- .../good_test_rotation_scan_parameters.json | 2 +- ...od_test_specified_three_d_grid_params.json | 2 +- ...ispyb_gridscan_system_test_parameters.json | 2 +- ...tre_collect_params_top_n_by_max_count.json | 7 ++-- .../test_gridscan_param_defaults.json | 2 +- .../test_oav_snapshot_params.json | 9 ++++-- .../test_grid_detection_plan.py | 10 ++++-- .../external_interaction/test_agamemnon.py | 6 ++-- .../parameters/test_parameter_model.py | 32 ++++++++----------- 23 files changed, 78 insertions(+), 57 deletions(-) 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 7a31ff79b6..058f761e5a 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 @@ -93,7 +93,8 @@ def kickoff_and_complete_gridscan( "omega_to_scan_spec": { # These have to be cast to strings due to a bug in orsjon. See # https://github.com/ijl/orjson/issues/414 - str(omega_starts_deg[i]): scan_points[i] + # TODO this errors if it's a float due to bluesky stuff, do we need the omega angle to be an int? + str(int(omega_starts_deg[i])): scan_points[i] for i in range(len(omega_starts_deg)) }, } diff --git a/src/mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py index 272ffce3e1..ae1b4dfd55 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py @@ -30,12 +30,12 @@ class GridParamUpdate(TypedDict): """ x_start_um: float - y_start_um: list[float] - z_start_um: list[float] + y_starts_um: list[float] + z_starts_um: list[float] x_steps: int y_steps: list[int] x_step_size_um: float - y_step_size_um: list[float] + y_step_sizes_um: list[float] class XYZParams(TypedDict, Generic[T]): @@ -117,10 +117,10 @@ def event(self, doc: Event): def get_grid_parameters(self) -> GridParamUpdate: return { "x_start_um": self.start_positions_um["x"], - "y_start_um": [self.start_positions_um["y"], self.start_positions_um["y"]], - "z_start_um": [self.start_positions_um["z"], self.start_positions_um["z"]], + "y_starts_um": [self.start_positions_um["y"], self.start_positions_um["y"]], + "z_starts_um": [self.start_positions_um["z"], self.start_positions_um["z"]], "x_steps": self.box_numbers["x"], "y_steps": [self.box_numbers["y"], self.box_numbers["z"]], "x_step_size_um": self.x_step_size_um, - "y_step_size_um": [self.y_step_size_um, self.z_step_size_um], + "y_step_sizes_um": [self.y_step_size_um, self.z_step_size_um], } diff --git a/src/mx_bluesky/common/parameters/components.py b/src/mx_bluesky/common/parameters/components.py index 6ee97801b0..093f141eca 100644 --- a/src/mx_bluesky/common/parameters/components.py +++ b/src/mx_bluesky/common/parameters/components.py @@ -30,7 +30,7 @@ GridscanParamConstants, ) -PARAMETER_VERSION = Version.parse("5.3.0") +PARAMETER_VERSION = Version.parse("6.0.0") def get_param_version() -> SemanticVersion: diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 7834270e96..99c84ad33a 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -138,7 +138,7 @@ class SpecifiedGrids(GenericGrid, XyzStarts, WithScan, Generic[GridScanParamType # In a 3D grid scan, n_steps[0] and n_steps[1] refers to Y and Z respectively. # We do an omega rotation between scanning across N dimensions to make N different axes y_step_sizes_um: list[PositiveFloat] = Field( - default=[GridscanParamConstants.BOX_WIDTH_UM] + default=[GridscanParamConstants.BOX_WIDTH_UM] * 2 ) x_steps: PositiveInt # Currently this must be the same for each grid for panda scan y_steps: list[PositiveInt] diff --git a/src/mx_bluesky/hyperion/external_interaction/agamemnon.py b/src/mx_bluesky/hyperion/external_interaction/agamemnon.py index 3fe676be40..17f8a62cc8 100644 --- a/src/mx_bluesky/hyperion/external_interaction/agamemnon.py +++ b/src/mx_bluesky/hyperion/external_interaction/agamemnon.py @@ -260,7 +260,7 @@ def _populate_parameters_from_agamemnon( "file_name": file_name, "tip_offset_um": pin_type.full_width / 2, "grid_width_um": pin_type.full_width, - "omega_start_deg": 0.0, + "omega_starts_deg": [0.0, 90.0], "chi_start_deg": collection["chi"], "transmission_frac": 1.0, **with_energy_params, diff --git a/tests/test_data/parameter_json_files/good_test_grid_with_edge_detect_parameters.json b/tests/test_data/parameter_json_files/good_test_grid_with_edge_detect_parameters.json index e0b2d30681..f808b0fdde 100644 --- a/tests/test_data/parameter_json_files/good_test_grid_with_edge_detect_parameters.json +++ b/tests/test_data/parameter_json_files/good_test_grid_with_edge_detect_parameters.json @@ -1,5 +1,5 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "beamline": "BL03S", "insertion_prefix": "SR03S", "storage_directory": "/tmp", @@ -10,7 +10,10 @@ "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", "exposure_time_s": 0.1, "detector_distance_mm": 100.0, - "omega_start_deg": 0.0, + "omega_starts_deg": [ + 0.0, + 90.0 + ], "grid_width_um": 290.6, "transmission_frac": 1.0, "visit": "cm31105-4", diff --git a/tests/test_data/parameter_json_files/good_test_load_centre_collect_params.json b/tests/test_data/parameter_json_files/good_test_load_centre_collect_params.json index 5f98f92e58..51c1db22b3 100644 --- a/tests/test_data/parameter_json_files/good_test_load_centre_collect_params.json +++ b/tests/test_data/parameter_json_files/good_test_load_centre_collect_params.json @@ -1,5 +1,5 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "beamline": "BL03S", "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", "insertion_prefix": "SR03S", diff --git a/tests/test_data/parameter_json_files/good_test_load_centre_collect_params_multi_rotation.json b/tests/test_data/parameter_json_files/good_test_load_centre_collect_params_multi_rotation.json index eb84f9bb94..78358fb5fa 100644 --- a/tests/test_data/parameter_json_files/good_test_load_centre_collect_params_multi_rotation.json +++ b/tests/test_data/parameter_json_files/good_test_load_centre_collect_params_multi_rotation.json @@ -1,5 +1,5 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "beamline": "BL03S", "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", "insertion_prefix": "SR03S", @@ -27,7 +27,12 @@ "demand_energy_ev": 11100, "rotation_increment_deg": 0.1, "shutter_opening_time_s": 0.6, - "snapshot_omegas_deg": [0, 90, 180, 270], + "snapshot_omegas_deg": [ + 0, + 90, + 180, + 270 + ], "run_number": 1, "rotation_scans": [ { diff --git a/tests/test_data/parameter_json_files/good_test_multi_rotation_scan_parameters.json b/tests/test_data/parameter_json_files/good_test_multi_rotation_scan_parameters.json index bb2958c3ef..03fffbd02d 100644 --- a/tests/test_data/parameter_json_files/good_test_multi_rotation_scan_parameters.json +++ b/tests/test_data/parameter_json_files/good_test_multi_rotation_scan_parameters.json @@ -1,5 +1,5 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "comment": "test", "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", "storage_directory": "{tmp_data}/123456/", diff --git a/tests/test_data/parameter_json_files/good_test_one_multi_rotation_scan_parameters.json b/tests/test_data/parameter_json_files/good_test_one_multi_rotation_scan_parameters.json index e1874e5164..bc27cd1792 100644 --- a/tests/test_data/parameter_json_files/good_test_one_multi_rotation_scan_parameters.json +++ b/tests/test_data/parameter_json_files/good_test_one_multi_rotation_scan_parameters.json @@ -1,5 +1,5 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "comment": "test", "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", "storage_directory": "{tmp_data}/123456/", diff --git a/tests/test_data/parameter_json_files/good_test_one_multi_rotation_scan_parameters_nomove.json b/tests/test_data/parameter_json_files/good_test_one_multi_rotation_scan_parameters_nomove.json index e8a2c24089..885d286763 100644 --- a/tests/test_data/parameter_json_files/good_test_one_multi_rotation_scan_parameters_nomove.json +++ b/tests/test_data/parameter_json_files/good_test_one_multi_rotation_scan_parameters_nomove.json @@ -1,5 +1,5 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "comment": "test", "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", "storage_directory": "{tmp_data}/123456/", diff --git a/tests/test_data/parameter_json_files/good_test_pin_centre_then_xray_centre_parameters.json b/tests/test_data/parameter_json_files/good_test_pin_centre_then_xray_centre_parameters.json index 4bae2ad6d8..6a9b87e5a7 100644 --- a/tests/test_data/parameter_json_files/good_test_pin_centre_then_xray_centre_parameters.json +++ b/tests/test_data/parameter_json_files/good_test_pin_centre_then_xray_centre_parameters.json @@ -1,5 +1,5 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "beamline": "BL03S", "insertion_prefix": "SR03S", "storage_directory": "/tmp", @@ -10,7 +10,10 @@ "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", "exposure_time_s": 0.1, "detector_distance_mm": 100.0, - "omega_start_deg": 0.0, + "omega_starts_deg": [ + 0.0, + 90.0 + ], "tip_offset_um": 108.9, "grid_width_um": 290.6, "transmission_frac": 1.0, diff --git a/tests/test_data/parameter_json_files/good_test_robot_load_and_centre_params.json b/tests/test_data/parameter_json_files/good_test_robot_load_and_centre_params.json index da32330450..7f5e874691 100644 --- a/tests/test_data/parameter_json_files/good_test_robot_load_and_centre_params.json +++ b/tests/test_data/parameter_json_files/good_test_robot_load_and_centre_params.json @@ -1,5 +1,5 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "beamline": "BL03S", "insertion_prefix": "SR03S", "storage_directory": "/tmp/", @@ -8,7 +8,10 @@ "run_number": 0, "use_roi_mode": false, "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", - "omega_start_deg": 0, + "omega_starts_deg": [ + 0, + 90 + ], "transmission_frac": 1.0, "exposure_time_s": 0.004, "detector_distance_mm": 255, diff --git a/tests/test_data/parameter_json_files/good_test_robot_load_params.json b/tests/test_data/parameter_json_files/good_test_robot_load_params.json index 72e111faa6..d189b97931 100644 --- a/tests/test_data/parameter_json_files/good_test_robot_load_params.json +++ b/tests/test_data/parameter_json_files/good_test_robot_load_params.json @@ -1,9 +1,9 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "beamline": "BL03S", "insertion_prefix": "SR03S", "snapshot_directory": "/tmp/", - "storage_directory":"/tmp/", + "storage_directory": "/tmp/", "visit": "cm31105-4", "demand_energy_ev": 11100, "sample_id": 12345, diff --git a/tests/test_data/parameter_json_files/good_test_rotation_scan_parameters.json b/tests/test_data/parameter_json_files/good_test_rotation_scan_parameters.json index f149b31936..0fb13b4777 100644 --- a/tests/test_data/parameter_json_files/good_test_rotation_scan_parameters.json +++ b/tests/test_data/parameter_json_files/good_test_rotation_scan_parameters.json @@ -1,5 +1,5 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "comment": "test", "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", "storage_directory": "{tmp_data}/123456/", diff --git a/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json b/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json index 8ae8d7fd1e..25bb359747 100644 --- a/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json +++ b/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json @@ -1,5 +1,5 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "demand_energy_ev": 100, "comment": "test", "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", diff --git a/tests/test_data/parameter_json_files/ispyb_gridscan_system_test_parameters.json b/tests/test_data/parameter_json_files/ispyb_gridscan_system_test_parameters.json index 490188798b..32b478a62a 100644 --- a/tests/test_data/parameter_json_files/ispyb_gridscan_system_test_parameters.json +++ b/tests/test_data/parameter_json_files/ispyb_gridscan_system_test_parameters.json @@ -1,5 +1,5 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "beamline": "BL03S", "insertion_prefix": "SR03S", "storage_directory": "{tmp_data}", diff --git a/tests/test_data/parameter_json_files/load_centre_collect_params_top_n_by_max_count.json b/tests/test_data/parameter_json_files/load_centre_collect_params_top_n_by_max_count.json index d4385e7859..a63e1793e6 100644 --- a/tests/test_data/parameter_json_files/load_centre_collect_params_top_n_by_max_count.json +++ b/tests/test_data/parameter_json_files/load_centre_collect_params_top_n_by_max_count.json @@ -1,5 +1,5 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "visit": "cm37235-4", "detector_distance_mm": 255, "sample_id": 5461074, @@ -12,7 +12,10 @@ "use_roi_mode": true, "demand_energy_ev": 11100, "transmission_frac": 1.0, - "omega_start_deg": 0, + "omega_starts_deg": [ + 0, + 90 + ], "chi_start_deg": 30, "use_panda": false }, diff --git a/tests/test_data/parameter_json_files/test_gridscan_param_defaults.json b/tests/test_data/parameter_json_files/test_gridscan_param_defaults.json index c8d5c600c9..6ffdd31edf 100644 --- a/tests/test_data/parameter_json_files/test_gridscan_param_defaults.json +++ b/tests/test_data/parameter_json_files/test_gridscan_param_defaults.json @@ -1,5 +1,5 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "beamline": "BL03S", "insertion_prefix": "SR03S", "demand_energy_ev": 100, diff --git a/tests/test_data/parameter_json_files/test_oav_snapshot_params.json b/tests/test_data/parameter_json_files/test_oav_snapshot_params.json index df9a7d19bc..78efb496ed 100644 --- a/tests/test_data/parameter_json_files/test_oav_snapshot_params.json +++ b/tests/test_data/parameter_json_files/test_oav_snapshot_params.json @@ -1,5 +1,10 @@ { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "snapshot_directory": "/tmp/my_snapshots", - "snapshot_omegas_deg": [0, 90, 180, 270] + "snapshot_omegas_deg": [ + 0, + 90, + 180, + 270 + ] } 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 98ab4ba6d2..9a8c2e1304 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 @@ -193,10 +193,14 @@ def decorated(): gridscan_params = grid_param_cb.get_grid_parameters() assert gridscan_params["x_start_um"] == pytest.approx(-804, abs=1) - assert gridscan_params["y_start_um"] == pytest.approx( - -550 - ((box_size_y_pixels / 2) * microns_per_pixel_y), abs=1 + assert ( + gridscan_params["y_starts_um"] + == [ + pytest.approx(-550 - ((box_size_y_pixels / 2) * microns_per_pixel_y), abs=1) + ] + * 2 ) - assert gridscan_params["z_start_um"] == pytest.approx(-534, abs=1) + assert gridscan_params["z_starts_um"] == [pytest.approx(-534, abs=1)] * 2 @patch( diff --git a/tests/unit_tests/hyperion/external_interaction/test_agamemnon.py b/tests/unit_tests/hyperion/external_interaction/test_agamemnon.py index 83906043c4..f866cfa35d 100644 --- a/tests/unit_tests/hyperion/external_interaction/test_agamemnon.py +++ b/tests/unit_tests/hyperion/external_interaction/test_agamemnon.py @@ -395,11 +395,11 @@ def test_create_parameters_from_agamemnon_contains_expected_robot_load_then_cent assert robot_load_params.sample_puck == 5 assert robot_load_params.sample_pin == 4 assert robot_load_params.demand_energy_ev == 12700.045934258673 - assert robot_load_params.omega_start_deg == 0.0 + assert robot_load_params.omega_starts_deg == [0.0, 90.0] assert robot_load_params.transmission_frac == 1.0 assert robot_load_params.tip_offset_um == 300.0 assert robot_load_params.grid_width_um == 600.0 - assert str(robot_load_params.parameter_model_version) == "5.3.0" + assert str(robot_load_params.parameter_model_version) == "6.0.0" assert ( robot_load_params.storage_directory == "/dls/i03/data/2025/mx34598-77/auto/CBLBA/CBLBA-x00242/xraycentring" @@ -436,7 +436,7 @@ def test_create_parameters_from_agamemnon_contains_expected_rotation_data( assert rotation_params.ispyb_experiment_type == "OSC" assert rotation_params.demand_energy_ev == 12700.045934258673 - assert str(rotation_params.parameter_model_version) == "5.3.0" + assert str(rotation_params.parameter_model_version) == "6.0.0" assert ( rotation_params.storage_directory == "/dls/i03/data/2025/mx34598-77/auto/CBLBA/CBLBA-x00242" diff --git a/tests/unit_tests/hyperion/parameters/test_parameter_model.py b/tests/unit_tests/hyperion/parameters/test_parameter_model.py index 9f951814e2..ac312c050c 100644 --- a/tests/unit_tests/hyperion/parameters/test_parameter_model.py +++ b/tests/unit_tests/hyperion/parameters/test_parameter_model.py @@ -45,16 +45,13 @@ def minimal_3d_gridscan_params(): return { "sample_id": 123, "x_start_um": 0.123, - "y_start_um": 0.777, - "z_start_um": 0.05, - "parameter_model_version": "5.0.0", + "y_starts_um": [0.777, 2], + "z_starts_um": [0.05, 2], + "parameter_model_version": "6.0.0", "visit": "cm12345", "file_name": "test_file_name", - "y2_start_um": 2, - "z2_start_um": 2, "x_steps": 5, - "y_steps": 7, - "z_steps": 9, + "y_steps": [7, 9], "storage_directory": "/tmp/dls/i03/data/2024/cm31105-4/xraycentring/123456/", } @@ -62,16 +59,12 @@ def minimal_3d_gridscan_params(): def get_empty_grid_parameters() -> GridParamUpdate: return { "x_start_um": 1, - "y_start_um": 1, - "y2_start_um": 1, - "z_start_um": 1, - "z2_start_um": 1, + "y_starts_um": [1, 1], + "z_starts_um": [1, 1], "x_steps": 1, - "y_steps": 1, - "z_steps": 1, + "y_steps": [1, 1], "x_step_size_um": 1, - "y_step_size_um": 1, - "z_step_size_um": 1, + "y_step_sizes_um": [1, 1], } @@ -103,10 +96,11 @@ def test_serialise_deserialise(minimal_3d_gridscan_params): "version, valid", [ ("4.3.0", False), - ("6.3.7", False), - ("5.0.0", True), - ("5.3.0", True), - ("5.3.7", True), + ("7.3.7", False), + ("5.0.0", False), + ("5.3.0", False), + ("5.3.7", False), + ("6.0.0", True), ], ) def test_param_version(minimal_3d_gridscan_params, version: str, valid: bool): From 40fa5c426102d624abba29ca19c87528e01a46a5 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Wed, 11 Feb 2026 13:23:16 +0000 Subject: [PATCH 04/45] wip --- src/mx_bluesky/common/parameters/gridscan.py | 6 ++-- ...od_test_specified_three_d_grid_params.json | 2 +- .../test_data/test_good_rotation_params.json | 2 +- .../test_grid_detection_plan.py | 30 +++++++++---------- .../xray_centre/test_nexus_handler.py | 22 +++++++------- .../hyperion/external_interaction/conftest.py | 4 +-- uv.lock | 4 +-- 7 files changed, 34 insertions(+), 36 deletions(-) diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 99c84ad33a..ab85a16e74 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -163,9 +163,9 @@ def __len__(self) -> int: def grid_specs(self) -> list[Product[str]]: _grid_specs = [] for idx in range(self.num_grids): - x_end = self.x_start_um + self.x_step_size_um * (self.x_step_size_um - 1) + x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1) y_end = self.y_starts_um[idx] + self.y_step_sizes_um[idx] * ( - self.y_step_sizes_um[idx] - 1 + self.y_steps[idx] - 1 ) grid_x = Line("sam_x", self.x_start_um, x_end, self.x_steps) grid_y = Line("sam_y", self.y_starts_um[idx], y_end, self.y_steps[idx]) @@ -177,7 +177,7 @@ def grid_specs(self) -> list[Product[str]]: def scan_indices(self) -> list[int]: """The first index of each gridscan, useful for writing nexus files/VDS""" _scan_indices = [0] - for idx in range(self.num_grids): + for idx in range(1, self.num_grids): _scan_indices.append( len( ScanPath(self.grid_specs[idx].calculate()) diff --git a/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json b/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json index 25bb359747..714d90f63b 100644 --- a/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json +++ b/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json @@ -16,7 +16,7 @@ "x_steps": 40, "y_steps": [ 20, - 30 + 10 ], "x_step_size_um": 100, "y_step_sizes_um": [ diff --git a/tests/unit_tests/beamlines/i24/jungfrau_commissioning/test_data/test_good_rotation_params.json b/tests/unit_tests/beamlines/i24/jungfrau_commissioning/test_data/test_good_rotation_params.json index f9a23453e2..55f1f14424 100644 --- a/tests/unit_tests/beamlines/i24/jungfrau_commissioning/test_data/test_good_rotation_params.json +++ b/tests/unit_tests/beamlines/i24/jungfrau_commissioning/test_data/test_good_rotation_params.json @@ -7,7 +7,7 @@ "sample_id": 123456, "transmission_frac": 0.1, "storage_directory": "from numtracker", - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "visit": "from numtracker", "shutter_opening_time_s": 0.5 } 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 9a8c2e1304..60f99c45bc 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 @@ -300,16 +300,15 @@ def test_when_grid_detection_plan_run_then_grid_detection_callback_gets_correct_ my_grid_params = cb.get_grid_parameters() assert my_grid_params["x_start_um"] == pytest.approx(-794.22) - assert my_grid_params["y_start_um"] == pytest.approx(-539.84 - (box_size_um / 2)) - assert my_grid_params["y2_start_um"] == pytest.approx(-539.84 - (box_size_um / 2)) - assert my_grid_params["z_start_um"] == pytest.approx(-524.04) - assert my_grid_params["z2_start_um"] == pytest.approx(-524.04) + assert ( + my_grid_params["y_starts_um"] + == [pytest.approx(-539.84 - (box_size_um / 2))] * 2 + ) + assert my_grid_params["z_starts_um"] == [pytest.approx(-524.04)] * 2 assert my_grid_params["x_step_size_um"] == box_size_um - assert my_grid_params["y_step_size_um"] == box_size_um - assert my_grid_params["z_step_size_um"] == box_size_um + assert my_grid_params["y_step_sizes_um"] == [box_size_um] * 2 assert my_grid_params["x_steps"] == pytest.approx(9) - assert my_grid_params["y_steps"] == pytest.approx(2) - assert my_grid_params["z_steps"] == pytest.approx(3) + assert my_grid_params["y_steps"] == [pytest.approx(2), pytest.approx(3)] assert cb.x_step_size_um == cb.y_step_size_um == cb.z_step_size_um == box_size_um @@ -348,16 +347,15 @@ def test_when_grid_detection_plan_run_with_different_omega_order_then_grid_detec my_grid_params = cb.get_grid_parameters() assert my_grid_params["x_start_um"] == pytest.approx(-794.22) - assert my_grid_params["y_start_um"] == pytest.approx(-539.84 - (box_size_um / 2)) - assert my_grid_params["y2_start_um"] == pytest.approx(-539.84 - (box_size_um / 2)) - assert my_grid_params["z_start_um"] == pytest.approx(-524.04) - assert my_grid_params["z2_start_um"] == pytest.approx(-524.04) + assert ( + my_grid_params["y_starts_um"] + == [pytest.approx(-539.84 - (box_size_um / 2))] * 2 + ) + assert my_grid_params["z_starts_um"] == [pytest.approx(-524.04)] * 2 assert my_grid_params["x_step_size_um"] == box_size_um - assert my_grid_params["y_step_size_um"] == box_size_um - assert my_grid_params["z_step_size_um"] == box_size_um + assert my_grid_params["y_step_sizes_um"] == [box_size_um] * 2 assert my_grid_params["x_steps"] == pytest.approx(9) - assert my_grid_params["y_steps"] == pytest.approx(2) - assert my_grid_params["z_steps"] == pytest.approx(3) + assert my_grid_params["y_steps"] == [pytest.approx(2), pytest.approx(3)] assert cb.x_step_size_um == cb.y_step_size_um == cb.z_step_size_um == box_size_um 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 5af8db32c2..57a9d63955 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 @@ -45,8 +45,8 @@ def test_writers_dont_create_on_init_but_do_on_during_collection_read_event( param_type=HyperionSpecifiedThreeDGridScan ) - assert nexus_handler.nexus_writer_1 is None - assert nexus_handler.nexus_writer_2 is None + assert nexus_handler._writers[0] is None + assert nexus_handler._writers[1] is None nexus_handler.activity_gated_start( test_event_data.test_gridscan_outer_start_document @@ -59,10 +59,10 @@ def test_writers_dont_create_on_init_but_do_on_during_collection_read_event( test_event_data.test_event_document_during_data_collection ) - assert nexus_handler.nexus_writer_1 is not None - assert nexus_handler.nexus_writer_2 is not None - nexus_handler.nexus_writer_1.create_nexus_file.assert_called_once() - nexus_handler.nexus_writer_2.create_nexus_file.assert_called_once() + assert nexus_handler._writers[0] is not None + assert nexus_handler._writers[1] is not None + nexus_handler._writers[0].create_nexus_file.assert_called_once() + nexus_handler._writers[1].create_nexus_file.assert_called_once() @pytest.mark.parametrize( @@ -98,12 +98,12 @@ def test_given_different_bit_depths_then_writers_created_wth_correct_virtual_dat nexus_handler.activity_gated_event(event_doc) - assert nexus_handler.nexus_writer_1 is not None - assert nexus_handler.nexus_writer_2 is not None - nexus_handler.nexus_writer_1.create_nexus_file.assert_called_once_with( # type:ignore + assert nexus_handler._writers[0] is not None + assert nexus_handler._writers[1] is not None + nexus_handler._writers[0].create_nexus_file.assert_called_once_with( # type:ignore vds_type ) - nexus_handler.nexus_writer_2.create_nexus_file.assert_called_once_with( # type:ignore + nexus_handler._writers[1].create_nexus_file.assert_called_once_with( # type:ignore vds_type ) @@ -130,7 +130,7 @@ def test_beam_and_attenuator_set_on_ispyb_transmission_event( test_event_data.test_event_document_during_data_collection ) - for writer in [nexus_handler.nexus_writer_1, nexus_handler.nexus_writer_2]: + for writer in [nexus_handler._writers[0], nexus_handler._writers[1]]: assert writer is not None assert writer.attenuator is not None assert writer.beam is not None diff --git a/tests/unit_tests/hyperion/external_interaction/conftest.py b/tests/unit_tests/hyperion/external_interaction/conftest.py index c78ea94779..68d4961963 100644 --- a/tests/unit_tests/hyperion/external_interaction/conftest.py +++ b/tests/unit_tests/hyperion/external_interaction/conftest.py @@ -26,13 +26,13 @@ def test_rotation_params(tmp_path): param_dict["scan_width_deg"] = 360.0 params = SingleRotationScan(**param_dict) params.x_start_um = 0 - params.y_starts_um = [0, 0] + params.y_start_um = 0 params.exposure_time_s = 0.004 return params @pytest.fixture(params=[1050]) -def test_fgs_params(request, tmp_path): +def test_three_d_grid_params(request, tmp_path): assert request.param % 25 == 0, "Please use a multiple of 25 images" params = HyperionSpecifiedThreeDGridScan(**default_raw_gridscan_params(tmp_path)) params.demand_energy_ev = convert_angstrom_to_ev(1.0) diff --git a/uv.lock b/uv.lock index df626f8fd9..601f590d20 100644 --- a/uv.lock +++ b/uv.lock @@ -802,8 +802,8 @@ wheels = [ [[package]] name = "dls-dodal" -version = "2.0.1.dev12+gfa22fd0ab" -source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#fa22fd0ab55aa63445da1d6462ec02c82cd64611" } +version = "2.0.1.dev15+gee97fc144" +source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#ee97fc1440fc588efe2ccb68885626406d4014fb" } dependencies = [ { name = "aiofiles", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "aiohttp", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, From 0fa10d9fdf092cfafc7bf1b71290ecf214aa3294 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Thu, 12 Feb 2026 18:15:15 +0000 Subject: [PATCH 05/45] Some fixes --- .../inner_plans/xrc_results_utils.py | 16 +++++++- .../example_load_centre_collect_params.json | 5 ++- .../xray_centre/test_nexus_handler.py | 7 ++-- .../hyperion/experiment_plans/conftest.py | 13 +++---- .../test_pin_centre_then_xray_centre_plan.py | 37 +------------------ 5 files changed, 29 insertions(+), 49 deletions(-) diff --git a/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py b/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py index e0d3d205f4..90af68b501 100644 --- a/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py +++ b/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py @@ -70,7 +70,21 @@ def fetch_xrc_results_from_zocalo( def _generate_dummy_xrc_result(params: SpecifiedThreeDGridScan) -> XRayCentreResult: - com = [params.x_steps / 2, params.y_steps / 2, params.z_steps / 2] + com = [] + assert params.num_grids % 2 == 0, ( + "XRC results in commissioning mode currently only works for an even number of grids" + ) + # todo get this working for odd number of grids - here we probably want XRC results to use z as z_start? + for grid in range(int(params.num_grids / 2)): + com.append( + [ + params.x_steps / 2, + params.y_steps[2 * grid] / 2, + params.y_steps[2 * grid + 1] / 2, + ] + ) + com = [sum(x) / len(x) for x in zip(*com, strict=True)] # Get average + max_voxel = [round(p) for p in com] return _xrc_result_in_boxes_to_result_in_mm( XrcResult( diff --git a/tests/test_data/parameter_json_files/example_load_centre_collect_params.json b/tests/test_data/parameter_json_files/example_load_centre_collect_params.json index 1f98c76aea..0625e0000e 100644 --- a/tests/test_data/parameter_json_files/example_load_centre_collect_params.json +++ b/tests/test_data/parameter_json_files/example_load_centre_collect_params.json @@ -12,7 +12,10 @@ "use_roi_mode": true, "demand_energy_ev": 11100, "transmission_frac": 1.0, - "omega_start_deg": 0, + "omega_starts_deg": [ + 0, + 90 + ], "chi_start_deg": 30 }, "multi_rotation_scan": { 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 57a9d63955..c5f970bb05 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 @@ -45,8 +45,7 @@ def test_writers_dont_create_on_init_but_do_on_during_collection_read_event( param_type=HyperionSpecifiedThreeDGridScan ) - assert nexus_handler._writers[0] is None - assert nexus_handler._writers[1] is None + assert not nexus_handler._writers nexus_handler.activity_gated_start( test_event_data.test_gridscan_outer_start_document @@ -61,8 +60,8 @@ def test_writers_dont_create_on_init_but_do_on_during_collection_read_event( assert nexus_handler._writers[0] is not None assert nexus_handler._writers[1] is not None - nexus_handler._writers[0].create_nexus_file.assert_called_once() - nexus_handler._writers[1].create_nexus_file.assert_called_once() + nexus_handler._writers[0].create_nexus_file.assert_called_once() # type: ignore + nexus_handler._writers[1].create_nexus_file.assert_called_once() # type: ignore @pytest.mark.parametrize( diff --git a/tests/unit_tests/hyperion/experiment_plans/conftest.py b/tests/unit_tests/hyperion/experiment_plans/conftest.py index db7c63398d..b4d98e96af 100644 --- a/tests/unit_tests/hyperion/experiment_plans/conftest.py +++ b/tests/unit_tests/hyperion/experiment_plans/conftest.py @@ -278,16 +278,13 @@ def grid_detection_callback_with_detected_grid(): "transmission_frac": 1.0, "exposure_time_s": 0, "x_start_um": 0, - "y_start_um": 0, - "y2_start_um": 0, - "z_start_um": 0, - "z2_start_um": 0, + "y_starts_um": [0, 0], + "z_starts_um": [0, 0], "x_steps": 10, - "y_steps": 10, - "z_steps": 10, + "y_steps": [10, 10], "x_step_size_um": 0.1, - "y_step_size_um": 0.1, - "z_step_size_um": 0.1, + "y_step_sizes_um": [0.1, 0.1], + "omega_starts_deg": [0, 90], } yield callback 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 ba41d3bc56..bec6fc82dd 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 @@ -39,25 +39,6 @@ from .conftest import FLYSCAN_RESULT_LOW, FLYSCAN_RESULT_MED, sim_fire_event_on_open_run -@pytest.fixture -def test_grid_params(): - return { - "transmission_frac": 1.0, - "exposure_time_s": 0, - "x_start_um": 0, - "y_start_um": 0, - "y2_start_um": 0, - "z_start_um": 0, - "z2_start_um": 0, - "x_steps": 10, - "y_steps": 10, - "z_steps": 10, - "x_step_size_um": 0.1, - "y_step_size_um": 0.1, - "z_step_size_um": 0.1, - } - - @pytest.fixture def test_pin_centre_then_xray_centre_params( tmp_path, @@ -172,9 +153,6 @@ def test_when_pin_centre_xray_centre_called_then_plan_runs_correctly( mock_pin_tip_centre.assert_called_once() -@patch( - "mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan.GridDetectionCallback", -) @patch( "mx_bluesky.hyperion.experiment_plans.pin_centre_then_xray_centre_plan.pin_tip_centre_plan", autospec=True, @@ -186,15 +164,12 @@ def test_when_pin_centre_xray_centre_called_then_plan_runs_correctly( def test_when_pin_centre_xray_centre_called_then_detector_positioned( mock_grid_detect: MagicMock, mock_pin_tip_centre: MagicMock, - mock_grid_callback: MagicMock, - test_grid_params: MagicMock, + grid_detection_callback_with_detected_grid, test_pin_centre_then_xray_centre_params: PinTipCentreThenXrayCentre, hyperion_grid_detect_xrc_devices: HyperionGridDetectThenXRayCentreComposite, test_config_files, sim_run_engine: RunEngineSimulator, ): - mock_grid_callback.return_value.get_grid_parameters.return_value = test_grid_params - sim_run_engine.add_handler_for_callback_subscribes() sim_run_engine.add_handler( @@ -439,9 +414,6 @@ def test_pin_tip_centre_then_xray_centre_moves_beamstop_into_place( @pytest.mark.parametrize("transmission_frac", [1, 0.5, 0.25]) -@patch( - "mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan.GridDetectionCallback", -) @patch( "mx_bluesky.hyperion.experiment_plans.pin_centre_then_xray_centre_plan.pin_tip_centre_plan" ) @@ -459,17 +431,12 @@ def test_pin_tip_centre_then_xray_centre_sets_transmission_fraction_and_xbpm_is_ mock_run_gridscan: MagicMock, mock_grid_detection_plan: MagicMock, mock_pin_tip_centre_plan: MagicMock, - mock_grid_detection_callback: MagicMock, - test_grid_params, + grid_detection_callback_with_detected_grid, transmission_frac: float, sim_run_engine: RunEngineSimulator, hyperion_grid_detect_xrc_devices: HyperionGridDetectThenXRayCentreComposite, test_pin_centre_then_xray_centre_params: PinTipCentreThenXrayCentre, ): - mock_grid_detection_callback.return_value.get_grid_parameters.return_value = ( - test_grid_params - ) - test_pin_centre_then_xray_centre_params.transmission_frac = transmission_frac msgs = sim_run_engine.simulate_plan( From 757ac751b90cd16c9b6b528b97aba477a0fc6ea0 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Thu, 12 Feb 2026 18:15:33 +0000 Subject: [PATCH 06/45] Partially fix nexus tests --- .../callbacks/xray_centre/nexus_callback.py | 43 +++++++++++-------- .../nexus/test_write_nexus.py | 40 ++++------------- 2 files changed, 32 insertions(+), 51 deletions(-) diff --git a/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py index 1d5b0dd9ef..7d6eeacd2e 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py @@ -22,6 +22,28 @@ T = TypeVar("T", bound="SpecifiedGrids") +def _create_writers_from_params(params: SpecifiedGrids) -> list[NexusWriter]: + num_writers = params.num_grids + writers = [] + d_size = params.detector_params.detector_size_constants.det_size_pixels + for idx in range(num_writers): + images_in_grid = len(params.scan_points[idx]["sam_x"]) + data_shape = (images_in_grid, d_size.width, d_size.height) + run_number = params.detector_params.run_number + idx + + writers.append( + NexusWriter( + params, + data_shape, + params.scan_points[idx], + run_number=run_number, + vds_start_index=params.scan_indices[idx], + omega_start_deg=params.omega_starts_deg[idx], + ) + ) + return writers + + class GridscanNexusFileCallback(PlanReactiveCallback): """Callback class to handle the creation of Nexus files based on experiment \ parameters. Initialises on receiving a 'start' document for the \ @@ -55,24 +77,7 @@ def activity_gated_start(self, doc: RunStart): f"Nexus writer received start document with experiment parameters {mx_bluesky_parameters}" ) parameters = self.param_type.model_validate_json(mx_bluesky_parameters) - num_writers = parameters.num_grids - - d_size = parameters.detector_params.detector_size_constants.det_size_pixels - for idx in range(0, num_writers - 1): - images_in_grid = parameters.scan_indices[idx + 1] - data_shape = (images_in_grid, d_size.width, d_size.height) - run_number = parameters.detector_params.run_number + idx - - self._writers.append( - NexusWriter( - parameters, - data_shape, - parameters.scan_points[idx], - run_number=run_number, - vds_start_index=parameters.scan_indices[idx], - omega_start_deg=parameters.omega_starts_deg[idx], - ) - ) + self._writers = _create_writers_from_params(parameters) self.run_start_uid = doc.get("uid") @@ -84,8 +89,8 @@ def activity_gated_event(self, doc: Event) -> Event | None: assert event_descriptor is not None if event_descriptor.get("name") == DocDescriptorNames.HARDWARE_READ_DURING: data = doc["data"] + assert self._writers, "Nexus callback did not receive start doc" for nexus_writer in self._writers: - assert nexus_writer, "Nexus callback did not receive start doc" ( nexus_writer.beam, nexus_writer.attenuator, 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 961991714f..04c98bdc61 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,6 +16,9 @@ ZebraGridScanParamsThreeD, ) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( + _create_writers_from_params, +) from mx_bluesky.common.external_interaction.nexus.nexus_utils import ( AxisDirection, create_beam_and_attenuator_parameters, @@ -38,41 +41,14 @@ def assert_end_data_correct(nexus_writer: NexusWriter): assert "end_time_estimated" in entry -def create_nexus_writer(parameters: HyperionSpecifiedThreeDGridScan, writer_num): - d_size = parameters.detector_params.detector_size_constants.det_size_pixels - n_img = ( - parameters.scan_indices[1] - if writer_num == 1 - else parameters.num_images - parameters.scan_indices[1] - ) - points = parameters.scan_points[0] if writer_num == 1 else parameters.scan_points[1] - data_shape = (n_img, d_size.width, d_size.height) - run_number = parameters.detector_params.run_number + writer_num - 1 - vds_start = 0 if writer_num == 1 else parameters.scan_indices[1] - omega_start = ( - parameters.omega_starts_deg[0] - if writer_num == 1 - else parameters.omega_starts_deg[1] - ) - nexus_writer = NexusWriter( - parameters, - data_shape, - scan_points=points, - run_number=run_number, - vds_start_index=vds_start, - omega_start_deg=omega_start, - ) - nexus_writer.beam, nexus_writer.attenuator = create_beam_and_attenuator_parameters( - 20, TEST_FLUX, 0.5 - ) - return nexus_writer - - @contextmanager def create_nexus_writers(parameters: HyperionSpecifiedThreeDGridScan): - writers = [create_nexus_writer(parameters, i) for i in [1, 2]] - writers[1].start_index = parameters.scan_indices[1] + writers = _create_writers_from_params(parameters) try: + for writer in writers: + writer.beam, writer.attenuator = create_beam_and_attenuator_parameters( + 20, TEST_FLUX, 0.5 + ) yield writers finally: for writer in writers: From 145a648260378d9aa75db3cc381afaf33d8eb354 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Mon, 16 Feb 2026 12:12:15 +0000 Subject: [PATCH 07/45] Fix nexus tests --- src/mx_bluesky/common/parameters/gridscan.py | 2 +- .../external_interaction/nexus/test_write_nexus.py | 7 ++++--- .../hyperion/parameters/test_parameter_model.py | 8 ++++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index ab85a16e74..16fd8c10ca 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -177,7 +177,7 @@ def grid_specs(self) -> list[Product[str]]: def scan_indices(self) -> list[int]: """The first index of each gridscan, useful for writing nexus files/VDS""" _scan_indices = [0] - for idx in range(1, self.num_grids): + for idx in range(self.num_grids - 1): _scan_indices.append( len( ScanPath(self.grid_specs[idx].calculate()) 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 04c98bdc61..694f248fc3 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 @@ -187,6 +187,7 @@ def test_given_dummy_data_then_datafile_written_correctly( nexus_writer_2.create_nexus_file(np.uint16) + # Nexus writer 2 has data for a rotated grid for filename in [nexus_writer_2.nexus_file, nexus_writer_2.master_file]: with h5py.File(filename, "r") as written_nexus_file: assert isinstance( @@ -195,12 +196,12 @@ def test_given_dummy_data_then_datafile_written_correctly( assert_x_data_stride_correct( data_path, grid_scan_params, grid_scan_params.z_steps ) - assert isinstance(sam_z := data_path["sam_z"], h5py.Dataset) + assert isinstance(sam_y := data_path["sam_y"], h5py.Dataset) assert_varying_axis_stride_correct( - sam_z[:], grid_scan_params, grid_scan_params.z_axis + sam_y[:], grid_scan_params, grid_scan_params.z_axis ) assert_axis_data_fixed( - written_nexus_file, "y", grid_scan_params.y2_start_mm + written_nexus_file, "z", grid_scan_params.y2_start_mm ) assert isinstance( flux := written_nexus_file["/entry/instrument/beam/total_flux"], diff --git a/tests/unit_tests/hyperion/parameters/test_parameter_model.py b/tests/unit_tests/hyperion/parameters/test_parameter_model.py index ac312c050c..c69427429e 100644 --- a/tests/unit_tests/hyperion/parameters/test_parameter_model.py +++ b/tests/unit_tests/hyperion/parameters/test_parameter_model.py @@ -70,7 +70,11 @@ def get_empty_grid_parameters() -> GridParamUpdate: def test_minimal_3d_gridscan_params(minimal_3d_gridscan_params): test_params = HyperionSpecifiedThreeDGridScan(**minimal_3d_gridscan_params) - assert {"sam_x", "sam_y", "sam_z"} == set(test_params.scan_points.keys()) + assert all( + {"sam_x", "sam_y", "sam_z"} == set(scan_point.keys()) + for scan_point in test_params.scan_points + ) + assert test_params.scan_indices == [0, 35] assert test_params.num_images == (5 * 7 + 5 * 9) assert test_params.exposure_time_s == GridscanParamConstants.EXPOSURE_TIME_S @@ -114,7 +118,7 @@ def test_param_version(minimal_3d_gridscan_params, version: str, valid: bool): def test_robot_load_then_centre_params(): params = { - "parameter_model_version": "5.0.0", + "parameter_model_version": "6.0.0", "sample_id": 123456, "visit": "cm12345", "file_name": "file_name", From e63677e710c4cacba851a8c384b43df3db634d3b Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Mon, 16 Feb 2026 12:54:28 +0000 Subject: [PATCH 08/45] fix test --- .../unit_tests/hyperion/external_interaction/test_agamemnon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/hyperion/external_interaction/test_agamemnon.py b/tests/unit_tests/hyperion/external_interaction/test_agamemnon.py index f866cfa35d..2680553583 100644 --- a/tests/unit_tests/hyperion/external_interaction/test_agamemnon.py +++ b/tests/unit_tests/hyperion/external_interaction/test_agamemnon.py @@ -366,7 +366,7 @@ def test_create_parameters_from_agamemnon_contains_expected_data(agamemnon_respo assert hyperion_params.sample_id == 6501159 assert hyperion_params.sample_puck == 5 assert hyperion_params.sample_pin == 4 - assert str(hyperion_params.parameter_model_version) == "5.3.0" + assert str(hyperion_params.parameter_model_version) == "6.0.0" assert hyperion_params.select_centres.n == 1 From c2a232d8370be46cab655e6ed050877eb483a7e8 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Mon, 16 Feb 2026 13:22:24 +0000 Subject: [PATCH 09/45] fix typing --- .../experiment_plans/inner_plans/xrc_results_utils.py | 6 +++--- .../callbacks/rotation/nexus_callback.py | 4 ++-- .../external_interaction/test_ispyb_dev_connection.py | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py b/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py index 90af68b501..b994801c8c 100644 --- a/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py +++ b/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py @@ -70,20 +70,20 @@ def fetch_xrc_results_from_zocalo( def _generate_dummy_xrc_result(params: SpecifiedThreeDGridScan) -> XRayCentreResult: - com = [] + coms = [] assert params.num_grids % 2 == 0, ( "XRC results in commissioning mode currently only works for an even number of grids" ) # todo get this working for odd number of grids - here we probably want XRC results to use z as z_start? for grid in range(int(params.num_grids / 2)): - com.append( + coms.append( [ params.x_steps / 2, params.y_steps[2 * grid] / 2, params.y_steps[2 * grid + 1] / 2, ] ) - com = [sum(x) / len(x) for x in zip(*com, strict=True)] # Get average + com = [sum(x) / len(x) for x in zip(*coms, strict=True)] # Get average max_voxel = [round(p) for p in com] return _xrc_result_in_boxes_to_result_in_mm( diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py index 60437c0e16..de7cf2e3a8 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py @@ -97,8 +97,8 @@ def activity_gated_start(self, doc: RunStart): shape, parameters.scan_points, omega_start_deg=parameters.omega_start_deg, - chi_start_deg=parameters.chi_start_deg, - phi_start_deg=parameters.phi_start_deg, + chi_start_deg=parameters.chi_start_deg or 0, + phi_start_deg=parameters.phi_start_deg or 0, vds_start_index=parameters.nexus_vds_start_img, full_num_of_images=self.full_num_of_images, meta_data_run_number=self.meta_data_run_number, 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 c774e08de8..d4d73450b3 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 @@ -168,9 +168,9 @@ def scan_xy_data_info_for_update( assert dummy_params is not None scan_data_info_for_update.data_collection_grid_info = DataCollectionGridInfo( dx_in_mm=dummy_params.x_step_size_um, - dy_in_mm=dummy_params.y_step_size_um, + dy_in_mm=dummy_params.y_step_sizes_um[0], steps_x=dummy_params.x_steps, - steps_y=dummy_params.y_steps, + steps_y=dummy_params.y_steps[0], microns_per_pixel_x=1.25, microns_per_pixel_y=1.25, # cast coordinates from numpy int64 to avoid mysql type conversion issues @@ -201,9 +201,9 @@ def scan_data_infos_for_update_3d( assert dummy_params is not None data_collection_grid_info = DataCollectionGridInfo( dx_in_mm=dummy_params.x_step_size_um, - dy_in_mm=dummy_params.z_step_size_um, + dy_in_mm=dummy_params.y_step_sizes_um[1], steps_x=dummy_params.x_steps, - steps_y=dummy_params.z_steps, + steps_y=dummy_params.y_steps[1], microns_per_pixel_x=1.25, microns_per_pixel_y=1.25, # cast coordinates from numpy int64 to avoid mysql type conversion issues From 860562b2e8c35f2446198bf4b8c7ba96068a7362 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Mon, 16 Feb 2026 15:46:39 +0000 Subject: [PATCH 10/45] Address some todos and add validator --- .../experiment_plans/inner_plans/do_fgs.py | 2 +- .../inner_plans/xrc_results_utils.py | 33 ++++++++++++------- .../common/parameters/components.py | 4 +-- src/mx_bluesky/common/parameters/gridscan.py | 29 +++++++++++++--- 4 files changed, 49 insertions(+), 19 deletions(-) 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 058f761e5a..d7e973a926 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 @@ -93,7 +93,7 @@ def kickoff_and_complete_gridscan( "omega_to_scan_spec": { # These have to be cast to strings due to a bug in orsjon. See # https://github.com/ijl/orjson/issues/414 - # TODO this errors if it's a float due to bluesky stuff, do we need the omega angle to be an int? + # See https://github.com/DiamondLightSource/mx-bluesky/issues/1631 regarding integer cast str(int(omega_starts_deg[i])): scan_points[i] for i in range(len(omega_starts_deg)) }, diff --git a/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py b/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py index b994801c8c..b95f75d48c 100644 --- a/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py +++ b/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py @@ -71,18 +71,29 @@ def fetch_xrc_results_from_zocalo( def _generate_dummy_xrc_result(params: SpecifiedThreeDGridScan) -> XRayCentreResult: coms = [] - assert params.num_grids % 2 == 0, ( + if params.num_grids % 2 == 0: "XRC results in commissioning mode currently only works for an even number of grids" - ) - # todo get this working for odd number of grids - here we probably want XRC results to use z as z_start? - for grid in range(int(params.num_grids / 2)): - coms.append( - [ - params.x_steps / 2, - params.y_steps[2 * grid] / 2, - params.y_steps[2 * grid + 1] / 2, - ] - ) + + for grid in range(int(params.num_grids / 2)): + # For even number of grids, Z steps are actually the even indexed y steps + coms.append( + [ + params.x_steps / 2, + params.y_steps[2 * grid] / 2, + params.y_steps[2 * grid + 1] / 2, + ] + ) + else: + # For odd number of grids, Z is fixed at the z start position + for grid in range(params.num_grids): + coms.append( + [ + params.x_steps / 2, + params.y_steps[grid] / 2, + params.z_starts_um[grid], + ] + ) + com = [sum(x) / len(x) for x in zip(*coms, strict=True)] # Get average max_voxel = [round(p) for p in com] diff --git a/src/mx_bluesky/common/parameters/components.py b/src/mx_bluesky/common/parameters/components.py index 093f141eca..150e466185 100644 --- a/src/mx_bluesky/common/parameters/components.py +++ b/src/mx_bluesky/common/parameters/components.py @@ -277,9 +277,7 @@ def selection_params(self) -> MultiXtalSelection: class OptionalXyzStarts(BaseModel): - x_start_um: float = ( - 0 # todo link to issue about X params needing to be the same for each grid - ) + x_start_um: float = 0 # See https://github.com/DiamondLightSource/mx-bluesky/issues/1632 for this not being a list y_starts_um: list[float | None] | None = None z_starts_um: list[float | None] | None = None diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 16fd8c10ca..2898dd2d7d 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -133,17 +133,38 @@ class SpecifiedGrids(GenericGrid, XyzStarts, WithScan, Generic[GridScanParamType ) x_step_size_um: PositiveFloat = Field( default=GridscanParamConstants.BOX_WIDTH_UM - ) # Think this needs to be the same for each grid too + ) # See https://github.com/DiamondLightSource/mx-bluesky/issues/1632 for this not being a list - # In a 3D grid scan, n_steps[0] and n_steps[1] refers to Y and Z respectively. + # In a 3D grid scan, y_steps[0] and y_steps[1] refers to Y and Z respectively. # We do an omega rotation between scanning across N dimensions to make N different axes y_step_sizes_um: list[PositiveFloat] = Field( default=[GridscanParamConstants.BOX_WIDTH_UM] * 2 ) - x_steps: PositiveInt # Currently this must be the same for each grid for panda scan + x_steps: PositiveInt # See https://github.com/DiamondLightSource/mx-bluesky/issues/1632 for this not being a list y_steps: list[PositiveInt] _set_stub_offsets: bool = PrivateAttr(default_factory=lambda: False) - # TODO validate that all the "per grid" things are the same length. _num_grids property can just print out length of this + + @model_validator(mode="after") + def _check_lengths_are_same(self): + fields = { + "omega_starts_deg": self.omega_starts_deg, + "y_step_sizes_um": self.y_step_sizes_um, + "y_steps": self.y_steps, + "y_starts_um": self.y_starts_um, + "z_starts_um": self.z_starts_um, + } + + lengths = {name: len(value) for name, value in fields.items()} + + if len(lengths) != len(set(lengths)): + details = "\n".join( + f" {name}: length={len(value)}, value={value}" + for name, value in fields.items() + ) + + raise ValueError("Fields must all have the same length:\n" + details) + + return self @property @abstractmethod From 7c037bb91a308afcbe6e4a3d307d35ce9f0f2ecc Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Wed, 18 Feb 2026 11:59:28 +0000 Subject: [PATCH 11/45] Fixes from merge --- src/mx_bluesky/common/parameters/constants.py | 4 +- src/mx_bluesky/common/parameters/gridscan.py | 16 ++--- .../common/parameters/test_gridscan.py | 71 +++++++++++++++++++ .../test_pin_centre_then_xray_centre_plan.py | 8 +-- 4 files changed, 79 insertions(+), 20 deletions(-) create mode 100644 tests/unit_tests/common/parameters/test_gridscan.py diff --git a/src/mx_bluesky/common/parameters/constants.py b/src/mx_bluesky/common/parameters/constants.py index e89ec7c564..d4855219cb 100644 --- a/src/mx_bluesky/common/parameters/constants.py +++ b/src/mx_bluesky/common/parameters/constants.py @@ -105,8 +105,8 @@ class GridscanParamConstants: EXPOSURE_TIME_S = 0.004 USE_ROI = True BOX_WIDTH_UM = 20.0 - OMEGA_1 = 0.0 - OMEGA_2 = 90.0 + OMEGA_1 = 0 + OMEGA_2 = 90 PANDA_RUN_UP_DISTANCE_MM = 0.2 ZOCALO_MIN_TOTAL_COUNT_THRESHOLD = 3 diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 2898dd2d7d..29045914da 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -102,11 +102,7 @@ def detector_params(self): directory=self.storage_directory, prefix=self.file_name, detector_distance=self.detector_distance_mm, - omega_start=0 - if not self.omega_starts_deg - else self.omega_starts_deg[ - 0 - ], # This value is probably a lie after this PR... Could it be stored somewhere else? Should be an experiment param, not detector + omega_start=0, # Metadata we set on detector isn't currently accurate, but also not used downstream omega_increment=0, num_images_per_trigger=1, num_triggers=self.num_images, @@ -117,9 +113,7 @@ def detector_params(self): ) -PositiveInt = Annotated[ - int, Field(gt=0) -] # todo test this actually validates as expected +PositiveInt = Annotated[int, Field(gt=0)] PositiveFloat = Annotated[float, Field(gt=0)] @@ -154,9 +148,9 @@ def _check_lengths_are_same(self): "z_starts_um": self.z_starts_um, } - lengths = {name: len(value) for name, value in fields.items()} - - if len(lengths) != len(set(lengths)): + name_and_length = {name: len(value) for name, value in fields.items()} + lengths = name_and_length.values() + if len(set(lengths)) != 1: details = "\n".join( f" {name}: length={len(value)}, value={value}" for name, value in fields.items() diff --git a/tests/unit_tests/common/parameters/test_gridscan.py b/tests/unit_tests/common/parameters/test_gridscan.py new file mode 100644 index 0000000000..4eac3661c7 --- /dev/null +++ b/tests/unit_tests/common/parameters/test_gridscan.py @@ -0,0 +1,71 @@ +import pytest +from pydantic import ValidationError + +from mx_bluesky.common.parameters.components import get_param_version +from mx_bluesky.common.parameters.gridscan import SpecifiedGrids + + +class GridParamsTest(SpecifiedGrids): + def fast_gridscan_params(): ... # type: ignore + + +@pytest.mark.parametrize( + "y_starts_um, z_starts_um, omega_starts_deg, y_step_sizes_um, y_steps, should_raise", + [ + ([1, 1], [1], [1], [1], [1, 1], True), + ( + [ + 1, + ], + [1], + [1], + [1, 1, 2, 3], + [1], + True, + ), + ([1, 1], [1, 1], [1, 1], [1, 1], [1], True), + ([1, 1, 1, 1], [1, 1, 1], [1], [1], [1, 1], True), + ([1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], False), + ], +) +def test_specified_grids_validation_error( + y_starts_um: list[float], + z_starts_um: list[float], + omega_starts_deg: list[float], + y_step_sizes_um: list[float], + y_steps: list[int], + should_raise: bool, +): + if should_raise: + with pytest.raises( + ValidationError, match="Fields must all have the same length:" + ): + GridParamsTest( + x_start_um=0, + y_starts_um=y_starts_um, + z_starts_um=z_starts_um, + omega_starts_deg=omega_starts_deg, + y_step_sizes_um=y_step_sizes_um, + y_steps=y_steps, + sample_id=0, + visit="/tmp", + parameter_model_version=get_param_version(), + file_name="/tmp", + storage_directory="/tmp", + x_steps=5, + ) + else: + GridParamsTest( + x_start_um=0, + y_starts_um=y_starts_um, + z_starts_um=z_starts_um, + omega_starts_deg=omega_starts_deg, + y_step_sizes_um=y_step_sizes_um, + y_steps=y_steps, + sample_id=0, + visit="/tmp", + parameter_model_version=get_param_version(), + file_name="/tmp", + storage_directory="/tmp", + x_steps=5, + ) 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 6dc240dbed..cc8d6ea210 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 @@ -105,9 +105,6 @@ def test_when_pin_centre_xray_centre_called_then_plan_runs_correctly( mock_pin_tip_centre.assert_called_once() -@patch( - "mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan.GridDetectionCallback", -) @patch( "mx_bluesky.hyperion.experiment_plans.pin_centre_then_gridscan_plan.pin_tip_centre_plan", autospec=True, @@ -369,9 +366,6 @@ def test_pin_tip_centre_then_xray_centre_moves_beamstop_into_place( @pytest.mark.parametrize("transmission_frac", [1, 0.5, 0.25]) -@patch( - "mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan.GridDetectionCallback", -) @patch( "mx_bluesky.hyperion.experiment_plans.pin_centre_then_gridscan_plan.pin_tip_centre_plan" ) @@ -507,4 +501,4 @@ def test_detect_grid_and_do_gridscan_gives_params_specified_grid( ) ) mock_fetch_xrc_results.assert_called_once() - assert mock_fetch_xrc_results.call_args[0][1] == test_three_d_grid_params \ No newline at end of file + assert mock_fetch_xrc_results.call_args[0][1] == test_three_d_grid_params From bf9449f081dc56185f12272aef09df1162829989 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Wed, 18 Feb 2026 12:14:44 +0000 Subject: [PATCH 12/45] Add test --- src/mx_bluesky/common/parameters/gridscan.py | 19 ++-------- .../load_centre_collect_full_plan.py | 1 - .../hyperion/parameters/gridscan.py | 6 +-- .../common/parameters/test_gridscan.py | 38 ++++++++++++++++++- 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 29045914da..5ab8c86670 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -169,7 +169,7 @@ def do_set_stub_offsets(self, value: bool): @property def num_grids(self): - return len(self.y_steps) # TODO this should probably just be an input + return len(self.y_steps) def __len__(self) -> int: return self.num_grids @@ -302,24 +302,13 @@ class SpecifiedThreeDGridScan( """Parameters representing a so-called 3D grid scan, which consists of doing a gridscan in X and Y, followed by one in X and Z.""" - # TODO: specified grids just show X and Y. Here we should be using z_steps = specifiedgrid.y_steps[1] - # sort this out when dealing with external/internal params - - # TODO validate that grid is length 2 on creation here - - # For 3D scans, number of Z steps and Z step size is the same as y_steps[1] and - # y_step_sizes_um[1]. It can be helpful to think of it as both the Z axis and as - # the second 2D scan, so we put some validation logic to allow you to use either name. - # Maybe should just refer to one name instead of having this validation logic? - - # TODO make a better validator about all the lists being length 2 - @model_validator(mode="after") def validate_y_and_z_axes(self): + _err_str = "must be length 2 for 3D scans" if len(self.y_steps) != 2: - raise ValueError(f"{self.y_steps=} must be length 2 for 3D scans") + raise ValueError(f"{self.y_steps=} {_err_str}") if len(self.y_step_sizes_um) != 2: - raise ValueError(f"{self.y_step_sizes_um=} must be length 2 for 3D scans") + raise ValueError(f"{self.y_step_sizes_um=} {_err_str}") return self @property diff --git a/src/mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py b/src/mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py index 2c7ca6c7de..00b52a9f5e 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py @@ -177,7 +177,6 @@ def _x_coordinate(sample_and_location: tuple[int, np.ndarray]) -> float: return sample_and_location[1][0] # type: ignore -# todo rotation scan params shouldnt be tied to our grid scan params def rotation_scan_generator( is_alternating: bool, ) -> Generator[ diff --git a/src/mx_bluesky/hyperion/parameters/gridscan.py b/src/mx_bluesky/hyperion/parameters/gridscan.py index 31f20e02fa..a5f26716dd 100644 --- a/src/mx_bluesky/hyperion/parameters/gridscan.py +++ b/src/mx_bluesky/hyperion/parameters/gridscan.py @@ -45,9 +45,9 @@ def detector_params(self): # Relative to common grid scan, stub offsets are defined by config server @property def fast_gridscan_params(self) -> ZebraGridScanParamsThreeD: - """Currently, x steps must be the same for each grid. Validate this somewhere? - Same for x and y step sizes. TODO Better is probably to not make it a list at all... - + """During 3D grid scans, there is an omega rotation before the second grid, + transforming Y -> Z axes, so use the second element of the Y params to set + Z params on the 3D grid scan device. """ return ZebraGridScanParamsThreeD( x_steps=self.x_steps, diff --git a/tests/unit_tests/common/parameters/test_gridscan.py b/tests/unit_tests/common/parameters/test_gridscan.py index 4eac3661c7..b619364d64 100644 --- a/tests/unit_tests/common/parameters/test_gridscan.py +++ b/tests/unit_tests/common/parameters/test_gridscan.py @@ -2,7 +2,10 @@ from pydantic import ValidationError from mx_bluesky.common.parameters.components import get_param_version -from mx_bluesky.common.parameters.gridscan import SpecifiedGrids +from mx_bluesky.common.parameters.gridscan import ( + SpecifiedGrids, + SpecifiedThreeDGridScan, +) class GridParamsTest(SpecifiedGrids): @@ -69,3 +72,36 @@ def test_specified_grids_validation_error( storage_directory="/tmp", x_steps=5, ) + + +@pytest.mark.parametrize( + "y_starts_um, z_starts_um, omega_starts_deg, y_step_sizes_um, y_steps, should_raise", + [ + ([1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], True), + ([1, 1], [1, 1], [1, 1], [1, 1], [1, 1], False), + ], +) +def test_three_d_grid_scan_validation( + y_starts_um: list[float], + z_starts_um: list[float], + omega_starts_deg: list[float], + y_step_sizes_um: list[float], + y_steps: list[int], + should_raise: bool, +): + if should_raise: + with pytest.raises(ValidationError, match="must be length 2 for 3D scans"): + SpecifiedThreeDGridScan( + x_start_um=0, + y_starts_um=y_starts_um, + z_starts_um=z_starts_um, + omega_starts_deg=omega_starts_deg, + y_step_sizes_um=y_step_sizes_um, + y_steps=y_steps, + sample_id=0, + visit="/tmp", + parameter_model_version=get_param_version(), + file_name="/tmp", + storage_directory="/tmp", + x_steps=5, + ) From 16885daf53131e62ef785cbb157d4d71afe4b26a Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Wed, 18 Feb 2026 13:27:09 +0000 Subject: [PATCH 13/45] Remove SingleGrid class --- src/mx_bluesky/common/parameters/gridscan.py | 61 -------------------- 1 file changed, 61 deletions(-) diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 5ab8c86670..f0523561a9 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -1,6 +1,5 @@ from __future__ import annotations -import weakref from abc import abstractmethod from typing import Annotated, Generic, TypeVar @@ -233,66 +232,6 @@ def num_images(self) -> int: _num_images += len(self.scan_points[grid]["sam_x"]) return _num_images - def __getitem__(self, idx: int) -> _SingleGrid: - if idx < 0 or idx >= self.num_grids: - raise IndexError(idx) - return _SingleGrid(self, idx) - - -class _SingleGrid(Generic[GridScanParamType]): - """Helper class for plan code to not need to refer to an index constantly.""" - - def __init__(self, grids: SpecifiedGrids[GridScanParamType], idx: int): - self._grids_ref = weakref.ref(grids) - self._idx = idx - - @property - def _grids(self) -> SpecifiedGrids[GridScanParamType]: - grids = self._grids_ref() - if grids is None: - raise ReferenceError("Parent SpecifiedGrids object no longer exists") - return grids - - @property - def idx(self) -> int: - return self._idx - - # ---- Per-grid scalar accessors ---- - - @property - def x_steps(self) -> int: - return self._grids.x_steps - - @property - def y_steps(self) -> int: - return self._grids.y_steps[self._idx] - - @property - def x_step_size_um(self) -> float: - return self._grids.x_step_size_um - - @property - def y_step_size_um(self) -> float: - return self._grids.y_step_sizes_um[self._idx] - - @property - def omega_start_deg(self) -> float | None: - return self._grids.omega_starts_deg[self._idx] - - # ---- Derived scan objects ---- - - @property - def grid_spec(self) -> Product[str]: - return self._grids.grid_specs[self._idx] - - @property - def grid_points(self): - return ScanPath(self.grid_spec.calculate()).consume().midpoints - - @property - def num_images(self) -> int: - return len(self.grid_points["sam_x"]) - class SpecifiedThreeDGridScan( SpecifiedGrids[ZebraGridScanParamsThreeD], From d856a8c981b2c067ec87f468883e9c68f909e8d3 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Wed, 18 Feb 2026 14:51:37 +0000 Subject: [PATCH 14/45] don't implement dummy mode xrc results for 2d grids --- .../inner_plans/xrc_results_utils.py | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py b/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py index b95f75d48c..634f1a838b 100644 --- a/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py +++ b/src/mx_bluesky/common/experiment_plans/inner_plans/xrc_results_utils.py @@ -71,28 +71,19 @@ def fetch_xrc_results_from_zocalo( def _generate_dummy_xrc_result(params: SpecifiedThreeDGridScan) -> XRayCentreResult: coms = [] - if params.num_grids % 2 == 0: + assert params.num_grids % 2 == 0, ( "XRC results in commissioning mode currently only works for an even number of grids" + ) - for grid in range(int(params.num_grids / 2)): - # For even number of grids, Z steps are actually the even indexed y steps - coms.append( - [ - params.x_steps / 2, - params.y_steps[2 * grid] / 2, - params.y_steps[2 * grid + 1] / 2, - ] - ) - else: - # For odd number of grids, Z is fixed at the z start position - for grid in range(params.num_grids): - coms.append( - [ - params.x_steps / 2, - params.y_steps[grid] / 2, - params.z_starts_um[grid], - ] - ) + for grid in range(int(params.num_grids / 2)): + # For even number of grids, Z steps are actually the even indexed y steps + coms.append( + [ + params.x_steps / 2, + params.y_steps[2 * grid] / 2, + params.y_steps[2 * grid + 1] / 2, + ] + ) com = [sum(x) / len(x) for x in zip(*coms, strict=True)] # Get average From a31185d3802438ec1255ca6e15b8e8083c1ebabe Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Wed, 18 Feb 2026 15:11:38 +0000 Subject: [PATCH 15/45] Link to issue in comments --- src/mx_bluesky/common/parameters/gridscan.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index f0523561a9..6cef9bdf04 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -121,6 +121,9 @@ class SpecifiedGrids(GenericGrid, XyzStarts, WithScan, Generic[GridScanParamType grid and box sizes, etc., as opposed to parameters for a plan which will create those parameters at some point (e.g. through optical pin detection).""" + # See https://github.com/DiamondLightSource/mx-bluesky/issues/1634 for a better structure for this + # class + omega_starts_deg: list[float] = Field( default=[GridscanParamConstants.OMEGA_1, GridscanParamConstants.OMEGA_2] ) From 668c8e0d7239b6f675e7939dad27480faaadcf1b Mon Sep 17 00:00:00 2001 From: Ollie Silvester Date: Mon, 2 Mar 2026 15:55:29 +0000 Subject: [PATCH 16/45] Keep up to date --- src/mx_bluesky/hyperion/blueapi/parameters.py | 2 +- .../hyperion/external_interaction/test_agamemnon.py | 2 +- .../external_load_centre_collect_params.json | 5 ++++- .../good_test_rotation_scan_parameters.json | 5 ++++- .../good_test_specified_three_d_grid_params.json | 5 ++++- .../ispyb_gridscan_system_test_parameters.json | 5 ++++- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/mx_bluesky/hyperion/blueapi/parameters.py b/src/mx_bluesky/hyperion/blueapi/parameters.py index dd405398aa..4a0f724b9e 100644 --- a/src/mx_bluesky/hyperion/blueapi/parameters.py +++ b/src/mx_bluesky/hyperion/blueapi/parameters.py @@ -21,7 +21,7 @@ class RobotLoadThenCentreParams(HyperionParam): file_name: str transmission_frac: float exposure_time_s: float - omega_start_deg: float + omega_starts_deg: list[int] chi_start_deg: float tip_offset_um: float grid_width_um: float diff --git a/tests/system_tests/hyperion/external_interaction/test_agamemnon.py b/tests/system_tests/hyperion/external_interaction/test_agamemnon.py index 399deb65b6..ccf967029e 100644 --- a/tests/system_tests/hyperion/external_interaction/test_agamemnon.py +++ b/tests/system_tests/hyperion/external_interaction/test_agamemnon.py @@ -30,7 +30,7 @@ "demand_energy_ev": 12700.045934258673, "tip_offset_um": 300, "grid_width_um": 600, - "omega_start_deg": 0, + "omega_starts_deg": [0, 90], "chi_start_deg": 0, "transmission_frac": 1.0, } diff --git a/tests/test_data/parameter_json_files/external_load_centre_collect_params.json b/tests/test_data/parameter_json_files/external_load_centre_collect_params.json index e5a52e97bf..4ced4d2c64 100644 --- a/tests/test_data/parameter_json_files/external_load_centre_collect_params.json +++ b/tests/test_data/parameter_json_files/external_load_centre_collect_params.json @@ -10,7 +10,10 @@ "file_name": "robot_load_centring_file", "transmission_frac": 1.0, "exposure_time_s": 0.002, - "omega_start_deg": 0, + "omega_starts_deg": [ + 0, + 90 + ], "chi_start_deg": 30, "tip_offset_um": 25, "grid_width_um": 20 diff --git a/tests/test_data/parameter_json_files/good_test_rotation_scan_parameters.json b/tests/test_data/parameter_json_files/good_test_rotation_scan_parameters.json index 0fb13b4777..a0a9c7c2c0 100644 --- a/tests/test_data/parameter_json_files/good_test_rotation_scan_parameters.json +++ b/tests/test_data/parameter_json_files/good_test_rotation_scan_parameters.json @@ -7,7 +7,10 @@ "demand_energy_ev": 100, "exposure_time_s": 0.1, "insertion_prefix": "SR03S", - "omega_start_deg": 0.0, + "omega_starts_deg": [ + 0, + 90 + ], "file_name": "file_name", "scan_width_deg": 180.0, "rotation_axis": "omega", diff --git a/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json b/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json index 714d90f63b..f0be3e0d06 100644 --- a/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json +++ b/tests/test_data/parameter_json_files/good_test_specified_three_d_grid_params.json @@ -7,7 +7,10 @@ "visit": "cm31105-4", "exposure_time_s": 0.1, "insertion_prefix": "SR03S", - "omega_start_deg": 0.0, + "omega_starts_deg": [ + 0, + 90 + ], "file_name": "file_name", "sample_id": 123456, "run_number": 0, diff --git a/tests/test_data/parameter_json_files/ispyb_gridscan_system_test_parameters.json b/tests/test_data/parameter_json_files/ispyb_gridscan_system_test_parameters.json index 32b478a62a..72947b1788 100644 --- a/tests/test_data/parameter_json_files/ispyb_gridscan_system_test_parameters.json +++ b/tests/test_data/parameter_json_files/ispyb_gridscan_system_test_parameters.json @@ -10,7 +10,10 @@ "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", "exposure_time_s": 0.12, "detector_distance_mm": 100.0, - "omega_start_deg": 0.0, + "omega_starts_deg": [ + 0, + 90 + ], "grid_width_um": 400, "transmission_frac": 0.49118, "visit": "cm31105-4", From 0e68e2b4fa8005e8d485761981a6282e0418ab89 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Wed, 18 Feb 2026 17:20:28 +0000 Subject: [PATCH 17/45] Add vmxm FGS entry point --- src/mx_bluesky/beamlines/i02_1/__init__.py | 0 .../i02_1/device_setup_plans/__init__.py | 0 .../i02_1/device_setup_plans/setup_zebra.py | 43 +++++ .../i02_1/i02_1_flyscan_xray_centre_plan.py | 166 ++++++++++++++++++ .../beamlines/i02_1/parameters/gridscan.py | 2 +- .../common_flyscan_xray_centre_plan.py | 15 +- src/mx_bluesky/common/parameters/gridscan.py | 4 +- 7 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 src/mx_bluesky/beamlines/i02_1/__init__.py create mode 100644 src/mx_bluesky/beamlines/i02_1/device_setup_plans/__init__.py create mode 100644 src/mx_bluesky/beamlines/i02_1/device_setup_plans/setup_zebra.py create mode 100644 src/mx_bluesky/beamlines/i02_1/i02_1_flyscan_xray_centre_plan.py diff --git a/src/mx_bluesky/beamlines/i02_1/__init__.py b/src/mx_bluesky/beamlines/i02_1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/mx_bluesky/beamlines/i02_1/device_setup_plans/__init__.py b/src/mx_bluesky/beamlines/i02_1/device_setup_plans/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/mx_bluesky/beamlines/i02_1/device_setup_plans/setup_zebra.py b/src/mx_bluesky/beamlines/i02_1/device_setup_plans/setup_zebra.py new file mode 100644 index 0000000000..f9a282920d --- /dev/null +++ b/src/mx_bluesky/beamlines/i02_1/device_setup_plans/setup_zebra.py @@ -0,0 +1,43 @@ +import bluesky.plan_stubs as bps +from dodal.devices.zebra.zebra import Zebra + +ZEBRA_STATUS_TIMEOUT = 30 + + +# Control Eiger from motion controller. Fast shutter is configured in GDA +def setup_zebra_for_xrc_flyscan( + zebra: Zebra, + ttl_detector: int | None = None, + group="setup_zebra_for_xrc", + wait=True, +): + """ + Assumes that the motion controller, as part of its gridscan PLC, will send triggers as required to the zebra's + IN1_TTL to control the detector. The fast shutter is configured in GDA, don't need to touch it in Bluesky for now. + """ + ttl_detector = ttl_detector or zebra.mapping.outputs.TTL_EIGER + yield from bps.abs_set( + zebra.output.out_pvs[ttl_detector], + zebra.mapping.sources.IN1_TTL, + ) + if wait: + yield from bps.wait(group, timeout=ZEBRA_STATUS_TIMEOUT) + + +def tidy_up_zebra_after_gridscan( + zebra: Zebra, + ttl_detector: int | None = None, + group="tidy_up_vmxm_zebra_after_gridscan", + wait=False, +): + ttl_detector = ttl_detector or zebra.mapping.outputs.TTL_EIGER + + """# Revert zebra to state expected by GDA""" + yield from bps.abs_set( + zebra.output.out_pvs[ttl_detector], + zebra.mapping.sources.OR1, + group=group, + ) + + if wait: + yield from bps.wait(group) diff --git a/src/mx_bluesky/beamlines/i02_1/i02_1_flyscan_xray_centre_plan.py b/src/mx_bluesky/beamlines/i02_1/i02_1_flyscan_xray_centre_plan.py new file mode 100644 index 0000000000..ba029718e0 --- /dev/null +++ b/src/mx_bluesky/beamlines/i02_1/i02_1_flyscan_xray_centre_plan.py @@ -0,0 +1,166 @@ +from functools import partial + +import bluesky.plan_stubs as bps +import bluesky.preprocessors as bpp +import pydantic +from bluesky.utils import MsgGenerator +from dodal.beamlines.i02_1 import SampleMotors, ZebraFastGridScanTwoD +from dodal.common import inject +from dodal.devices.attenuator.attenuator import ReadOnlyAttenuator +from dodal.devices.common_dcm import DoubleCrystalMonochromatorBase +from dodal.devices.eiger import EigerDetector +from dodal.devices.fast_grid_scan import ( + set_fast_grid_scan_params as set_flyscan_params_plan, +) +from dodal.devices.flux import Flux +from dodal.devices.s4_slit_gaps import S4SlitGaps +from dodal.devices.synchrotron import Synchrotron +from dodal.devices.undulator import BaseUndulator +from dodal.devices.zebra.zebra import Zebra + +from mx_bluesky.beamlines.i02_1.device_setup_plans.setup_zebra import ( + setup_zebra_for_xrc_flyscan, + tidy_up_zebra_after_gridscan, +) +from mx_bluesky.beamlines.i02_1.parameters.gridscan import SpecifiedTwoDGridScan +from mx_bluesky.common.experiment_plans.common_flyscan_xray_centre_plan import ( + BeamlineSpecificFGSFeatures, + common_flyscan_xray_centre, + construct_beamline_specific_fast_gridscan_features, +) +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 ( + GridscanNexusFileCallback, +) +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.utils.log import LOGGER + + +def create_gridscan_callbacks() -> tuple[ + GridscanNexusFileCallback, GridscanISPyBCallback +]: + return ( + GridscanNexusFileCallback(param_type=SpecifiedTwoDGridScan), + GridscanISPyBCallback( + param_type=GenericGrid, + emit=ZocaloCallback( + PlanNameConstants.DO_FGS, + EnvironmentConstants.ZOCALO_ENV, + generate_start_info_from_omega_map, + ), + ), + ) + + +@pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True}) +class FlyScanXRayCentreComposite(FlyScanEssentialDevices[GonioWithOmegaType]): + """All devices which are directly or indirectly required by this plan""" + + zebra: Zebra + zebra_fast_grid_scan: ZebraFastGridScanTwoD + dcm: DoubleCrystalMonochromatorBase + attenuator: ReadOnlyAttenuator + flux: Flux + undulator: BaseUndulator + s4_slit_gaps: S4SlitGaps + + +def construct_i02_1_specific_features( + fgs_composite: FlyScanXRayCentreComposite, + parameters: SpecifiedTwoDGridScan, +) -> BeamlineSpecificFGSFeatures: + signals_to_read_pre_flyscan = [ + fgs_composite.synchrotron.synchrotron_mode, + fgs_composite.gonio, + fgs_composite.dcm.energy_in_keV, + fgs_composite.undulator.current_gap, + fgs_composite.s4_slit_gaps, + ] + + signals_to_read_during_collection = [ + fgs_composite.attenuator.actual_transmission, + fgs_composite.flux.flux_reading, + fgs_composite.dcm.energy_in_keV, + fgs_composite.eiger.bit_depth, + fgs_composite.eiger.cam.roi_mode, + fgs_composite.eiger.ispyb_detector_id, + ] + + return construct_beamline_specific_fast_gridscan_features( + partial(_zebra_triggering_setup), + partial(_tidy_plan, fgs_composite, group="flyscan_zebra_tidy", wait=True), + partial( + set_flyscan_params_plan, + fgs_composite.zebra_fast_grid_scan, + parameters.fast_gridscan_params, + ), + fgs_composite.zebra_fast_grid_scan, + signals_to_read_pre_flyscan, + signals_to_read_during_collection, # type: ignore # See : https://github.com/bluesky/bluesky/issues/1809 + ) + + +def _zebra_triggering_setup(fgs_composite: FlyScanXRayCentreComposite, _): + yield from setup_zebra_for_xrc_flyscan(fgs_composite.zebra) + + +def _tidy_plan( + fgs_composite: FlyScanXRayCentreComposite, group, wait=True +) -> MsgGenerator: + LOGGER.info("Tidying up Zebra") + yield from tidy_up_zebra_after_gridscan(fgs_composite.zebra) + + +def i02_1_flyscan_xray_centre( + parameters: SpecifiedTwoDGridScan, + eiger: EigerDetector = inject("eiger"), + zebra_fast_grid_scan: ZebraFastGridScanTwoD = inject("ZebraFastGridScanTwoD"), + synchrotron: Synchrotron = inject("synchrotron"), + zebra: Zebra = inject("zebra"), + gonio: SampleMotors = inject("goniometer"), + attenuator: ReadOnlyAttenuator = inject("attenuator"), + dcm: DoubleCrystalMonochromatorBase = inject("dcm"), + flux: Flux = inject("flux"), + undulator: BaseUndulator = inject("undulator"), + s4_slit_gaps: S4SlitGaps = inject("s4_slit_gaps"), +) -> MsgGenerator: + """BlueAPI entry point for XRC grid scans""" + + # Composites have to be made this way until https://github.com/DiamondLightSource/dodal/issues/874 + # is done and we can properly use composite devices in BlueAPI + composite = FlyScanXRayCentreComposite( + eiger, + synchrotron, + gonio, + zebra, + zebra_fast_grid_scan, + dcm, + attenuator, + flux, + undulator, + s4_slit_gaps, + ) + + beamline_specific = construct_i02_1_specific_features(composite, parameters) + callbacks = create_gridscan_callbacks() + + @bpp.subs_decorator(callbacks) + def decorated_flyscan_plan(): + yield from bps.null() + yield from common_flyscan_xray_centre(composite, parameters, beamline_specific) + + yield from decorated_flyscan_plan() diff --git a/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py b/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py index 7c31bc5830..34ee4d0212 100644 --- a/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py +++ b/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py @@ -24,5 +24,5 @@ def fast_gridscan_params(self) -> ZebraGridScanParamsTwoD: z1_start_mm=self.z_starts_um[0] / 1000, set_stub_offsets=self._set_stub_offsets, transmission_fraction=0.5, - dwell_time_ms=self.exposure_time_s, + dwell_time_ms=self.exposure_time_s * 1000, ) diff --git a/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py b/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py index 2d2a2a3d34..b176d97045 100644 --- a/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +++ b/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py @@ -30,7 +30,9 @@ FlyScanEssentialDevices, GonioWithOmegaType, ) -from mx_bluesky.common.parameters.gridscan import SpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import ( + SpecifiedGrids, +) from mx_bluesky.common.utils.exceptions import ( SampleError, ) @@ -114,7 +116,7 @@ def construct_beamline_specific_fast_gridscan_features( def common_flyscan_xray_centre( composite: FlyScanEssentialDevices[GonioWithOmegaType], - parameters: SpecifiedThreeDGridScan, + parameters: SpecifiedGrids, beamline_specific: BeamlineSpecificFGSFeatures, ) -> MsgGenerator: """Main entry point of the MX-Bluesky x-ray centering flyscan @@ -156,7 +158,7 @@ def _decorated_flyscan(): @bpp.finalize_decorator(lambda: _overall_tidy()) def run_gridscan_and_tidy( fgs_composite: FlyScanEssentialDevices[GonioWithOmegaType], - params: SpecifiedThreeDGridScan, + params: SpecifiedGrids, beamline_specific: BeamlineSpecificFGSFeatures, ) -> MsgGenerator: yield from beamline_specific.setup_trigger_plan(fgs_composite, parameters) @@ -174,12 +176,13 @@ def run_gridscan_and_tidy( def run_gridscan( fgs_composite: FlyScanEssentialDevices[GonioWithOmegaType], - parameters: SpecifiedThreeDGridScan, + parameters: SpecifiedGrids, beamline_specific: BeamlineSpecificFGSFeatures, ): - # Currently gridscan only works for omega 0, see https://github.com/DiamondLightSource/mx-bluesky/issues/410 with TRACER.start_span("moving_omega_to_0"): - yield from bps.abs_set(fgs_composite.gonio.omega, 0) + yield from bps.abs_set( + fgs_composite.gonio.omega, parameters.omega_starts_deg[0], wait=True + ) with TRACER.start_span("ispyb_hardware_readings"): yield from beamline_specific.read_pre_flyscan_plan() diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 6cef9bdf04..1d2863a490 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -4,7 +4,7 @@ from typing import Annotated, Generic, TypeVar from dodal.devices.aperturescatterguard import ApertureValue -from dodal.devices.detector.det_dim_constants import EIGER2_X_9M_SIZE, EIGER2_X_16M_SIZE +from dodal.devices.detector.det_dim_constants import EIGER2_X_4M_SIZE, EIGER2_X_16M_SIZE from dodal.devices.detector.detector import DetectorParams from dodal.devices.fast_grid_scan import ( GridScanParamsCommon, @@ -32,7 +32,7 @@ ) DETECTOR_SIZE_PER_BEAMLINE = { - "i02-1": EIGER2_X_9M_SIZE, + "i02-1": EIGER2_X_4M_SIZE, "dev": EIGER2_X_16M_SIZE, "i03": EIGER2_X_16M_SIZE, "i04": EIGER2_X_16M_SIZE, From 945cb4c1c5d69348934ec544ceea95bf9a3acdb9 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Wed, 18 Feb 2026 18:33:46 +0000 Subject: [PATCH 18/45] Fixes and tests --- .../i02_1/device_setup_plans/setup_zebra.py | 10 +- ..._centre_plan.py => i02_1_gridscan_plan.py} | 40 +---- .../beamlines/i02_1/parameters/gridscan.py | 13 ++ src/mx_bluesky/common/parameters/constants.py | 2 + src/mx_bluesky/common/parameters/gridscan.py | 3 + tests/unit_tests/beamlines/i02_1/__init__.py | 0 .../i02_1/test_i02_1_gridscan_plan.py | 151 ++++++++++++++++++ .../beamlines/i02_1/test_setup_zebra.py | 53 ++++++ 8 files changed, 234 insertions(+), 38 deletions(-) rename src/mx_bluesky/beamlines/i02_1/{i02_1_flyscan_xray_centre_plan.py => i02_1_gridscan_plan.py} (77%) create mode 100644 tests/unit_tests/beamlines/i02_1/__init__.py create mode 100644 tests/unit_tests/beamlines/i02_1/test_i02_1_gridscan_plan.py create mode 100644 tests/unit_tests/beamlines/i02_1/test_setup_zebra.py diff --git a/src/mx_bluesky/beamlines/i02_1/device_setup_plans/setup_zebra.py b/src/mx_bluesky/beamlines/i02_1/device_setup_plans/setup_zebra.py index f9a282920d..be8bd041ff 100644 --- a/src/mx_bluesky/beamlines/i02_1/device_setup_plans/setup_zebra.py +++ b/src/mx_bluesky/beamlines/i02_1/device_setup_plans/setup_zebra.py @@ -1,14 +1,16 @@ import bluesky.plan_stubs as bps from dodal.devices.zebra.zebra import Zebra +from mx_bluesky.common.parameters.constants import PlanGroupCheckpointConstants + ZEBRA_STATUS_TIMEOUT = 30 # Control Eiger from motion controller. Fast shutter is configured in GDA -def setup_zebra_for_xrc_flyscan( +def setup_zebra_for_gridscan( zebra: Zebra, ttl_detector: int | None = None, - group="setup_zebra_for_xrc", + group=PlanGroupCheckpointConstants.SETUP_ZEBRA_FOR_GRIDSCAN, wait=True, ): """ @@ -27,12 +29,12 @@ def setup_zebra_for_xrc_flyscan( def tidy_up_zebra_after_gridscan( zebra: Zebra, ttl_detector: int | None = None, - group="tidy_up_vmxm_zebra_after_gridscan", + group=PlanGroupCheckpointConstants.TIDY_ZEBRA_AFTER_GRIDSCAN, wait=False, ): + """Revert zebra to state expected by GDA""" ttl_detector = ttl_detector or zebra.mapping.outputs.TTL_EIGER - """# Revert zebra to state expected by GDA""" yield from bps.abs_set( zebra.output.out_pvs[ttl_detector], zebra.mapping.sources.OR1, diff --git a/src/mx_bluesky/beamlines/i02_1/i02_1_flyscan_xray_centre_plan.py b/src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py similarity index 77% rename from src/mx_bluesky/beamlines/i02_1/i02_1_flyscan_xray_centre_plan.py rename to src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py index ba029718e0..fe19a767c5 100644 --- a/src/mx_bluesky/beamlines/i02_1/i02_1_flyscan_xray_centre_plan.py +++ b/src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py @@ -1,25 +1,22 @@ from functools import partial -import bluesky.plan_stubs as bps import bluesky.preprocessors as bpp import pydantic from bluesky.utils import MsgGenerator -from dodal.beamlines.i02_1 import SampleMotors, ZebraFastGridScanTwoD +from dodal.beamlines.i02_1 import ZebraFastGridScanTwoD from dodal.common import inject from dodal.devices.attenuator.attenuator import ReadOnlyAttenuator from dodal.devices.common_dcm import DoubleCrystalMonochromatorBase -from dodal.devices.eiger import EigerDetector from dodal.devices.fast_grid_scan import ( set_fast_grid_scan_params as set_flyscan_params_plan, ) from dodal.devices.flux import Flux from dodal.devices.s4_slit_gaps import S4SlitGaps -from dodal.devices.synchrotron import Synchrotron from dodal.devices.undulator import BaseUndulator from dodal.devices.zebra.zebra import Zebra from mx_bluesky.beamlines.i02_1.device_setup_plans.setup_zebra import ( - setup_zebra_for_xrc_flyscan, + setup_zebra_for_gridscan, tidy_up_zebra_after_gridscan, ) from mx_bluesky.beamlines.i02_1.parameters.gridscan import SpecifiedTwoDGridScan @@ -115,7 +112,7 @@ def construct_i02_1_specific_features( def _zebra_triggering_setup(fgs_composite: FlyScanXRayCentreComposite, _): - yield from setup_zebra_for_xrc_flyscan(fgs_composite.zebra) + yield from setup_zebra_for_gridscan(fgs_composite.zebra) def _tidy_plan( @@ -125,42 +122,17 @@ def _tidy_plan( yield from tidy_up_zebra_after_gridscan(fgs_composite.zebra) -def i02_1_flyscan_xray_centre( +def i02_1_gridscan_plan( parameters: SpecifiedTwoDGridScan, - eiger: EigerDetector = inject("eiger"), - zebra_fast_grid_scan: ZebraFastGridScanTwoD = inject("ZebraFastGridScanTwoD"), - synchrotron: Synchrotron = inject("synchrotron"), - zebra: Zebra = inject("zebra"), - gonio: SampleMotors = inject("goniometer"), - attenuator: ReadOnlyAttenuator = inject("attenuator"), - dcm: DoubleCrystalMonochromatorBase = inject("dcm"), - flux: Flux = inject("flux"), - undulator: BaseUndulator = inject("undulator"), - s4_slit_gaps: S4SlitGaps = inject("s4_slit_gaps"), + composite: FlyScanXRayCentreComposite = inject(""), ) -> MsgGenerator: - """BlueAPI entry point for XRC grid scans""" - - # Composites have to be made this way until https://github.com/DiamondLightSource/dodal/issues/874 - # is done and we can properly use composite devices in BlueAPI - composite = FlyScanXRayCentreComposite( - eiger, - synchrotron, - gonio, - zebra, - zebra_fast_grid_scan, - dcm, - attenuator, - flux, - undulator, - s4_slit_gaps, - ) + """BlueAPI entry point for i02-1 grid scans""" beamline_specific = construct_i02_1_specific_features(composite, parameters) callbacks = create_gridscan_callbacks() @bpp.subs_decorator(callbacks) def decorated_flyscan_plan(): - yield from bps.null() yield from common_flyscan_xray_centre(composite, parameters, beamline_specific) yield from decorated_flyscan_plan() diff --git a/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py b/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py index 34ee4d0212..6bc6ae241b 100644 --- a/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py +++ b/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py @@ -1,4 +1,5 @@ from dodal.devices.beamlines.i02_1.fast_grid_scan import ZebraGridScanParamsTwoD +from pydantic import model_validator from mx_bluesky.common.parameters.components import SplitScan, WithOptionalEnergyChange from mx_bluesky.common.parameters.gridscan import SpecifiedGrids @@ -26,3 +27,15 @@ def fast_gridscan_params(self) -> ZebraGridScanParamsTwoD: transmission_fraction=0.5, dwell_time_ms=self.exposure_time_s * 1000, ) + + @model_validator(mode="after") + def validate_y_axes(self): + _err_str = "must be length 1 for 2D scans" + if len(self.y_steps) != 1: + raise ValueError(f"{self.y_steps=} {_err_str}") + if len(self.y_step_sizes_um) != 1: + raise ValueError(f"{self.y_step_sizes_um=} {_err_str}") + if len(self.omega_starts_deg) != 1: + raise ValueError(f"{self.y_step_sizes_um=} {_err_str}") + + return self diff --git a/src/mx_bluesky/common/parameters/constants.py b/src/mx_bluesky/common/parameters/constants.py index d4855219cb..158dbe28af 100644 --- a/src/mx_bluesky/common/parameters/constants.py +++ b/src/mx_bluesky/common/parameters/constants.py @@ -144,6 +144,8 @@ class PlanGroupCheckpointConstants: READY_FOR_OAV = "ready_for_oav" PREPARE_APERTURE = "prepare_aperture" SETUP_ZEBRA_FOR_ROTATION = "setup_zebra_for_rotation" + SETUP_ZEBRA_FOR_GRIDSCAN = "setup_zebra_for_gridscan" + TIDY_ZEBRA_AFTER_GRIDSCAN = "tidy_zebra_after_gridscan" # Eventually replace below with https://github.com/DiamondLightSource/mx-bluesky/issues/798 diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 1d2863a490..4ab7344690 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -251,6 +251,9 @@ def validate_y_and_z_axes(self): raise ValueError(f"{self.y_steps=} {_err_str}") if len(self.y_step_sizes_um) != 2: raise ValueError(f"{self.y_step_sizes_um=} {_err_str}") + if len(self.omega_starts_deg) != 2: + raise ValueError(f"{self.y_step_sizes_um=} {_err_str}") + return self @property diff --git a/tests/unit_tests/beamlines/i02_1/__init__.py b/tests/unit_tests/beamlines/i02_1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..600ec4c6f3 --- /dev/null +++ b/tests/unit_tests/beamlines/i02_1/test_i02_1_gridscan_plan.py @@ -0,0 +1,151 @@ +from unittest.mock import MagicMock, patch + +import pytest +from bluesky.run_engine import RunEngine +from dodal.beamlines import i02_1 +from dodal.beamlines.i02_1 import ZebraFastGridScanTwoD +from dodal.devices.attenuator.attenuator import ReadOnlyAttenuator +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.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.i02_1_gridscan_plan import ( + FlyScanXRayCentreComposite, + 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, +) + + +@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", + file_name="test_file", + storage_directory=str(tmp_path), + x_steps=5, + y_steps=[3], + ) + + +@pytest.fixture +def zebra_fgs_two_d() -> ZebraFastGridScanTwoD: + device = i02_1.zebra_fast_grid_scan.build(connect_immediately=True, mock=True) + + return device + + +@pytest.fixture +def fgs_composite( + eiger: EigerDetector, + synchrotron: Synchrotron, + smargon: GonioWithOmega, + zebra_fgs_two_d: ZebraFastGridScanTwoD, + dcm: DoubleCrystalMonochromatorBase, + attenuator: ReadOnlyAttenuator, + flux: Flux, + undulator: BaseUndulator, + s4_slit_gaps: S4SlitGaps, + zebra: Zebra, +) -> FlyScanXRayCentreComposite: + return FlyScanXRayCentreComposite( + eiger, + synchrotron, + smargon, + zebra, + zebra_fgs_two_d, + dcm, + attenuator, + flux, + undulator, + s4_slit_gaps, + ) + + +@pytest.mark.parametrize( + "y_starts_um, z_starts_um, omega_starts_deg, y_step_sizes_um, y_steps, should_raise", + [ + ([1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], True), + ([1], [1], [1], [1], [1], False), + ], +) +def test_three_d_grid_scan_validation( + y_starts_um: list[float], + z_starts_um: list[float], + omega_starts_deg: list[float], + y_step_sizes_um: list[float], + y_steps: list[int], + should_raise: bool, + tmp_path, +): + if should_raise: + with pytest.raises(ValidationError, match="must be length 1 for 2D scans"): + SpecifiedTwoDGridScan( + x_start_um=0, + y_starts_um=y_starts_um, + z_starts_um=z_starts_um, + y_step_sizes_um=y_step_sizes_um, + omega_starts_deg=omega_starts_deg, + parameter_model_version=get_param_version(), + sample_id=0, + visit="visit", + file_name="test_file", + storage_directory=str(tmp_path), + x_steps=5, + y_steps=y_steps, + ) + else: + 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", + file_name="test_file", + storage_directory=str(tmp_path), + x_steps=5, + y_steps=[3], + ) + + +@patch( + "mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan.construct_i02_1_specific_features", +) +@patch( + "mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan.common_flyscan_xray_centre", +) +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_composite: FlyScanXRayCentreComposite, +): + expected_features = construct_i02_1_specific_features( + fgs_composite, fgs_params_two_d + ) + + 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 + ) diff --git a/tests/unit_tests/beamlines/i02_1/test_setup_zebra.py b/tests/unit_tests/beamlines/i02_1/test_setup_zebra.py new file mode 100644 index 0000000000..cd9d02c37b --- /dev/null +++ b/tests/unit_tests/beamlines/i02_1/test_setup_zebra.py @@ -0,0 +1,53 @@ +import pytest +from bluesky.simulators import RunEngineSimulator, assert_message_and_return_remaining +from dodal.beamlines import i02_1 +from dodal.devices.zebra.zebra import Zebra + +from mx_bluesky.beamlines.i02_1.device_setup_plans.setup_zebra import ( + setup_zebra_for_gridscan, + tidy_up_zebra_after_gridscan, +) +from mx_bluesky.common.parameters.constants import PlanGroupCheckpointConstants + + +@pytest.fixture +def zebra(): + return i02_1.zebra.build(connect_immediately=True, mock=True) + + +async def test_zebra_set_up_for_gridscan( + sim_run_engine: RunEngineSimulator, + zebra: Zebra, +): + msgs = sim_run_engine.simulate_plan(setup_zebra_for_gridscan(zebra)) + assert_message_and_return_remaining( + msgs, + lambda msg: msg.command == "set" + and msg.obj.name == f"zebra-output-out_pvs-{zebra.mapping.outputs.TTL_EIGER}" + and msg.args[0] == zebra.mapping.sources.IN1_TTL, + ) + assert_message_and_return_remaining( + msgs, + lambda msg: msg.command == "wait" + and msg.kwargs["group"] + == PlanGroupCheckpointConstants.SETUP_ZEBRA_FOR_GRIDSCAN, + ) + + +async def test_tidy_up_zebra_after_gridscan( + sim_run_engine: RunEngineSimulator, + zebra: Zebra, +): + msgs = sim_run_engine.simulate_plan(tidy_up_zebra_after_gridscan(zebra, wait=True)) + assert_message_and_return_remaining( + msgs, + lambda msg: msg.command == "set" + and msg.obj.name == f"zebra-output-out_pvs-{zebra.mapping.outputs.TTL_EIGER}" + and msg.args[0] == zebra.mapping.sources.OR1, + ) + assert_message_and_return_remaining( + msgs, + lambda msg: msg.command == "wait" + and msg.kwargs["group"] + == PlanGroupCheckpointConstants.TIDY_ZEBRA_AFTER_GRIDSCAN, + ) From 4a93d733aa87e6e3fbf26a22f88b7bbf543dda4f Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Wed, 18 Feb 2026 18:37:07 +0000 Subject: [PATCH 19/45] Typo --- src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py b/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py index 6bc6ae241b..e60a1f534d 100644 --- a/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py +++ b/src/mx_bluesky/beamlines/i02_1/parameters/gridscan.py @@ -36,6 +36,6 @@ def validate_y_axes(self): if len(self.y_step_sizes_um) != 1: raise ValueError(f"{self.y_step_sizes_um=} {_err_str}") if len(self.omega_starts_deg) != 1: - raise ValueError(f"{self.y_step_sizes_um=} {_err_str}") + raise ValueError(f"{self.omega_starts_deg=} {_err_str}") return self From 6198594d4d0e0a63bfe8036583c88595e6bc89fa Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Wed, 18 Feb 2026 18:44:07 +0000 Subject: [PATCH 20/45] make codecov happy --- src/mx_bluesky/common/parameters/gridscan.py | 2 +- tests/unit_tests/common/parameters/test_gridscan.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 4ab7344690..71469e56eb 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -252,7 +252,7 @@ def validate_y_and_z_axes(self): if len(self.y_step_sizes_um) != 2: raise ValueError(f"{self.y_step_sizes_um=} {_err_str}") if len(self.omega_starts_deg) != 2: - raise ValueError(f"{self.y_step_sizes_um=} {_err_str}") + raise ValueError(f"{self.omega_starts_deg=} {_err_str}") return self diff --git a/tests/unit_tests/common/parameters/test_gridscan.py b/tests/unit_tests/common/parameters/test_gridscan.py index b619364d64..100413e889 100644 --- a/tests/unit_tests/common/parameters/test_gridscan.py +++ b/tests/unit_tests/common/parameters/test_gridscan.py @@ -78,6 +78,8 @@ def test_specified_grids_validation_error( "y_starts_um, z_starts_um, omega_starts_deg, y_step_sizes_um, y_steps, should_raise", [ ([1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], True), + ([1, 1], [1, 1], [1, 1], [1, 1, 1], [1, 1], False), + ([1, 1], [1, 1], [1, 1, 1], [1, 1], [1, 1], False), ([1, 1], [1, 1], [1, 1], [1, 1], [1, 1], False), ], ) From 638a8c1860441beec0fdfa0c23e5b9a5d584cd71 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Thu, 19 Feb 2026 09:45:36 +0000 Subject: [PATCH 21/45] improve tests --- .../i02_1/test_i02_1_gridscan_plan.py | 46 ++++++------ .../common/parameters/test_gridscan.py | 71 ++++++++++--------- 2 files changed, 58 insertions(+), 59 deletions(-) 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 600ec4c6f3..49f5cb5513 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 @@ -78,14 +78,22 @@ def fgs_composite( ) +class SpecifiedTwoDTest(SpecifiedTwoDGridScan): + # Skip parent validation for easier testing + def _check_lengths_are_same(self): # type: ignore + return self + + @pytest.mark.parametrize( "y_starts_um, z_starts_um, omega_starts_deg, y_step_sizes_um, y_steps, should_raise", [ ([1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], True), + ([1], [1], [1], [1, 1], [1], True), + ([1], [1], [1, 1], [1], [1], True), ([1], [1], [1], [1], [1], False), ], ) -def test_three_d_grid_scan_validation( +def test_two_d_grid_scan_validation( y_starts_um: list[float], z_starts_um: list[float], omega_starts_deg: list[float], @@ -94,38 +102,28 @@ def test_three_d_grid_scan_validation( should_raise: bool, tmp_path, ): - if should_raise: - with pytest.raises(ValidationError, match="must be length 1 for 2D scans"): - SpecifiedTwoDGridScan( - x_start_um=0, - y_starts_um=y_starts_um, - z_starts_um=z_starts_um, - y_step_sizes_um=y_step_sizes_um, - omega_starts_deg=omega_starts_deg, - parameter_model_version=get_param_version(), - sample_id=0, - visit="visit", - file_name="test_file", - storage_directory=str(tmp_path), - x_steps=5, - y_steps=y_steps, - ) - else: - SpecifiedTwoDGridScan( + def create_params(): + SpecifiedTwoDTest( x_start_um=0, - y_starts_um=[0], - z_starts_um=[0], - y_step_sizes_um=[10], - omega_starts_deg=[0], + y_starts_um=y_starts_um, + z_starts_um=z_starts_um, + y_step_sizes_um=y_step_sizes_um, + omega_starts_deg=omega_starts_deg, parameter_model_version=get_param_version(), sample_id=0, visit="visit", file_name="test_file", storage_directory=str(tmp_path), x_steps=5, - y_steps=[3], + y_steps=y_steps, ) + if should_raise: + with pytest.raises(ValidationError, match="must be length 1 for 2D scans"): + create_params() + else: + create_params() + @patch( "mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan.construct_i02_1_specific_features", diff --git a/tests/unit_tests/common/parameters/test_gridscan.py b/tests/unit_tests/common/parameters/test_gridscan.py index 100413e889..60e592e15d 100644 --- a/tests/unit_tests/common/parameters/test_gridscan.py +++ b/tests/unit_tests/common/parameters/test_gridscan.py @@ -39,25 +39,7 @@ def test_specified_grids_validation_error( y_steps: list[int], should_raise: bool, ): - if should_raise: - with pytest.raises( - ValidationError, match="Fields must all have the same length:" - ): - GridParamsTest( - x_start_um=0, - y_starts_um=y_starts_um, - z_starts_um=z_starts_um, - omega_starts_deg=omega_starts_deg, - y_step_sizes_um=y_step_sizes_um, - y_steps=y_steps, - sample_id=0, - visit="/tmp", - parameter_model_version=get_param_version(), - file_name="/tmp", - storage_directory="/tmp", - x_steps=5, - ) - else: + def make_params(): GridParamsTest( x_start_um=0, y_starts_um=y_starts_um, @@ -73,13 +55,27 @@ def test_specified_grids_validation_error( x_steps=5, ) + if should_raise: + with pytest.raises( + ValidationError, match="Fields must all have the same length:" + ): + make_params() + else: + make_params() + + +class SpecifiedThreeDTest(SpecifiedThreeDGridScan): + # Skip parent validation for easier testing + def _check_lengths_are_same(self): # type: ignore + return self + @pytest.mark.parametrize( "y_starts_um, z_starts_um, omega_starts_deg, y_step_sizes_um, y_steps, should_raise", [ ([1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1], True), - ([1, 1], [1, 1], [1, 1], [1, 1, 1], [1, 1], False), - ([1, 1], [1, 1], [1, 1, 1], [1, 1], [1, 1], False), + ([1, 1], [1, 1], [1, 1], [1, 1, 1], [1, 1], True), + ([1, 1], [1, 1], [1, 1, 1], [1, 1], [1, 1], True), ([1, 1], [1, 1], [1, 1], [1, 1], [1, 1], False), ], ) @@ -91,19 +87,24 @@ def test_three_d_grid_scan_validation( y_steps: list[int], should_raise: bool, ): + def make_params(): + SpecifiedThreeDTest( + x_start_um=0, + y_starts_um=y_starts_um, + z_starts_um=z_starts_um, + omega_starts_deg=omega_starts_deg, + y_step_sizes_um=y_step_sizes_um, + y_steps=y_steps, + sample_id=0, + visit="/tmp", + parameter_model_version=get_param_version(), + file_name="/tmp", + storage_directory="/tmp", + x_steps=5, + ) + if should_raise: with pytest.raises(ValidationError, match="must be length 2 for 3D scans"): - SpecifiedThreeDGridScan( - x_start_um=0, - y_starts_um=y_starts_um, - z_starts_um=z_starts_um, - omega_starts_deg=omega_starts_deg, - y_step_sizes_um=y_step_sizes_um, - y_steps=y_steps, - sample_id=0, - visit="/tmp", - parameter_model_version=get_param_version(), - file_name="/tmp", - storage_directory="/tmp", - x_steps=5, - ) + make_params() + else: + make_params() From 49ffcd91d7020f78345538b2ca42a73b9990ecb5 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Thu, 19 Feb 2026 11:05:12 +0000 Subject: [PATCH 22/45] wip --- .../beamlines/i02_1/i02_1_gridscan_plan.py | 29 +++++++++++++ .../callbacks/common/ispyb_callback_base.py | 42 ++++++++++++------- 2 files changed, 57 insertions(+), 14 deletions(-) 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 fe19a767c5..7cf26ab848 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 @@ -14,6 +15,9 @@ from dodal.devices.s4_slit_gaps import S4SlitGaps from dodal.devices.undulator import BaseUndulator from dodal.devices.zebra.zebra import Zebra +from pydantic import BaseModel, PrivateAttr +from pydantic_extra_types.semantic_version import SemanticVersion +from semver import Version from mx_bluesky.beamlines.i02_1.device_setup_plans.setup_zebra import ( setup_zebra_for_gridscan, @@ -122,6 +126,31 @@ 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): + gda_parameter_version: SemanticVersion + visit: str + file_name: str + storage_directory: Path + 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 + sample_id: int | None = None + + # Internal parameter version compatible with this external model + _internal_param_version: str = PrivateAttr(default="6.0.0") + + def i02_1_gridscan_plan( parameters: SpecifiedTwoDGridScan, composite: FlyScanXRayCentreComposite = inject(""), 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 028e630a88..19a2b4ac35 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 @@ -129,25 +129,33 @@ 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( "ISPyB handler received event from read hardware" ) - synchrotron_mode = doc["data"]["synchrotron-synchrotron_mode"] + synchrotron_mode = _data["synchrotron-synchrotron_mode"] assert isinstance(synchrotron_mode, SynchrotronMode) hwscan_data_collection_info = DataCollectionInfo( - undulator_gap1=doc["data"]["undulator-current_gap"], + undulator_gap1=_data["undulator-current_gap"], synchrotron_mode=synchrotron_mode.value, - slitgap_horizontal=doc["data"]["s4_slit_gaps-xgap"], - slitgap_vertical=doc["data"]["s4_slit_gaps-ygap"], + slitgap_horizontal=_data["s4_slit_gaps-xgap"], + slitgap_vertical=_data["s4_slit_gaps-ygap"], ) hwscan_data_collection_info = _update_based_on_energy( doc, self.params.detector_params, hwscan_data_collection_info ) + + # VMXm doesn't have a gonio-y position, allow None + pos_y = _data.get("gonio-y", None) + if pos_y: + pos_y = float(pos_y) + 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=pos_y, + pos_z=float(_data["gonio-z"]), ) scan_data_infos = self.populate_info_for_update( hwscan_data_collection_info, hwscan_position_info, self.params @@ -160,18 +168,24 @@ 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["beamsize-x_um"] / 1000 + ) # todo beamsize for vmxm is complicated. Do hacky thing where beamsize is read from fake device until we make real one + beamsize_y_mm = _data["beamsize-y_um"] / 1000 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( From c212134f124cda2de79ac08125d6e20b2c5d1bc2 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Mon, 23 Feb 2026 17:10:58 +0000 Subject: [PATCH 23/45] wip --- .../i02_1/external_interaction/__init__.py | 0 .../callbacks/__init__.py | 0 .../callbacks/gridscan/__init__.py | 0 .../callbacks/gridscan/ispyb_callback.py | 315 ++++++++++++++++++ .../beamlines/i02_1/i02_1_gridscan_plan.py | 15 +- .../callbacks/common/ispyb_callback_base.py | 29 +- 6 files changed, 348 insertions(+), 11 deletions(-) create mode 100644 src/mx_bluesky/beamlines/i02_1/external_interaction/__init__.py create mode 100644 src/mx_bluesky/beamlines/i02_1/external_interaction/callbacks/__init__.py create mode 100644 src/mx_bluesky/beamlines/i02_1/external_interaction/callbacks/gridscan/__init__.py create mode 100644 src/mx_bluesky/beamlines/i02_1/external_interaction/callbacks/gridscan/ispyb_callback.py diff --git a/src/mx_bluesky/beamlines/i02_1/external_interaction/__init__.py b/src/mx_bluesky/beamlines/i02_1/external_interaction/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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..9859ba0ef1 --- /dev/null +++ b/src/mx_bluesky/beamlines/i02_1/external_interaction/callbacks/gridscan/ispyb_callback.py @@ -0,0 +1,315 @@ +from __future__ import annotations + +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.beamlines.i02_1.i02_1_gridscan_plan import I02_1FgsParams +from mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base import ( + BaseISPyBCallback, + D, +) +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.xray_centre.ispyb_callback import ( + GridscanPlane, + _smargon_omega_to_xyxz_plane, +) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping import ( + construct_comment_for_gridscan, +) +from mx_bluesky.common.external_interaction.ispyb.data_model import ( + DataCollectionGridInfo, + DataCollectionGroupInfo, + DataCollectionInfo, + DataCollectionPositionInfo, + Orientation, + 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.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="I02_1FgsParams") +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): + return bpp.set_run_key_wrapper( + bpp.run_wrapper( + plan_generator, + md={ + "activate_callbacks": ["GridscanISPyBCallback"], + "subplan_name": PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN, + "mx_bluesky_parameters": parameters.model_dump_json(), + }, + ), + PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN, + ) + + +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. + + 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_plane_to_id_map: dict[GridscanPlane, int] = {} + self._grid_plane_to_width_map: dict[GridscanPlane, int] = {} + self.data_collection_group_info: DataCollectionGroupInfo | None + + def activity_gated_start(self, doc: RunStart): + if doc.get("subplan_name") == PlanNameConstants.DO_FGS: + 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) + assert isinstance(self.params, I02_1FgsParams) + self.ispyb = StoreInIspyb(self.ispyb_config) + self.data_collection_group_info = populate_data_collection_group( + self.params + ) + + # todo fix this: define scan_data_infos here and then overwrite later + 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, + ) + ) + ) + + # todo make a function which populates all of this. "fill deposition with grid info" + + self.ispyb_ids = self.ispyb.begin_deposition( + self.data_collection_group_info, scan_data_infos + ) + # Use grid information given by GDA to complete ispyb info + scan_data_infos = self._get_scan_infos(doc) + self.ispyb_ids = self.ispyb.update_deposition( + self.ispyb_ids, scan_data_infos + ) + self.ispyb.update_data_collection_group_table( + self.data_collection_group_info, self.ispyb_ids.data_collection_group_id + ) + + 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): + 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 + ) + + def _get_scan_infos(self, doc) -> Sequence[ScanDataInfo]: + """ + so grid information is available immediately after the plan is triggered. + In contrast, i03 and i04 use the OAV to automatically detect their grid. + """ + 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]) + + # Don't need to do deal with the grid plane here since vmxm only do + # one plane, but leave it in so it's easier to standardise in the future + grid_plane = _smargon_omega_to_xyxz_plane(omega) + ISPYB_ZOCALO_CALLBACK_LOGGER.info( + f"Generating dc info for gridplane {grid_plane}, omega {omega}" + ) + data_collection_number = self.data_collection_number_from_gridplane( + grid_plane + ) + 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 + ) + + # Grid plane logic isn't needed for VMX, but keep it for now anyway + data_collection_id = self.ispyb_ids.data_collection_ids[ + 0 if grid_plane == GridscanPlane.OMEGA_XY else 1 + ] + self._grid_plane_to_id_map[grid_plane] = data_collection_id + self._grid_plane_to_width_map[grid_plane] = ( + data_collection_grid_info.steps_y + ) + + y_steps = self._grid_plane_to_width_map.get(GridscanPlane.OMEGA_XY, "_") + self.data_collection_group_info.comments = ( + f"Diffraction grid scan of {data_collection_grid_info.steps_x} by " + f"{y_steps}." + ) + + self._populate_axis_info(data_collection_info, doc["data"]) + + # todo do all this stuff as soon as possible after plan starts + + 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 + + def _populate_axis_info(self, data_collection_info: DataCollectionInfo, doc: dict): + if (omega_start := doc.get("gonio-omega")) is not None: + omega_in_gda_space = -omega_start + 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 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, I02_1FgsParams) + scan_data_infos = [] + for grid_num in range(self.params.num_grids): + scan_data_info = ScanDataInfo( + data_collection_info=event_sourced_data_collection_info, + data_collection_id=self.ispyb_ids.data_collection_ids[grid_num], + ) + 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.GRID_DETECT_AND_DO_GRIDSCAN} 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 + self._grid_plane_to_id_map.clear() + self._grid_plane_to_width_map.clear() + 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_plane_to_id_map: + doc["grid_plane_to_id_map"] = self._grid_plane_to_id_map + return doc # type: ignore + + def data_collection_number_from_gridplane(self, plane) -> int: + assert self.params + base_number = self.params.detector_params.run_number + return base_number if plane == GridscanPlane.OMEGA_XY else base_number + 1 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 7cf26ab848..fec811f46c 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 @@ -151,8 +151,21 @@ class ExternalGridScanParams(BaseModel): _internal_param_version: str = PrivateAttr(default="6.0.0") +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 + + def i02_1_gridscan_plan( - parameters: SpecifiedTwoDGridScan, + parameters: I02_1FgsParams, composite: FlyScanXRayCentreComposite = inject(""), ) -> MsgGenerator: """BlueAPI entry point for i02-1 grid scans""" 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 19a2b4ac35..a6ef1989d5 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 @@ -9,6 +9,7 @@ from dodal.devices.detector.det_resolution import resolution from dodal.devices.synchrotron import SynchrotronMode +from mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan import I02_1FgsParams from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import ( PlanReactiveCallback, ) @@ -147,14 +148,9 @@ def _handle_ispyb_hardware_read(self, doc) -> Sequence[ScanDataInfo]: doc, self.params.detector_params, hwscan_data_collection_info ) - # VMXm doesn't have a gonio-y position, allow None - pos_y = _data.get("gonio-y", None) - if pos_y: - pos_y = float(pos_y) - hwscan_position_info = DataCollectionPositionInfo( pos_x=float(_data["gonio-x"]), - pos_y=pos_y, + pos_y=float(_data.get("gonio-y")), pos_z=float(_data["gonio-z"]), ) scan_data_infos = self.populate_info_for_update( @@ -174,10 +170,23 @@ def _handle_ispyb_transmission_flux_read( aperture = _data.get( "aperture_scatterguard-selected_aperture", "Not implemented" ) - beamsize_x_mm = ( - _data["beamsize-x_um"] / 1000 - ) # todo beamsize for vmxm is complicated. Do hacky thing where beamsize is read from fake device until we make real one - beamsize_y_mm = _data["beamsize-y_um"] / 1000 + 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): + # todo write issue about vmxm beamsize and link here + try: + assert isinstance(self.params, I02_1FgsParams) + beamsize_x_mm = self.params.beam_size_x + beamsize_y_mm = self.params.beam_size_y + except AssertionError: + 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, From eb47c555f20044436d31c6cd609ee1c726c1d6b7 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Wed, 25 Feb 2026 17:07:06 +0000 Subject: [PATCH 24/45] wip --- src/mx_bluesky/beamlines/i02_1/composites.py | 16 + .../callbacks/gridscan/ispyb_callback.py | 315 ----------------- .../beamlines/i02_1/i02_1_gridscan_plan.py | 53 +-- .../i04_grid_detect_then_xray_centre_plan.py | 10 +- ...ommon_grid_detect_then_xray_centre_plan.py | 2 +- .../callbacks/{xray_centre => }/__init__.py | 0 .../callbacks/common/ispyb_callback_base.py | 8 +- .../callbacks/common/ispyb_mapping.py | 2 +- .../callbacks/grid/__init__.py | 0 .../grid/grid_detect_and_scan/__init__.py | 0 .../grid_detect_and_scan}/ispyb_callback.py | 9 +- .../grid_detect_and_scan}/ispyb_mapping.py | 0 .../grid_detect_and_scan}/nexus_callback.py | 0 .../callbacks/grid/gridscan/__init__.py | 0 .../callbacks/grid/gridscan/ispyb_callback.py | 320 ++++++++++++++++++ .../pin_centre_then_gridscan_plan.py | 2 +- .../callbacks/__main__.py | 16 +- tests/conftest.py | 2 +- .../callbacks/test_external_callbacks.py | 2 +- .../test_ispyb_dev_connection.py | 10 +- .../test_load_centre_collect_full_plan.py | 32 +- .../test_zocalo_system.py | 2 +- .../inner_plans/test_do_fgs.py | 2 +- .../test_common_flyscan_xray_centre_plan.py | 20 +- ...ommon_grid_detect_then_xray_centre_plan.py | 6 +- .../test_grid_detection_plan.py | 8 +- .../callbacks/test_zocalo_handler.py | 8 +- .../xray_centre/test_ispyb_callback.py | 24 +- .../xray_centre/test_ispyb_handler.py | 18 +- .../xray_centre/test_ispyb_mapping.py | 4 +- .../xray_centre/test_nexus_handler.py | 8 +- tests/unit_tests/conftest.py | 22 +- .../hyperion/experiment_plans/conftest.py | 6 +- .../test_hyperion_flyscan_xray_centre_plan.py | 12 +- .../test_pin_centre_then_xray_centre_plan.py | 2 +- .../nexus/test_write_nexus.py | 2 +- 36 files changed, 493 insertions(+), 450 deletions(-) create mode 100644 src/mx_bluesky/beamlines/i02_1/composites.py rename src/mx_bluesky/common/external_interaction/callbacks/{xray_centre => }/__init__.py (100%) create mode 100644 src/mx_bluesky/common/external_interaction/callbacks/grid/__init__.py create mode 100644 src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/__init__.py rename src/mx_bluesky/common/external_interaction/callbacks/{xray_centre => grid/grid_detect_and_scan}/ispyb_callback.py (97%) rename src/mx_bluesky/common/external_interaction/callbacks/{xray_centre => grid/grid_detect_and_scan}/ispyb_mapping.py (100%) rename src/mx_bluesky/common/external_interaction/callbacks/{xray_centre => grid/grid_detect_and_scan}/nexus_callback.py (100%) create mode 100644 src/mx_bluesky/common/external_interaction/callbacks/grid/gridscan/__init__.py create mode 100644 src/mx_bluesky/common/external_interaction/callbacks/grid/gridscan/ispyb_callback.py 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/beamlines/i02_1/external_interaction/callbacks/gridscan/ispyb_callback.py b/src/mx_bluesky/beamlines/i02_1/external_interaction/callbacks/gridscan/ispyb_callback.py index 9859ba0ef1..e69de29bb2 100644 --- 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 @@ -1,315 +0,0 @@ -from __future__ import annotations - -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.beamlines.i02_1.i02_1_gridscan_plan import I02_1FgsParams -from mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base import ( - BaseISPyBCallback, - D, -) -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.xray_centre.ispyb_callback import ( - GridscanPlane, - _smargon_omega_to_xyxz_plane, -) -from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping import ( - construct_comment_for_gridscan, -) -from mx_bluesky.common.external_interaction.ispyb.data_model import ( - DataCollectionGridInfo, - DataCollectionGroupInfo, - DataCollectionInfo, - DataCollectionPositionInfo, - Orientation, - 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.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="I02_1FgsParams") -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): - return bpp.set_run_key_wrapper( - bpp.run_wrapper( - plan_generator, - md={ - "activate_callbacks": ["GridscanISPyBCallback"], - "subplan_name": PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN, - "mx_bluesky_parameters": parameters.model_dump_json(), - }, - ), - PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN, - ) - - -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. - - 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_plane_to_id_map: dict[GridscanPlane, int] = {} - self._grid_plane_to_width_map: dict[GridscanPlane, int] = {} - self.data_collection_group_info: DataCollectionGroupInfo | None - - def activity_gated_start(self, doc: RunStart): - if doc.get("subplan_name") == PlanNameConstants.DO_FGS: - 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) - assert isinstance(self.params, I02_1FgsParams) - self.ispyb = StoreInIspyb(self.ispyb_config) - self.data_collection_group_info = populate_data_collection_group( - self.params - ) - - # todo fix this: define scan_data_infos here and then overwrite later - 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, - ) - ) - ) - - # todo make a function which populates all of this. "fill deposition with grid info" - - self.ispyb_ids = self.ispyb.begin_deposition( - self.data_collection_group_info, scan_data_infos - ) - # Use grid information given by GDA to complete ispyb info - scan_data_infos = self._get_scan_infos(doc) - self.ispyb_ids = self.ispyb.update_deposition( - self.ispyb_ids, scan_data_infos - ) - self.ispyb.update_data_collection_group_table( - self.data_collection_group_info, self.ispyb_ids.data_collection_group_id - ) - - 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): - 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 - ) - - def _get_scan_infos(self, doc) -> Sequence[ScanDataInfo]: - """ - so grid information is available immediately after the plan is triggered. - In contrast, i03 and i04 use the OAV to automatically detect their grid. - """ - 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]) - - # Don't need to do deal with the grid plane here since vmxm only do - # one plane, but leave it in so it's easier to standardise in the future - grid_plane = _smargon_omega_to_xyxz_plane(omega) - ISPYB_ZOCALO_CALLBACK_LOGGER.info( - f"Generating dc info for gridplane {grid_plane}, omega {omega}" - ) - data_collection_number = self.data_collection_number_from_gridplane( - grid_plane - ) - 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 - ) - - # Grid plane logic isn't needed for VMX, but keep it for now anyway - data_collection_id = self.ispyb_ids.data_collection_ids[ - 0 if grid_plane == GridscanPlane.OMEGA_XY else 1 - ] - self._grid_plane_to_id_map[grid_plane] = data_collection_id - self._grid_plane_to_width_map[grid_plane] = ( - data_collection_grid_info.steps_y - ) - - y_steps = self._grid_plane_to_width_map.get(GridscanPlane.OMEGA_XY, "_") - self.data_collection_group_info.comments = ( - f"Diffraction grid scan of {data_collection_grid_info.steps_x} by " - f"{y_steps}." - ) - - self._populate_axis_info(data_collection_info, doc["data"]) - - # todo do all this stuff as soon as possible after plan starts - - 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 - - def _populate_axis_info(self, data_collection_info: DataCollectionInfo, doc: dict): - if (omega_start := doc.get("gonio-omega")) is not None: - omega_in_gda_space = -omega_start - 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 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, I02_1FgsParams) - scan_data_infos = [] - for grid_num in range(self.params.num_grids): - scan_data_info = ScanDataInfo( - data_collection_info=event_sourced_data_collection_info, - data_collection_id=self.ispyb_ids.data_collection_ids[grid_num], - ) - 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.GRID_DETECT_AND_DO_GRIDSCAN} 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 - self._grid_plane_to_id_map.clear() - self._grid_plane_to_width_map.clear() - 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_plane_to_id_map: - doc["grid_plane_to_id_map"] = self._grid_plane_to_id_map - return doc # type: ignore - - def data_collection_number_from_gridplane(self, plane) -> int: - assert self.params - base_number = self.params.detector_params.run_number - return base_number if plane == GridscanPlane.OMEGA_XY else base_number + 1 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 fec811f46c..42645ffd53 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 @@ -19,6 +19,7 @@ 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, @@ -31,14 +32,15 @@ ) from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import ( ZocaloCallback, + ZocaloInfoGenerator, + ZocaloStartInfo, ) -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 ( + GridscanISPyBCallback, +) from mx_bluesky.common.parameters.constants import ( EnvironmentConstants, PlanNameConstants, @@ -47,8 +49,30 @@ FlyScanEssentialDevices, GonioWithOmegaType, ) -from mx_bluesky.common.parameters.gridscan import GenericGrid from mx_bluesky.common.utils.log import LOGGER +from mx_bluesky.common.utils.utils import number_of_frames_from_scan_spec + + +# todo this all needs chanigng +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([0, 90]): + 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 create_gridscan_callbacks() -> tuple[ @@ -57,11 +81,11 @@ def create_gridscan_callbacks() -> tuple[ 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, + generate_start_info_from_omega_map, # todo dont need this ), ), ) @@ -151,19 +175,6 @@ class ExternalGridScanParams(BaseModel): _internal_param_version: str = PrivateAttr(default="6.0.0") -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 - - def i02_1_gridscan_plan( parameters: I02_1FgsParams, composite: FlyScanXRayCentreComposite = inject(""), 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 68a8450a49..305fac78d2 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,11 +62,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, 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.parameters.components import get_param_version @@ -270,11 +270,11 @@ 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, 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 2bf71cdf0c..3d9a94dc3e 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 @@ -31,7 +31,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/external_interaction/callbacks/xray_centre/__init__.py b/src/mx_bluesky/common/external_interaction/callbacks/__init__.py similarity index 100% rename from src/mx_bluesky/common/external_interaction/callbacks/xray_centre/__init__.py rename to src/mx_bluesky/common/external_interaction/callbacks/__init__.py 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 a6ef1989d5..5de9ea845b 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 @@ -9,7 +9,6 @@ from dodal.devices.detector.det_resolution import resolution from dodal.devices.synchrotron import SynchrotronMode -from mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan import I02_1FgsParams from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import ( PlanReactiveCallback, ) @@ -179,10 +178,9 @@ def _handle_ispyb_transmission_flux_read( if not (beamsize_x_mm and beamsize_y_mm): # todo write issue about vmxm beamsize and link here try: - assert isinstance(self.params, I02_1FgsParams) - beamsize_x_mm = self.params.beam_size_x - beamsize_y_mm = self.params.beam_size_y - except AssertionError: + 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" ) 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 97% 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 ebfcd270b0..d0c4025ec3 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 @@ -21,7 +21,7 @@ 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.ispyb.data_model import ( @@ -66,7 +66,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(), }, @@ -78,12 +78,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) 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..b7e6d6a893 --- /dev/null +++ b/src/mx_bluesky/common/external_interaction/callbacks/grid/gridscan/ispyb_callback.py @@ -0,0 +1,320 @@ +from __future__ import annotations + +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.beamlines.i02_1.composites import I02_1FgsParams +from mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base import ( + BaseISPyBCallback, + D, +) +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.grid_detect_and_scan.ispyb_callback import ( + GridscanPlane, + _smargon_omega_to_xyxz_plane, +) +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, + DataCollectionPositionInfo, + Orientation, + 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="I02_1FgsParams") +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): + return bpp.set_run_key_wrapper( + bpp.run_wrapper( + plan_generator, + md={ + "activate_callbacks": ["GridDetectAndScanISPyBCallback"], + "subplan_name": PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN, + "mx_bluesky_parameters": parameters.model_dump_json(), + }, + ), + PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN, + ) + + +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. + + 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_plane_to_id_map: dict[GridscanPlane, int] = {} + self._grid_plane_to_width_map: dict[GridscanPlane, int] = {} + self.data_collection_group_info: DataCollectionGroupInfo | None + + def activity_gated_start(self, doc: RunStart): + if doc.get("subplan_name") == PlanNameConstants.DO_FGS: + 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): + 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 + ) + + def _get_scan_infos(self, doc) -> Sequence[ScanDataInfo]: + """ + so grid information is available immediately after the plan is triggered. + In contrast, i03 and i04 use the OAV to automatically detect their grid. + """ + 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]) + + # Don't need to do deal with the grid plane here since vmxm only do + # one plane, but leave it in so it's easier to standardise in the future + grid_plane = _smargon_omega_to_xyxz_plane(omega) + ISPYB_ZOCALO_CALLBACK_LOGGER.info( + f"Generating dc info for gridplane {grid_plane}, omega {omega}" + ) + data_collection_number = self.data_collection_number_from_gridplane( + grid_plane + ) + file_template = f"{self.params.detector_params.prefix}_{data_collection_number}_master.h5" + # Snapshots have already been taken in GDA + + # todo the _get_scan_infos here should be vmxm specific since they require us asserting that params includes path_to_xtal_snapshot + # and do this by asserting params are vmxm params + + 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 + ) + + # Grid plane logic isn't needed for VMX, but keep it for now anyway + data_collection_id = self.ispyb_ids.data_collection_ids[ + 0 if grid_plane == GridscanPlane.OMEGA_XY else 1 + ] + self._grid_plane_to_id_map[grid_plane] = data_collection_id + self._grid_plane_to_width_map[grid_plane] = ( + data_collection_grid_info.steps_y + ) + + y_steps = self._grid_plane_to_width_map.get(GridscanPlane.OMEGA_XY, "_") + self.data_collection_group_info.comments = ( + f"Diffraction grid scan of {data_collection_grid_info.steps_x} by " + f"{y_steps}." + ) + + self._populate_axis_info(data_collection_info, doc["data"]) + + # todo do all this stuff as soon as possible after plan starts + + 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 + + def _populate_axis_info(self, data_collection_info: DataCollectionInfo, doc: dict): + if (omega_start := doc.get("gonio-omega")) is not None: + omega_in_gda_space = -omega_start + 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 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, I02_1FgsParams) + scan_data_infos = [] + for grid_num in range(self.params.num_grids): + scan_data_info = ScanDataInfo( + data_collection_info=event_sourced_data_collection_info, + data_collection_id=self.ispyb_ids.data_collection_ids[grid_num], + ) + 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.GRID_DETECT_AND_DO_GRIDSCAN} 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 + self._grid_plane_to_id_map.clear() + self._grid_plane_to_width_map.clear() + 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_plane_to_id_map: + doc["grid_plane_to_id_map"] = self._grid_plane_to_id_map + return doc # type: ignore + + def data_collection_number_from_gridplane(self, plane) -> int: + assert self.params + base_number = self.params.detector_params.run_number + return base_number if plane == GridscanPlane.OMEGA_XY else base_number + 1 + + 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[ScanDataInfo] = list(make_scan_infos_with_grid_info()) + self.ispyb_ids = self.ispyb.update_deposition(self.ispyb_ids, scan_data_infos) + 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/hyperion/experiment_plans/pin_centre_then_gridscan_plan.py b/src/mx_bluesky/hyperion/experiment_plans/pin_centre_then_gridscan_plan.py index 8d407b4827..43133e6d7a 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 @@ -19,7 +19,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 2f6696466e..cad6482812 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py @@ -25,16 +25,16 @@ 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.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, +from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( + GridDetectAndScanISPyBCallback, 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.sample_handling.sample_handling_callback import ( + SampleHandlingCallback, +) from mx_bluesky.common.utils.log import ( ISPYB_ZOCALO_CALLBACK_LOGGER, NEXUS_LOGGER, @@ -75,11 +75,11 @@ def create_gridscan_callbacks() -> tuple[ - GridscanNexusFileCallback, GridscanISPyBCallback + GridscanNexusFileCallback, GridDetectAndScanISPyBCallback ]: return ( GridscanNexusFileCallback(param_type=HyperionSpecifiedThreeDGridScan), - GridscanISPyBCallback( + GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams, emit=ZocaloCallback( CONST.PLAN.DO_FGS, CONST.ZOCALO_ENV, generate_start_info_from_omega_map diff --git a/tests/conftest.py b/tests/conftest.py index f91f9447e8..de0b1b9dec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -90,7 +90,7 @@ from mx_bluesky.beamlines.i04.external_interaction.config_server import ( get_i04_config_client, ) -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 dc7152ebd1..2d626df06d 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 @@ -27,12 +27,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, @@ -294,7 +294,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() @@ -473,7 +473,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() @@ -565,7 +565,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() @@ -599,7 +599,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() @@ -651,7 +651,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() @@ -683,7 +683,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() @@ -741,7 +741,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() @@ -775,7 +775,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() @@ -857,7 +857,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() @@ -910,7 +910,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() @@ -976,7 +976,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() @@ -1033,7 +1033,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() @@ -1167,7 +1167,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/common/experiment_plans/inner_plans/test_do_fgs.py b/tests/unit_tests/common/experiment_plans/inner_plans/test_do_fgs.py index 921ed85540..23f75acd3a 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 dcb4b8f8d6..ea516e365e 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 @@ -319,7 +321,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, ): @@ -342,7 +344,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)] @@ -576,7 +578,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 38cba4d938..83390b533b 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 @@ -24,7 +24,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 ( @@ -267,7 +267,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 @@ -340,7 +340,7 @@ def test_grid_detect_then_xray_centre_activates_ispyb_callback( assert_message_and_return_remaining( 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 3f3b526763..e6c181e27b 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 @@ -26,8 +26,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 @@ -218,7 +218,7 @@ async def test_when_grid_detection_plan_run_then_ispyb_callback_gets_correct_val ): params = OAVParameters("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) @@ -232,7 +232,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/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 b1c9d67e69..0a3c62112d 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..d967e716f5 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,13 @@ 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", + "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback.time", side_effect=[2, 100], ) 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 ): - 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/conftest.py b/tests/unit_tests/conftest.py index e8a66b1dbb..f373841b3a 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -49,11 +49,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, 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.ispyb.data_model import ( @@ -170,11 +170,11 @@ 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, @@ -210,7 +210,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( @@ -227,13 +227,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) @@ -271,7 +275,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 b4d98e96af..30a98d21b9 100644 --- a/tests/unit_tests/hyperion/experiment_plans/conftest.py +++ b/tests/unit_tests/hyperion/experiment_plans/conftest.py @@ -110,10 +110,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 @@ -121,7 +121,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 d2c2f6aae1..286cfc7d37 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 369f1c52d8..d873531075 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 @@ -149,7 +149,7 @@ def test_pin_centre_then_gridscan_plan_activates_ispyb_callback_before_pin_tip_c msgs = assert_message_and_return_remaining( 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( msgs, lambda msg: msg.command == "pin_tip_centre_plan" 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 ( From f41d162256073d8d209a5d86ccd699fda05aa37e Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Fri, 27 Feb 2026 14:48:32 +0000 Subject: [PATCH 25/45] Make common ispyb callback for gridscans with no grid detect --- .../callbacks/gridscan/ispyb_callback.py | 85 ++++++++++ .../beamlines/i02_1/i02_1_gridscan_plan.py | 42 ++--- .../i04_grid_detect_then_xray_centre_plan.py | 23 ++- .../callbacks/common/ispyb_callback_base.py | 2 +- .../grid_detect_and_scan/ispyb_callback.py | 57 ++----- .../callbacks/grid/gridscan/ispyb_callback.py | 156 ++++-------------- .../callbacks/grid/utils.py | 73 ++++++++ .../common/parameters/components.py | 2 +- src/mx_bluesky/common/parameters/constants.py | 1 + src/mx_bluesky/common/parameters/gridscan.py | 2 +- .../callbacks/__main__.py | 14 +- 11 files changed, 236 insertions(+), 221 deletions(-) create mode 100644 src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py 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 index e69de29bb2..0c6830078b 100644 --- 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 @@ -0,0 +1,85 @@ +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, +) +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 + + +class I021GridscanISPyBCallback(GridscanISPyBCallback): + 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 = ( + f"Diffraction grid scan of {data_collection_grid_info.steps_x} by " + f"{self.params.y_steps}." + ) + + 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 42645ffd53..3afa851898 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 @@ -32,8 +32,6 @@ ) from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import ( ZocaloCallback, - ZocaloInfoGenerator, - ZocaloStartInfo, ) from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.nexus_callback import ( GridscanNexusFileCallback, @@ -41,6 +39,9 @@ from mx_bluesky.common.external_interaction.callbacks.grid.gridscan.ispyb_callback import ( GridscanISPyBCallback, ) +from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( + generate_start_info_from_omega_map, +) from mx_bluesky.common.parameters.constants import ( EnvironmentConstants, PlanNameConstants, @@ -50,34 +51,11 @@ GonioWithOmegaType, ) from mx_bluesky.common.utils.log import LOGGER -from mx_bluesky.common.utils.utils import number_of_frames_from_scan_spec - - -# todo this all needs chanigng -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([0, 90]): - 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 create_gridscan_callbacks() -> tuple[ - GridscanNexusFileCallback, GridscanISPyBCallback -]: + + +def create_gridscan_callbacks( + params: I02_1FgsParams, +) -> tuple[GridscanNexusFileCallback, GridscanISPyBCallback]: return ( GridscanNexusFileCallback(param_type=SpecifiedTwoDGridScan), GridscanISPyBCallback( @@ -85,7 +63,7 @@ def create_gridscan_callbacks() -> tuple[ emit=ZocaloCallback( PlanNameConstants.DO_FGS, EnvironmentConstants.ZOCALO_ENV, - generate_start_info_from_omega_map, # todo dont need this + lambda: generate_start_info_from_omega_map(params.omega_starts_deg), ), ), ) @@ -182,7 +160,7 @@ def i02_1_gridscan_plan( """BlueAPI entry point for i02-1 grid scans""" beamline_specific = construct_i02_1_specific_features(composite, parameters) - callbacks = create_gridscan_callbacks() + callbacks = create_gridscan_callbacks(parameters) @bpp.subs_decorator(callbacks) def 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 305fac78d2..2fb0518a2f 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 @@ -64,14 +64,17 @@ ) from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( GridDetectAndScanISPyBCallback, - generate_start_info_from_omega_map, ) 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, @@ -81,6 +84,7 @@ ) from mx_bluesky.common.parameters.gridscan import ( GenericGrid, + SpecifiedGrids, SpecifiedThreeDGridScan, ) from mx_bluesky.common.preprocessors.preprocessors import ( @@ -208,7 +212,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(grid_common_params.specified_grid_params), + flyscan_event_handler, + ) @bpp.subs_decorator(callbacks) @verify_undulator_gap_before_run_decorator(composite) @@ -269,17 +276,19 @@ def get_ready_for_oav_and_close_shutter( yield from bps.wait(group) -def create_gridscan_callbacks() -> tuple[ - GridscanNexusFileCallback, GridDetectAndScanISPyBCallback -]: +def create_gridscan_callbacks( + params: SpecifiedGrids | None, +) -> tuple[GridscanNexusFileCallback, GridDetectAndScanISPyBCallback]: return ( GridscanNexusFileCallback(param_type=SpecifiedThreeDGridScan), GridDetectAndScanISPyBCallback( - param_type=GenericGrid, + 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] + ), ), ), ) 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 5de9ea845b..2860891d8a 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 @@ -176,7 +176,7 @@ def _handle_ispyb_transmission_flux_read( if beamsize_y_mm: beamsize_y_mm = beamsize_y_mm / 1000 if not (beamsize_x_mm and beamsize_y_mm): - # todo write issue about vmxm beamsize and link here + # 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 diff --git a/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py index d0c4025ec3..8dca22d8bd 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py @@ -8,7 +8,6 @@ from bluesky import preprocessors as bpp from bluesky.utils import MsgGenerator, make_decorator -from dodal.devices.zocalo import ZocaloStartInfo from mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base import ( BaseISPyBCallback, @@ -18,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.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, + add_processing_time_to_comment, + common_populate_axis_info, +) from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionGridInfo, DataCollectionGroupInfo, @@ -38,13 +39,12 @@ ) from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample from mx_bluesky.common.parameters.constants import DocDescriptorNames, PlanNameConstants -from mx_bluesky.common.parameters.gridscan import GenericGrid +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 -from mx_bluesky.common.utils.utils import number_of_frames_from_scan_spec OMEGA_TOLERANCE = 1 @@ -57,8 +57,7 @@ class GridscanPlane(StrEnum): if TYPE_CHECKING: 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" +T = TypeVar("T", bound="SpecifiedGrids") def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters): @@ -173,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 + add_processing_time_to_comment( + self, processing_start_time, self.data_collection_group_info ) def _handle_oav_grid_snapshot_triggered(self, doc) -> Sequence[ScanDataInfo]: @@ -258,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 (omega_start := doc.get("gonio-omega")) is not None: - omega_in_gda_space = -omega_start - 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, @@ -340,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) 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 index b7e6d6a893..05d70296d2 100644 --- 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 @@ -1,5 +1,6 @@ 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 @@ -7,10 +8,8 @@ from bluesky import preprocessors as bpp from bluesky.utils import MsgGenerator, make_decorator -from mx_bluesky.beamlines.i02_1.composites import I02_1FgsParams from mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base import ( BaseISPyBCallback, - D, ) from mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping import ( populate_data_collection_group, @@ -18,17 +17,15 @@ ) from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( GridscanPlane, - _smargon_omega_to_xyxz_plane, ) -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 ( + add_processing_time_to_comment, + common_populate_axis_info, ) from mx_bluesky.common.external_interaction.ispyb.data_model import ( - DataCollectionGridInfo, DataCollectionGroupInfo, DataCollectionInfo, DataCollectionPositionInfo, - Orientation, ScanDataInfo, ) from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( @@ -47,8 +44,7 @@ if TYPE_CHECKING: from event_model import RunStart, RunStop -T = TypeVar("T", bound="I02_1FgsParams") -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" +T = TypeVar("T", bound="SpecifiedGrids") def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters): @@ -56,12 +52,12 @@ def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters): bpp.run_wrapper( plan_generator, md={ - "activate_callbacks": ["GridDetectAndScanISPyBCallback"], - "subplan_name": PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN, + "activate_callbacks": ["GridscanISPyBCallback"], + "subplan_name": PlanNameConstants.TRIGGER_GRIDSCAN_ISPYB_CALLBACK, "mx_bluesky_parameters": parameters.model_dump_json(), }, ), - PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN, + PlanNameConstants.TRIGGER_GRIDSCAN_ISPYB_CALLBACK, ) @@ -74,6 +70,9 @@ class GridscanISPyBCallback(BaseISPyBCallback): 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) @@ -116,113 +115,20 @@ def activity_gated_start(self, doc: RunStart): return super().activity_gated_start(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 + add_processing_time_to_comment( + self, processing_start_time, self.data_collection_group_info ) - def _get_scan_infos(self, doc) -> Sequence[ScanDataInfo]: - """ - so grid information is available immediately after the plan is triggered. - In contrast, i03 and i04 use the OAV to automatically detect their grid. - """ - 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]) - - # Don't need to do deal with the grid plane here since vmxm only do - # one plane, but leave it in so it's easier to standardise in the future - grid_plane = _smargon_omega_to_xyxz_plane(omega) - ISPYB_ZOCALO_CALLBACK_LOGGER.info( - f"Generating dc info for gridplane {grid_plane}, omega {omega}" - ) - data_collection_number = self.data_collection_number_from_gridplane( - grid_plane - ) - file_template = f"{self.params.detector_params.prefix}_{data_collection_number}_master.h5" - # Snapshots have already been taken in GDA - - # todo the _get_scan_infos here should be vmxm specific since they require us asserting that params includes path_to_xtal_snapshot - # and do this by asserting params are vmxm params + @abstractmethod + def _get_scan_infos(self, doc) -> Sequence[ScanDataInfo]: ... - 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 - ) - - # Grid plane logic isn't needed for VMX, but keep it for now anyway - data_collection_id = self.ispyb_ids.data_collection_ids[ - 0 if grid_plane == GridscanPlane.OMEGA_XY else 1 - ] - self._grid_plane_to_id_map[grid_plane] = data_collection_id - self._grid_plane_to_width_map[grid_plane] = ( - data_collection_grid_info.steps_y - ) - - y_steps = self._grid_plane_to_width_map.get(GridscanPlane.OMEGA_XY, "_") - self.data_collection_group_info.comments = ( - f"Diffraction grid scan of {data_collection_grid_info.steps_x} by " - f"{y_steps}." - ) - - self._populate_axis_info(data_collection_info, doc["data"]) - - # todo do all this stuff as soon as possible after plan starts - - 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 + """ + 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): - if (omega_start := doc.get("gonio-omega")) is not None: - omega_in_gda_space = -omega_start - 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, @@ -233,7 +139,7 @@ def populate_info_for_update( assert self.ispyb_ids.data_collection_ids, ( "Expect at least one valid data collection to record scan data" ) - assert isinstance(self.params, I02_1FgsParams) + assert isinstance(self.params, SpecifiedGrids) scan_data_infos = [] for grid_num in range(self.params.num_grids): scan_data_info = ScanDataInfo( @@ -271,22 +177,12 @@ def activity_gated_stop(self, doc: RunStop) -> RunStop: self.ispyb_ids.data_collection_group_id, ) self.data_collection_group_info = None - self._grid_plane_to_id_map.clear() - self._grid_plane_to_width_map.clear() 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_plane_to_id_map: - doc["grid_plane_to_id_map"] = self._grid_plane_to_id_map - return doc # type: ignore - def data_collection_number_from_gridplane(self, plane) -> int: assert self.params - base_number = self.params.detector_params.run_number - return base_number if plane == GridscanPlane.OMEGA_XY else base_number + 1 + return self.params.detector_params.run_number def fill_gridscan_deposition_and_store( self, make_scan_infos_with_grid_info: Callable[..., Sequence[ScanDataInfo]] @@ -313,8 +209,12 @@ def fill_gridscan_deposition_and_store( self.data_collection_group_info, scan_data_infos ) # Now use grid information to complete deposition - scan_data_infos: list[ScanDataInfo] = list(make_scan_infos_with_grid_info()) - self.ispyb_ids = self.ispyb.update_deposition(self.ispyb_ids, scan_data_infos) + 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..45f98cb71d --- /dev/null +++ b/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from time import time + +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.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 common_populate_axis_info(data_collection_info: DataCollectionInfo, doc: dict): + if (omega_start := doc.get("gonio-omega")) is not None: + omega_in_gda_space = -omega_start + 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 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..1c54bf9a00 100644 --- a/src/mx_bluesky/common/parameters/components.py +++ b/src/mx_bluesky/common/parameters/components.py @@ -271,7 +271,7 @@ 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] + 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 158dbe28af..a7961b1c6e 100644 --- a/src/mx_bluesky/common/parameters/constants.py +++ b/src/mx_bluesky/common/parameters/constants.py @@ -71,6 +71,7 @@ class PlanNameConstants: GRIDSCAN_OUTER = "run_gridscan_move_and_tidy" DO_FGS = "do_fgs" FLYSCAN_RESULTS = "xray_centre_results" + 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 71469e56eb..e6bebfa63e 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/external_interaction/callbacks/__main__.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py index cad6482812..297446fe56 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py @@ -27,14 +27,17 @@ ) from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( GridDetectAndScanISPyBCallback, - generate_start_info_from_omega_map, ) 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.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, @@ -63,7 +66,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, ) @@ -80,9 +82,13 @@ def create_gridscan_callbacks() -> tuple[ return ( GridscanNexusFileCallback(param_type=HyperionSpecifiedThreeDGridScan), GridDetectAndScanISPyBCallback( - param_type=GenericGridWithHyperionDetectorParams, + 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] + ), ), ), ) From 5a0d9f30d89b1041d52dd28324fefa1a4c42b99d Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Fri, 27 Feb 2026 16:11:43 +0000 Subject: [PATCH 26/45] Fixes --- pyproject.toml | 2 +- .../beamlines/i02_1/i02_1_gridscan_plan.py | 4 +-- .../i04_grid_detect_then_xray_centre_plan.py | 9 +++--- .../grid_detect_and_scan/ispyb_callback.py | 4 +-- .../callbacks/grid/gridscan/ispyb_callback.py | 18 ++++++++---- .../callbacks/grid/utils.py | 28 +++++++++++++++++++ .../xray_centre/test_ispyb_handler.py | 8 ++++-- tests/unit_tests/conftest.py | 9 ++++-- 8 files changed, 62 insertions(+), 20 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9b8fedcbe5..07bc7310d0 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/i02_1_gridscan_plan.py b/src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py index 3afa851898..702f16de97 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 @@ -40,7 +40,7 @@ GridscanISPyBCallback, ) from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( - generate_start_info_from_omega_map, + generate_start_info_from_num_grids, ) from mx_bluesky.common.parameters.constants import ( EnvironmentConstants, @@ -63,7 +63,7 @@ def create_gridscan_callbacks( emit=ZocaloCallback( PlanNameConstants.DO_FGS, EnvironmentConstants.ZOCALO_ENV, - lambda: generate_start_info_from_omega_map(params.omega_starts_deg), + lambda: generate_start_info_from_num_grids(params), ), ), ) 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 2fb0518a2f..ae11e4c232 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 @@ -84,7 +84,6 @@ ) from mx_bluesky.common.parameters.gridscan import ( GenericGrid, - SpecifiedGrids, SpecifiedThreeDGridScan, ) from mx_bluesky.common.preprocessors.preprocessors import ( @@ -213,7 +212,7 @@ def _inner_grid_detect_then_xrc(): # https://github.com/DiamondLightSource/mx-bluesky/issues/1117 flyscan_event_handler = XRayCentreEventHandler() callbacks = ( - *create_gridscan_callbacks(grid_common_params.specified_grid_params), + *create_gridscan_callbacks(), flyscan_event_handler, ) @@ -276,9 +275,9 @@ def get_ready_for_oav_and_close_shutter( yield from bps.wait(group) -def create_gridscan_callbacks( - params: SpecifiedGrids | None, -) -> tuple[GridscanNexusFileCallback, GridDetectAndScanISPyBCallback]: +def create_gridscan_callbacks() -> tuple[ + GridscanNexusFileCallback, GridDetectAndScanISPyBCallback +]: return ( GridscanNexusFileCallback(param_type=SpecifiedThreeDGridScan), GridDetectAndScanISPyBCallback( diff --git a/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py index 8dca22d8bd..c118a51bfd 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py @@ -39,7 +39,7 @@ ) from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample from mx_bluesky.common.parameters.constants import DocDescriptorNames, PlanNameConstants -from mx_bluesky.common.parameters.gridscan import SpecifiedGrids +from mx_bluesky.common.parameters.gridscan import GenericGrid from mx_bluesky.common.utils.exceptions import ( ISPyBDepositionNotMadeError, SampleError, @@ -57,7 +57,7 @@ class GridscanPlane(StrEnum): if TYPE_CHECKING: from event_model import Event, RunStart, RunStop -T = TypeVar("T", bound="SpecifiedGrids") +T = TypeVar("T", bound="GenericGrid") def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters): 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 index 05d70296d2..75962b48eb 100644 --- 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 @@ -15,9 +15,6 @@ populate_data_collection_group, populate_remaining_data_collection_info, ) -from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( - GridscanPlane, -) from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( add_processing_time_to_comment, common_populate_axis_info, @@ -45,6 +42,7 @@ from event_model import RunStart, RunStop T = TypeVar("T", bound="SpecifiedGrids") +D = TypeVar("D") def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters): @@ -93,8 +91,7 @@ def __init__( self.param_type = param_type self._start_of_fgs_uid: str | None = None self._processing_start_time: float | None = None - self._grid_plane_to_id_map: dict[GridscanPlane, int] = {} - self._grid_plane_to_width_map: dict[GridscanPlane, int] = {} + self._grid_num_to_id_map: dict[int, int] = {} self.data_collection_group_info: DataCollectionGroupInfo | None def activity_gated_start(self, doc: RunStart): @@ -142,9 +139,11 @@ def populate_info_for_update( 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=self.ispyb_ids.data_collection_ids[grid_num], + data_collection_id=id, ) scan_data_infos.append(scan_data_info) return scan_data_infos @@ -180,6 +179,13 @@ def activity_gated_stop(self, doc: RunStop) -> RunStop: 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 data_collection_number_from_gridplane(self, plane) -> int: assert self.params return self.params.detector_params.run_number diff --git a/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py index 45f98cb71d..18ac853878 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py @@ -15,6 +15,7 @@ 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" @@ -44,6 +45,33 @@ def generate_start_info_from_omega_map( 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 [] + start_frame = 0 + infos = [] + for grid_num in range(params.num_grids): + 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): if (omega_start := doc.get("gonio-omega")) is not None: omega_in_gda_space = -omega_start 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 d967e716f5..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 @@ -196,12 +196,16 @@ def test_given_ispyb_callback_finished_writing_to_ispyb_when_messages_logged_the latest_record = gelf_handler.emit.call_args.args[-1] assert not hasattr(latest_record, "dc_group_id") + @patch( + "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", - side_effect=[2, 100], + 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 = GridDetectAndScanISPyBCallback( param_type=GenericGridWithHyperionDetectorParams, diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index f373841b3a..2cd2120ac8 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -51,11 +51,13 @@ ) from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( GridDetectAndScanISPyBCallback, - generate_start_info_from_omega_map, ) 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, ) @@ -66,6 +68,7 @@ from mx_bluesky.common.parameters.constants import ( DocDescriptorNames, EnvironmentConstants, + GridscanParamConstants, PlanNameConstants, ) from mx_bluesky.common.parameters.device_composites import ( @@ -179,7 +182,9 @@ def create_gridscan_callbacks() -> tuple[ 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] + ), ), ), ) From 5e8af69118139113560fd9d3677383cc5e08d9a4 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Fri, 27 Feb 2026 16:38:51 +0000 Subject: [PATCH 27/45] Add test stubs --- .../beamlines/i02_1/i02_1_gridscan_plan.py | 2 ++ .../i04_grid_detect_then_xray_centre_plan.py | 2 +- .../grid_detect_and_scan/ispyb_callback.py | 4 ++-- .../callbacks/grid/gridscan/ispyb_callback.py | 6 ++--- .../callbacks/grid/utils.py | 2 +- .../i02_1/test_gridscan_ispyb_callback.py | 1 + .../i02_1/test_i02_1_gridscan_plan.py | 23 +++++++++++++++---- .../common/test_ispyb_callback_base.py | 5 ++++ .../callbacks/grid/__init__.py | 0 .../grid/test_ispyb_gridscan_callback.py | 1 + .../callbacks/grid/test_utils.py | 1 + 11 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 tests/unit_tests/beamlines/i02_1/test_gridscan_ispyb_callback.py create mode 100644 tests/unit_tests/common/external_interaction/callbacks/grid/__init__.py create mode 100644 tests/unit_tests/common/external_interaction/callbacks/grid/test_ispyb_gridscan_callback.py create mode 100644 tests/unit_tests/common/external_interaction/callbacks/grid/test_utils.py 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 702f16de97..282de76d6e 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 @@ -38,6 +38,7 @@ ) from mx_bluesky.common.external_interaction.callbacks.grid.gridscan.ispyb_callback import ( GridscanISPyBCallback, + ispyb_activation_decorator, ) from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( generate_start_info_from_num_grids, @@ -162,6 +163,7 @@ def i02_1_gridscan_plan( beamline_specific = construct_i02_1_specific_features(composite, parameters) callbacks = create_gridscan_callbacks(parameters) + @ispyb_activation_decorator @bpp.subs_decorator(callbacks) def decorated_flyscan_plan(): yield from common_flyscan_xray_centre(composite, parameters, beamline_specific) 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 ae11e4c232..d205ccff53 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 @@ -281,7 +281,7 @@ def create_gridscan_callbacks() -> tuple[ return ( GridscanNexusFileCallback(param_type=SpecifiedThreeDGridScan), GridDetectAndScanISPyBCallback( - param_type=SpecifiedThreeDGridScan, + param_type=GenericGrid, emit=ZocaloCallback( PlanNameConstants.DO_FGS, EnvironmentConstants.ZOCALO_ENV, diff --git a/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py index c118a51bfd..39c914fdfd 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/grid/grid_detect_and_scan/ispyb_callback.py @@ -22,7 +22,7 @@ ) from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( ASSERT_START_BEFORE_EVENT_DOC_MESSAGE, - add_processing_time_to_comment, + common_add_processing_time_to_comment, common_populate_axis_info, ) from mx_bluesky.common.external_interaction.ispyb.data_model import ( @@ -172,7 +172,7 @@ def activity_gated_event(self, doc: Event): return doc def _add_processing_time_to_comment(self, processing_start_time: float): - add_processing_time_to_comment( + common_add_processing_time_to_comment( self, processing_start_time, self.data_collection_group_info ) 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 index 75962b48eb..ab0b523cbe 100644 --- 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 @@ -16,7 +16,7 @@ populate_remaining_data_collection_info, ) from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( - add_processing_time_to_comment, + common_add_processing_time_to_comment, common_populate_axis_info, ) from mx_bluesky.common.external_interaction.ispyb.data_model import ( @@ -112,7 +112,7 @@ def activity_gated_start(self, doc: RunStart): return super().activity_gated_start(doc) def _add_processing_time_to_comment(self, processing_start_time: float): - add_processing_time_to_comment( + common_add_processing_time_to_comment( self, processing_start_time, self.data_collection_group_info ) @@ -150,7 +150,7 @@ def populate_info_for_update( 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.GRID_DETECT_AND_DO_GRIDSCAN} start document" + 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() diff --git a/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py index 18ac853878..2729ef9f90 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py @@ -83,7 +83,7 @@ def common_populate_axis_info(data_collection_info: DataCollectionInfo, doc: dic data_collection_info.chi_start = chi_start -def add_processing_time_to_comment( +def common_add_processing_time_to_comment( callback: BaseISPyBCallback, processing_start_time: float, data_collection_group_info: DataCollectionGroupInfo | None, 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..5ed1f1320a --- /dev/null +++ b/tests/unit_tests/beamlines/i02_1/test_gridscan_ispyb_callback.py @@ -0,0 +1 @@ +def test_get_scan_infos_gives_expected_output(): ... 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 49f5cb5513..68a8de4fee 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 @@ -14,6 +14,7 @@ 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 ( FlyScanXRayCentreComposite, construct_i02_1_specific_features, @@ -27,8 +28,8 @@ @pytest.fixture -def fgs_params_two_d(tmp_path) -> SpecifiedTwoDGridScan: - return SpecifiedTwoDGridScan( +def fgs_params_two_d(tmp_path) -> I02_1FgsParams: + return I02_1FgsParams( x_start_um=0, y_starts_um=[0], z_starts_um=[0], @@ -41,6 +42,13 @@ def fgs_params_two_d(tmp_path) -> SpecifiedTwoDGridScan: 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, ) @@ -96,7 +104,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, @@ -135,7 +143,7 @@ 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: FlyScanXRayCentreComposite, ): expected_features = construct_i02_1_specific_features( @@ -143,7 +151,12 @@ def test_i02_1_flyscan_xray_centre_in_re( ) mock_create_features.return_value = expected_features - run_engine(i02_1_gridscan_plan(fgs_params_two_d, fgs_composite)) + run_engine( + i02_1_gridscan_plan(fgs_params_two_d, fgs_composite) + ) # todo fix this typing mock_common_scan.assert_called_once_with( fgs_composite, fgs_params_two_d, expected_features ) + + +def test_ispyb_callbacks_activated_when_expected(): ... 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..a6c66f06b3 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 @@ -58,3 +58,8 @@ def test_plan(): with pytest.raises(ValueError): run_engine(test_plan()) + + +def test_handle_ispyb_transmission_flux_read_if_no_beamsize(): + # Test exception and check if params exist then it still works + ... 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..c567398461 --- /dev/null +++ b/tests/unit_tests/common/external_interaction/callbacks/grid/test_ispyb_gridscan_callback.py @@ -0,0 +1 @@ +# todo use tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_callback.py as a template to test this file 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..00e89fc4a0 --- /dev/null +++ b/tests/unit_tests/common/external_interaction/callbacks/grid/test_utils.py @@ -0,0 +1 @@ +def test_generate_start_info_from_num_grids(): ... From 644191871270610afa603e1636960c9f807519aa Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Mon, 2 Mar 2026 15:23:47 +0000 Subject: [PATCH 28/45] Add fixes and tests --- .../callbacks/gridscan/ispyb_callback.py | 13 +- .../beamlines/i02_1/i02_1_gridscan_plan.py | 6 +- .../experiment_plans/inner_plans/do_fgs.py | 4 +- .../callbacks/grid/gridscan/ispyb_callback.py | 6 +- .../callbacks/grid/utils.py | 3 +- .../common/parameters/components.py | 2 + tests/unit_tests/beamlines/i02_1/conftest.py | 30 +++++ .../i02_1/test_gridscan_ispyb_callback.py | 77 +++++++++++- .../i02_1/test_i02_1_gridscan_plan.py | 84 ++++++++----- .../common/test_ispyb_callback_base.py | 51 +++++++- .../grid/test_ispyb_gridscan_callback.py | 113 +++++++++++++++++- .../callbacks/grid/test_utils.py | 29 ++++- .../common/parameters/test_gridscan.py | 4 +- 13 files changed, 369 insertions(+), 53 deletions(-) create mode 100644 tests/unit_tests/beamlines/i02_1/conftest.py 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 index 0c6830078b..3e9affe91f 100644 --- 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 @@ -5,7 +5,7 @@ construct_comment_for_gridscan, ) from mx_bluesky.common.external_interaction.callbacks.grid.gridscan.ispyb_callback import ( - GridscanISPyBCallback, + GridscanISPyBCallback as CommonGridscanISPyBCallback, ) from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionGridInfo, @@ -16,7 +16,11 @@ from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER -class I021GridscanISPyBCallback(GridscanISPyBCallback): +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. @@ -63,9 +67,8 @@ def _get_scan_infos(self, doc) -> Sequence[ScanDataInfo]: data_collection_id = self.ispyb_ids.data_collection_ids[0] - self.data_collection_group_info.comments = ( - f"Diffraction grid scan of {data_collection_grid_info.steps_x} by " - f"{self.params.y_steps}." + 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"]) 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 282de76d6e..a348dbbb4e 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 @@ -24,6 +24,9 @@ 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, @@ -37,7 +40,6 @@ GridscanNexusFileCallback, ) from mx_bluesky.common.external_interaction.callbacks.grid.gridscan.ispyb_callback import ( - GridscanISPyBCallback, ispyb_activation_decorator, ) from mx_bluesky.common.external_interaction.callbacks.grid.utils import ( @@ -163,8 +165,8 @@ def i02_1_gridscan_plan( beamline_specific = construct_i02_1_specific_features(composite, parameters) callbacks = create_gridscan_callbacks(parameters) - @ispyb_activation_decorator @bpp.subs_decorator(callbacks) + @ispyb_activation_decorator(parameters) def decorated_flyscan_plan(): yield from common_flyscan_xray_centre(composite, parameters, beamline_specific) 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/grid/gridscan/ispyb_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/gridscan/ispyb_callback.py index ab0b523cbe..82ed95e03b 100644 --- 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 @@ -95,7 +95,7 @@ def __init__( self.data_collection_group_info: DataCollectionGroupInfo | None def activity_gated_start(self, doc: RunStart): - if doc.get("subplan_name") == PlanNameConstants.DO_FGS: + 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 " @@ -186,10 +186,6 @@ def tag_doc(self, doc: D) -> D: doc["_grid_num_to_id_map"] = self._grid_num_to_id_map return doc # type: ignore - def data_collection_number_from_gridplane(self, plane) -> int: - assert self.params - return self.params.detector_params.run_number - def fill_gridscan_deposition_and_store( self, make_scan_infos_with_grid_info: Callable[..., Sequence[ScanDataInfo]] ): diff --git a/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py b/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py index 2729ef9f90..e115d27299 100644 --- a/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/grid/utils.py @@ -55,9 +55,10 @@ def generate_start_info_from_num_grids( """ doc = yield [] - start_frame = 0 + 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( diff --git a/src/mx_bluesky/common/parameters/components.py b/src/mx_bluesky/common/parameters/components.py index 1c54bf9a00..8fb57e2b03 100644 --- a/src/mx_bluesky/common/parameters/components.py +++ b/src/mx_bluesky/common/parameters/components.py @@ -271,6 +271,8 @@ def _start_for_axis(self, axis: XyzAxis, grid: int) -> float: class OptionalGonioAngleStarts(BaseModel): # Gridscans have different omega starts + # 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 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..56e1de725b --- /dev/null +++ b/tests/unit_tests/beamlines/i02_1/conftest.py @@ -0,0 +1,30 @@ +import pytest + +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, + ) 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 index 5ed1f1320a..c6280d8915 100644 --- a/tests/unit_tests/beamlines/i02_1/test_gridscan_ispyb_callback.py +++ b/tests/unit_tests/beamlines/i02_1/test_gridscan_ispyb_callback.py @@ -1 +1,76 @@ -def test_get_scan_infos_gives_expected_output(): ... +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 68a8de4fee..2ae688a210 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 @@ -21,37 +21,20 @@ i02_1_gridscan_plan, ) from mx_bluesky.beamlines.i02_1.parameters.gridscan import SpecifiedTwoDGridScan +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 get_param_version from mx_bluesky.common.parameters.device_composites import ( GonioWithOmega, ) -@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="visit", - 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, - ) - - @pytest.fixture def zebra_fgs_two_d() -> ZebraFastGridScanTwoD: device = i02_1.zebra_fast_grid_scan.build(connect_immediately=True, mock=True) @@ -151,12 +134,55 @@ def test_i02_1_flyscan_xray_centre_in_re( ) mock_create_features.return_value = expected_features - run_engine( - i02_1_gridscan_plan(fgs_params_two_d, fgs_composite) - ) # todo fix this typing + 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 ) -def test_ispyb_callbacks_activated_when_expected(): ... +@patch( + "mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan.construct_i02_1_specific_features", +) +@patch( + "mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan.common_flyscan_xray_centre", + new=MagicMock(), +) +@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, + run_engine: RunEngine, + fgs_params_two_d: I02_1FgsParams, + fgs_composite: FlyScanXRayCentreComposite, +): + mock_ispyb = MagicMock() + + 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 + # uid = "test uid" + + # def do_run_start_and_run_stop(): + # yield from RunStart(uid=uid, time=0) + # yield from RunStop(uid=uid, ) + + run_engine(i02_1_gridscan_plan(fgs_params_two_d, fgs_composite)) + initial_group_info = populate_data_collection_group(fgs_params_two_d) + initial_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." + initial_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( + initial_group_info, [initial_scan_info] + ) 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 a6c66f06b3..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 @@ -60,6 +60,49 @@ def test_plan(): run_engine(test_plan()) -def test_handle_ispyb_transmission_flux_read_if_no_beamsize(): - # Test exception and check if params exist then it still works - ... +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/test_ispyb_gridscan_callback.py b/tests/unit_tests/common/external_interaction/callbacks/grid/test_ispyb_gridscan_callback.py index c567398461..841d69867b 100644 --- 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 @@ -1 +1,112 @@ -# todo use tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_callback.py as a template to test this file +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.DO_FGS, + "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 index 00e89fc4a0..9cb8718504 100644 --- a/tests/unit_tests/common/external_interaction/callbacks/grid/test_utils.py +++ b/tests/unit_tests/common/external_interaction/callbacks/grid/test_utils.py @@ -1 +1,28 @@ -def test_generate_start_info_from_num_grids(): ... +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/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, From 55e35175e16af296e51aca74b58856a488a12bcd Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Mon, 2 Mar 2026 15:25:04 +0000 Subject: [PATCH 29/45] Remove comment --- tests/unit_tests/beamlines/i02_1/test_i02_1_gridscan_plan.py | 5 ----- 1 file changed, 5 deletions(-) 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 2ae688a210..723cec4e91 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 @@ -166,11 +166,6 @@ def test_ispyb_activated_correct_params( run_engine.md["data"] = {} mock_create_features.return_value = expected_features - # uid = "test uid" - - # def do_run_start_and_run_stop(): - # yield from RunStart(uid=uid, time=0) - # yield from RunStop(uid=uid, ) run_engine(i02_1_gridscan_plan(fgs_params_two_d, fgs_composite)) initial_group_info = populate_data_collection_group(fgs_params_two_d) From 0293b71c56298b7cf3926a7975dcfd7642cd90e9 Mon Sep 17 00:00:00 2001 From: Ollie Silvester Date: Tue, 3 Mar 2026 13:34:20 +0000 Subject: [PATCH 30/45] Fix tests --- tests/unit_tests/beamlines/i02_1/test_i02_1_gridscan_plan.py | 4 ++++ .../callbacks/grid/test_ispyb_gridscan_callback.py | 2 +- uv.lock | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) 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 723cec4e91..3c275abf82 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 @@ -116,6 +116,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", ) 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 index 841d69867b..10b15e7aa5 100644 --- 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 @@ -37,7 +37,7 @@ def test_gridscan_callback_start_calls_correct_funcs( cb = Callback(SpecifiedThreeDGridScan) cb.fill_gridscan_deposition_and_store = MagicMock() doc = { - "subplan_name": PlanNameConstants.DO_FGS, + "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 diff --git a/uv.lock b/uv.lock index 5d16ef9d82..62c4645062 100644 --- a/uv.lock +++ b/uv.lock @@ -802,8 +802,8 @@ wheels = [ [[package]] name = "dls-dodal" -version = "2.0.1.dev29+ge70315c70" -source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#e70315c70dabec4973844a085a1d0f3f6bbfe08e" } +version = "2.1.0" +source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#8dfcdaa5d32d3ec0b41b7e049994e7ba10f5cf62" } dependencies = [ { name = "aiofiles", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "aiohttp", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, From 3d52fce21b9cfce38084a2eacde6007849398d4e Mon Sep 17 00:00:00 2001 From: Ollie Silvester Date: Fri, 6 Mar 2026 10:23:19 +0000 Subject: [PATCH 31/45] Fix device types and params --- .../beamlines/i02_1/i02_1_gridscan_plan.py | 64 ++++++++++++++----- .../i02_1/test_i02_1_gridscan_plan.py | 64 ++++++++++++++++--- uv.lock | 4 +- 3 files changed, 106 insertions(+), 26 deletions(-) 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 a348dbbb4e..1273b8a87b 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 @@ -7,15 +7,15 @@ 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.slits import Slits from dodal.devices.undulator import BaseUndulator from dodal.devices.zebra.zebra import Zebra -from pydantic import BaseModel, PrivateAttr +from pydantic import BaseModel from pydantic_extra_types.semantic_version import SemanticVersion from semver import Version @@ -45,6 +45,10 @@ 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, @@ -82,7 +86,7 @@ class FlyScanXRayCentreComposite(FlyScanEssentialDevices[GonioWithOmegaType]): attenuator: ReadOnlyAttenuator flux: Flux undulator: BaseUndulator - s4_slit_gaps: S4SlitGaps + s4_slit_gaps: Slits def construct_i02_1_specific_features( @@ -139,10 +143,9 @@ def get_internal_param_version() -> SemanticVersion: class ExternalGridScanParams(BaseModel): - gda_parameter_version: SemanticVersion visit: str file_name: str - storage_directory: Path + storage_directory: str exposure_time_s: float snapshot_directory: Path x_start_um: float @@ -150,24 +153,55 @@ class ExternalGridScanParams(BaseModel): z_start_um: float x_steps: int y_steps: int - sample_id: int | None = None - - # Internal parameter version compatible with this external model - _internal_param_version: str = PrivateAttr(default="6.0.0") + 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 + + +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=[0], + 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.GRIDSCAN_2D, + ) def i02_1_gridscan_plan( - parameters: I02_1FgsParams, + parameters: ExternalGridScanParams, composite: FlyScanXRayCentreComposite = inject(""), ) -> MsgGenerator: """BlueAPI entry point for i02-1 grid scans""" - beamline_specific = construct_i02_1_specific_features(composite, parameters) - callbacks = create_gridscan_callbacks(parameters) + 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(parameters) + @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/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 3c275abf82..9818867f0f 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,10 +5,10 @@ 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.slits import Slits from dodal.devices.synchrotron import Synchrotron from dodal.devices.undulator import BaseUndulator from dodal.devices.zebra.zebra import Zebra @@ -16,6 +16,7 @@ from mx_bluesky.beamlines.i02_1.composites import I02_1FgsParams from mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan import ( + ExternalGridScanParams, FlyScanXRayCentreComposite, construct_i02_1_specific_features, i02_1_gridscan_plan, @@ -42,6 +43,44 @@ def zebra_fgs_two_d() -> ZebraFastGridScanTwoD: return device +@pytest.fixture +def entry_params(tmp_path) -> ExternalGridScanParams: + return ExternalGridScanParams( + visit="visit", + file_name="file_name", + 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=5, + 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=1, + ) + + +@pytest.fixture +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 + + @pytest.fixture def fgs_composite( eiger: EigerDetector, @@ -52,7 +91,7 @@ def fgs_composite( attenuator: ReadOnlyAttenuator, flux: Flux, undulator: BaseUndulator, - s4_slit_gaps: S4SlitGaps, + slits: Slits, zebra: Zebra, ) -> FlyScanXRayCentreComposite: return FlyScanXRayCentreComposite( @@ -65,7 +104,7 @@ def fgs_composite( attenuator, flux, undulator, - s4_slit_gaps, + slits, ) @@ -132,36 +171,43 @@ def test_i02_1_flyscan_xray_centre_in_re( run_engine: RunEngine, fgs_params_two_d: I02_1FgsParams, fgs_composite: FlyScanXRayCentreComposite, + entry_params: ExternalGridScanParams, ): expected_features = construct_i02_1_specific_features( fgs_composite, fgs_params_two_d ) mock_create_features.return_value = expected_features - run_engine(i02_1_gridscan_plan(fgs_params_two_d, fgs_composite)) + run_engine(i02_1_gridscan_plan(entry_params, fgs_composite)) mock_common_scan.assert_called_once_with( fgs_composite, fgs_params_two_d, expected_features ) -@patch( - "mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan.construct_i02_1_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: FlyScanXRayCentreComposite, + 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( @@ -171,7 +217,7 @@ def test_ispyb_activated_correct_params( mock_create_features.return_value = expected_features - run_engine(i02_1_gridscan_plan(fgs_params_two_d, fgs_composite)) + run_engine(i02_1_gridscan_plan(entry_params, fgs_composite)) initial_group_info = populate_data_collection_group(fgs_params_two_d) initial_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." initial_scan_info = ScanDataInfo( diff --git a/uv.lock b/uv.lock index 62c4645062..8904ab719d 100644 --- a/uv.lock +++ b/uv.lock @@ -802,8 +802,8 @@ wheels = [ [[package]] name = "dls-dodal" -version = "2.1.0" -source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#8dfcdaa5d32d3ec0b41b7e049994e7ba10f5cf62" } +version = "2.1.1.dev3+g9baf92f5e" +source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#9baf92f5ec1371aaa789ae40f481badd1730e030" } dependencies = [ { name = "aiofiles", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "aiohttp", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, From 2a3e3f2713ddf3ccdfbb035c2eaa22a07223b38a Mon Sep 17 00:00:00 2001 From: Ollie Silvester Date: Tue, 10 Mar 2026 17:05:28 +0000 Subject: [PATCH 32/45] Add blueapi entry point and fix ispyb slits read --- src/mx_bluesky/beamlines/i02_1/__init__.py | 3 ++ .../callbacks/common/ispyb_callback_base.py | 29 +++++++++++++++---- uv.lock | 4 +-- 3 files changed, 28 insertions(+), 8 deletions(-) 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/common/external_interaction/callbacks/common/ispyb_callback_base.py b/src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py index 2860891d8a..bb15157ba8 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 @@ -137,12 +137,29 @@ def _handle_ispyb_hardware_read(self, doc) -> Sequence[ScanDataInfo]: ) synchrotron_mode = _data["synchrotron-synchrotron_mode"] assert isinstance(synchrotron_mode, SynchrotronMode) - 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"], - ) + + # We should improve slit PV name to give consistency, or come up with a way + # to get better typing on the _data dict + if _data["s4_slit_gaps-xgap"]: + 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 _data["s4_slit_gaps-x_gap"]: + hwscan_data_collection_info = DataCollectionInfo( + undulator_gap1=_data["undulator-current_gap"], + synchrotron_mode=synchrotron_mode.value, + 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 ) diff --git a/uv.lock b/uv.lock index 8904ab719d..bb87e099ef 100644 --- a/uv.lock +++ b/uv.lock @@ -802,8 +802,8 @@ wheels = [ [[package]] name = "dls-dodal" -version = "2.1.1.dev3+g9baf92f5e" -source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#9baf92f5ec1371aaa789ae40f481badd1730e030" } +version = "2.1.1.dev15+gb6927cc54" +source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#b6927cc548cc0474f30328c53c9464fa51193366" } dependencies = [ { name = "aiofiles", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "aiohttp", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, From f76dbc0bb735c0ed469faefacf728df4b031a9ff Mon Sep 17 00:00:00 2001 From: Ollie Silvester Date: Tue, 10 Mar 2026 17:14:45 +0000 Subject: [PATCH 33/45] Extend gda param model to include box size and omega start --- .../callbacks/gridscan/ispyb_callback.py | 4 ++-- src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) 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 index 3e9affe91f..3455847000 100644 --- 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 @@ -50,8 +50,8 @@ def _get_scan_infos(self, doc) -> Sequence[ScanDataInfo]: 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, + 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, 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 1273b8a87b..29ead549e4 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 @@ -57,6 +57,7 @@ FlyScanEssentialDevices, GonioWithOmegaType, ) +from mx_bluesky.common.parameters.gridscan import PositiveFloat from mx_bluesky.common.utils.log import LOGGER @@ -162,13 +163,18 @@ class ExternalGridScanParams(BaseModel): 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=[0], + omega_starts_deg=[params.omega_start_deg], sample_id=params.sample_id, visit=params.visit, parameter_model_version=get_param_version(), @@ -185,6 +191,8 @@ def get_internal_params(params: ExternalGridScanParams) -> I02_1FgsParams: upper_left_y=params.upper_left_y, detector_distance_mm=params.detector_distance_mm, ispyb_experiment_type=IspybExperimentType.GRIDSCAN_2D, + x_step_size_um=params.x_step_size_um, + y_step_sizes_um=params.y_step_sizes_um, ) From fd121e9967daa0a291ea528ee0f9cf49f20a0aad Mon Sep 17 00:00:00 2001 From: Ollie Silvester Date: Tue, 10 Mar 2026 17:16:32 +0000 Subject: [PATCH 34/45] Use SAD experiment type --- src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 29ead549e4..19e4ba8dcd 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 @@ -190,7 +190,7 @@ def get_internal_params(params: ExternalGridScanParams) -> I02_1FgsParams: 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.GRIDSCAN_2D, + ispyb_experiment_type=IspybExperimentType.SAD, x_step_size_um=params.x_step_size_um, y_step_sizes_um=params.y_step_sizes_um, ) From 54d8aaa2515256031fc27982660a1a8c790ff6f0 Mon Sep 17 00:00:00 2001 From: Ollie Silvester Date: Tue, 10 Mar 2026 17:20:20 +0000 Subject: [PATCH 35/45] Use correct vmxm detector type --- src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py | 1 + src/mx_bluesky/common/parameters/gridscan.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) 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 19e4ba8dcd..5e8fe16c33 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 @@ -193,6 +193,7 @@ def get_internal_params(params: ExternalGridScanParams) -> I02_1FgsParams: 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, ) diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index e6bebfa63e..2eec168105 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -4,7 +4,10 @@ from typing import Annotated, Generic, TypeVar from dodal.devices.aperturescatterguard import ApertureValue -from dodal.devices.detector.det_dim_constants import EIGER2_X_4M_SIZE, EIGER2_X_16M_SIZE +from dodal.devices.detector.det_dim_constants import ( + EIGER2_X_9M_SIZE, + EIGER2_X_16M_SIZE, +) from dodal.devices.detector.detector import DetectorParams from dodal.devices.fast_grid_scan import ( GridScanParamsCommon, @@ -32,7 +35,7 @@ ) DETECTOR_SIZE_PER_BEAMLINE = { - "i02-1": EIGER2_X_4M_SIZE, + "i02-1": EIGER2_X_9M_SIZE, "dev": EIGER2_X_16M_SIZE, "i03": EIGER2_X_16M_SIZE, "i04": EIGER2_X_16M_SIZE, From 28cff713e2b79daef66a396e2574d0d7a87f7f20 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Tue, 5 May 2026 11:27:16 +0100 Subject: [PATCH 36/45] Update uv.lock --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index 8e9b2f3cac..b121da86d7 100644 --- a/uv.lock +++ b/uv.lock @@ -807,8 +807,8 @@ wheels = [ [[package]] name = "dls-dodal" -version = "2.2.4.dev6+g454f60aab" -source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#454f60aabfb68548eb1a7535adea6ce464e392c8" } +version = "2.2.4.dev7+g0beeb3135" +source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#0beeb3135ae40248df69aa3c4d04ec43af055436" } dependencies = [ { name = "aiofiles", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "aiohttp", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, From 176d60bba6620639ee63a992d3cd2932fc3624e2 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Tue, 5 May 2026 14:51:04 +0100 Subject: [PATCH 37/45] Tidy gridscan plantUML --- .../hyperion/reference/gridscan.puml | 158 ++++++++++-------- 1 file changed, 87 insertions(+), 71 deletions(-) diff --git a/docs/developer/hyperion/reference/gridscan.puml b/docs/developer/hyperion/reference/gridscan.puml index 3968f88cc0..c0f8fdab39 100644 --- a/docs/developer/hyperion/reference/gridscan.puml +++ b/docs/developer/hyperion/reference/gridscan.puml @@ -1,38 +1,70 @@ @startuml title Gridscan Parameter Relationships -class DiffractionExperiment -class DiffractionExperimentWithSample -class GenericGrid { - grid_width_um - exposure_time_s +package dodal { + class AbstractExperimentParameterBase + class AbstractExperimentWithBeamParams { + transmission_fraction + } + class GridScanParamsCommon { + x_steps + y_steps + x_step_size_mm + y_step_size_mm + x_start_mm + y1_start_mm + z1_start_mm + set_stub_offsets + } + class GridScanParamsThreeD + class PandAGridScanParams + class ZebraGridScanParamsThreeD + class ZebraGridScanParamsTwoD } -class GridScanWithEdgeDetect { - box_size_um -} -class HyperionGenericGrid { - enable_dev_shm + +rectangle "Internal Experiment Parameter Model" { + class MxBlueSkyParameters + class DiffractionExperiment + class DiffractionExperimentWithSample + class GenericGrid { + grid_width_um + exposure_time_s + } + class GenericGridWithHyperionDetectorParams + class GridScanWithEdgeDetect { + box_size_um + } + class PinTipCentreThenXrayCentre + class RobotLoadThenCentre } -class HyperionThreeDGridScan { - x_step_size_um - y_step_size_um - z_step_size_um - y2_start_um - z2_start_um - -- - grid_1_spec - grid_2_spec - scan_indices - scan_spec - scan_points - scan_points_first_grid - scan_points_second_grid - num_images - FGS_Params - panda_FGS_Params + + + +rectangle "Specified Grids" { + class SpecifiedGrids { + omega_starts_deg: float[] + x_step_size_um + y_step_sizes_um + x_steps + y_steps: int[] + -- + fast_grid_scan_params: GenericParamType + grid_specs + scan_indices + scan_points + scan_spec + num_images + } + class "SpecifiedGrids" as SpecifiedGridsZebraGridScanParamsThreeD + class "SpecifiedGrids" as SpecifiedGridsZebraGridScanParamsTwoD + class SpecifiedTwoDGridScan + class SpecifiedThreeDGridScan + class HyperionSpecifiedThreeDGridScan { + -- + panda_fast_gridscan_params: PandAGridScanParams + } } -class MxBlueSkyParameters -class SpecifiedGrid + class XyzStarts { x_start_um y_start_um @@ -43,29 +75,28 @@ class OptionalXYZStarts { y_start_um z_start_um } -class RotationScanPerSweep MxBlueSkyParameters <|-- DiffractionExperiment DiffractionExperiment <|-- DiffractionExperimentWithSample DiffractionExperimentWithSample <|-- GenericGrid -GenericGrid <|-- GridScanWithEdgeDetect -GenericGrid <|-- HyperionGenericGrid -HyperionGenericGrid <|-- HyperionThreeDGridScan -SpecifiedGrid <|-- HyperionThreeDGridScan -XyzStarts <|-- SpecifiedGrid +GenericGrid <|-- GenericGridWithHyperionDetectorParams +GenericGridWithHyperionDetectorParams <|-- GridScanWithEdgeDetect +GenericGridWithHyperionDetectorParams <|-- PinTipCentreThenXrayCentre +GenericGridWithHyperionDetectorParams <|-- RobotLoadThenCentre +GenericGrid <|-- SpecifiedGrids +SpecifiedGridsZebraGridScanParamsTwoD <|-- SpecifiedTwoDGridScan +SpecifiedGridsZebraGridScanParamsThreeD <|-- SpecifiedThreeDGridScan +SpecifiedThreeDGridScan <|-- HyperionSpecifiedThreeDGridScan +XyzStarts <|-- SpecifiedGrids OptionalXYZStarts <|-- RotationScanPerSweep class GridParamUpdate { - x_start_um - y_start_um - y2_start_um - z_start_um - z2_start_um - x_steps - y_steps - z_steps - x_step_size_um - y_step_size_um - z_step_size_um + x_start_um: float + y_starts_um: float[] + z_starts_um: float[] + x_steps: float + y_steps: float[] + x_step_size_um: float + y_step_sizes_um: float[] } class GridDetectionCallback { @@ -73,37 +104,22 @@ class GridDetectionCallback { } GridDetectionCallback --> GridParamUpdate : generates from event. Adds 0.5 to get box-centres -GridParamUpdate --> HyperionThreeDGridScan : combines with GridScanWithEdgeDetect +GridParamUpdate --> HyperionSpecifiedThreeDGridScan : combines with GridScanWithEdgeDetect class experiment_plans { grid_detect_then_xray_centre() common_flyscan_xray_centre() - create_parameters_for_flyscan_xray_centre(GridScanWithEdgeDetect, GridParamUpdate) -> HyperionThreeDGridScan -} - -class AbstractExperimentBase -class AbstractExperimentWithBeamParams -class GridScanParamsCommon { - x_steps - y_steps - z_steps - x_step_size_mm - y_step_size_mm - z_step_size_mm - x_start_mm - y1_start_mm - y2_start_mm - z1_start_mm - z2_start_mm + create_parameters_for_flyscan_xray_centre(GridScanWithEdgeDetect, GridParamUpdate) -> HyperionSpecifiedThreeDGridScan } -class PandAGridScanParams -class ZebraGridScanParamsThreeD -AbstractExperimentBase <|-- AbstractExperimentWithBeamParams +AbstractExperimentParameterBase <|-- AbstractExperimentWithBeamParams AbstractExperimentWithBeamParams <|-- GridScanParamsCommon -GridScanParamsCommon <|-- PandAGridScanParams -GridScanParamsCommon <|-- ZebraGridScanParamsThreeD +GridScanParamsCommon <|-- ZebraGridScanParamsTwoD +GridScanParamsCommon <|-- GridScanParamsThreeD +GridScanParamsThreeD <|-- PandAGridScanParams +GridScanParamsThreeD <|-- ZebraGridScanParamsThreeD -HyperionThreeDGridScan --> ZebraGridScanParamsThreeD : generates -HyperionThreeDGridScan --> PandAGridScanParams : generates +HyperionSpecifiedThreeDGridScan --> ZebraGridScanParamsThreeD : generates +HyperionSpecifiedThreeDGridScan --> PandAGridScanParams : generates +SpecifiedTwoDGridScan --> ZebraGridScanParamsTwoD : generates @enduml From 2e37ac2fb65d1f185aa095b8ace219bbe0ce81eb Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Wed, 6 May 2026 16:15:57 +0100 Subject: [PATCH 38/45] Update dodal dependency to require branch --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cefafbeb94..4d555dbd85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dependencies = [ "ophyd >= 1.10.5", "ophyd-async >= 0.16.0", "bluesky >= 1.14.6", - "dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@main", + "dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@add_vmxm_devices_for_ispyb_integration", ] From a300a43a9ee45fad08904be15249c304ff0ee1b1 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Thu, 14 May 2026 13:10:10 +0100 Subject: [PATCH 39/45] Fixup use of typevars for the gonio --- src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py | 4 ++-- tests/unit_tests/beamlines/i02_1/conftest.py | 6 ++++++ .../beamlines/i02_1/test_i02_1_gridscan_plan.py | 8 +++----- 3 files changed, 11 insertions(+), 7 deletions(-) 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 770bbb4ea9..683beae593 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 @@ -12,6 +12,7 @@ from dodal.devices.fast_grid_scan import ( set_fast_grid_scan_params as set_flyscan_params_plan, ) +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 @@ -55,7 +56,6 @@ ) from mx_bluesky.common.parameters.device_composites import ( FlyScanEssentialDevices, - GonioWithOmegaType, ) from mx_bluesky.common.parameters.gridscan import PositiveFloat from mx_bluesky.common.utils.log import LOGGER @@ -78,7 +78,7 @@ def create_gridscan_callbacks( @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 diff --git a/tests/unit_tests/beamlines/i02_1/conftest.py b/tests/unit_tests/beamlines/i02_1/conftest.py index ae1f7812bc..eb4f4e059f 100644 --- a/tests/unit_tests/beamlines/i02_1/conftest.py +++ b/tests/unit_tests/beamlines/i02_1/conftest.py @@ -1,4 +1,5 @@ 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 @@ -33,3 +34,8 @@ def fgs_params_two_d(tmp_path) -> I02_1FgsParams: @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_i02_1_gridscan_plan.py b/tests/unit_tests/beamlines/i02_1/test_i02_1_gridscan_plan.py index d109ff6598..a4ee1fd79d 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 @@ -8,6 +8,7 @@ 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.motors import XYZWrappedOmegaStage from dodal.devices.slits import Slits from dodal.devices.synchrotron import Synchrotron from dodal.devices.undulator import BaseUndulator @@ -34,9 +35,6 @@ IspybExperimentType, get_param_version, ) -from mx_bluesky.common.parameters.device_composites import ( - GonioWithOmega, -) @pytest.fixture @@ -91,7 +89,7 @@ def flux() -> Flux: def fgs_composite( eiger: EigerDetector, synchrotron: Synchrotron, - smargon: GonioWithOmega, + goniometer: XYZWrappedOmegaStage, zebra_fgs_two_d: ZebraFastGridScanTwoD, dcm: DoubleCrystalMonochromatorBase, attenuator: ReadOnlyAttenuator, @@ -103,7 +101,7 @@ def fgs_composite( return I021FlyScanXRayCentreComposite( eiger, synchrotron, - smargon, + goniometer, zebra, zebra_fgs_two_d, dcm, From e0b9a1253fee60f408da6fae301761ed213d1724 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Thu, 14 May 2026 13:11:36 +0100 Subject: [PATCH 40/45] Repin dodal --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4d555dbd85..132579a4bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dependencies = [ "ophyd >= 1.10.5", "ophyd-async >= 0.16.0", "bluesky >= 1.14.6", - "dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@add_vmxm_devices_for_ispyb_integration", + "dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@device_changes_for_vmxm_ispyb", ] From 6f61bc765e4b92db7ca7cd05a6f99dd32d2532dd Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Thu, 14 May 2026 13:11:59 +0100 Subject: [PATCH 41/45] Update uv.lock --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index b7fd9c53b3..7121cfac1c 100644 --- a/uv.lock +++ b/uv.lock @@ -807,8 +807,8 @@ wheels = [ [[package]] name = "dls-dodal" -version = "2.3.1.dev4+gdd687cce0" -source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#dd687cce03e1a18ba9a0698d7ebc1e8f6b460ad9" } +version = "2.3.1.dev6+g067e07a27" +source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=device_changes_for_vmxm_ispyb#067e07a273228cc210b40fc0a3e763b4cbbb4cc8" } dependencies = [ { name = "aiofiles", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "aiohttp", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, @@ -2118,7 +2118,7 @@ requires-dist = [ { name = "caproto" }, { name = "daq-config-server", specifier = ">=1.3.0" }, { name = "deepdiff" }, - { name = "dls-dodal", git = "https://github.com/DiamondLightSource/dodal.git?rev=main" }, + { name = "dls-dodal", git = "https://github.com/DiamondLightSource/dodal.git?rev=device_changes_for_vmxm_ispyb" }, { name = "fastapi", extras = ["all"] }, { name = "flask-restful" }, { name = "jupyterlab" }, From d0b499b92c78da9cf2b7c756476341bec1c49962 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Thu, 14 May 2026 16:57:25 +0100 Subject: [PATCH 42/45] Override the default grid box size for Vmxm --- src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py | 3 +++ 1 file changed, 3 insertions(+) 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 683beae593..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 @@ -60,6 +60,8 @@ 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( params: I02_1FgsParams, @@ -194,6 +196,7 @@ def get_internal_params(params: ExternalGridScanParams) -> I02_1FgsParams: 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, ) From 529d0f094cc8ea870e8ffddf28998a92a3c0342f Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Fri, 15 May 2026 10:37:10 +0100 Subject: [PATCH 43/45] unpin dodal --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 132579a4bc..cefafbeb94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dependencies = [ "ophyd >= 1.10.5", "ophyd-async >= 0.16.0", "bluesky >= 1.14.6", - "dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@device_changes_for_vmxm_ispyb", + "dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@main", ] From 9f367c034dad1575a868d4554cb5ac878e4dfa48 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Fri, 15 May 2026 10:37:45 +0100 Subject: [PATCH 44/45] Update uv.lock --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 7121cfac1c..a932ab5ee9 100644 --- a/uv.lock +++ b/uv.lock @@ -807,8 +807,8 @@ wheels = [ [[package]] name = "dls-dodal" -version = "2.3.1.dev6+g067e07a27" -source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=device_changes_for_vmxm_ispyb#067e07a273228cc210b40fc0a3e763b4cbbb4cc8" } +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'" }, @@ -2118,7 +2118,7 @@ requires-dist = [ { name = "caproto" }, { name = "daq-config-server", specifier = ">=1.3.0" }, { name = "deepdiff" }, - { name = "dls-dodal", git = "https://github.com/DiamondLightSource/dodal.git?rev=device_changes_for_vmxm_ispyb" }, + { name = "dls-dodal", git = "https://github.com/DiamondLightSource/dodal.git?rev=main" }, { name = "fastapi", extras = ["all"] }, { name = "flask-restful" }, { name = "jupyterlab" }, From a9bbe0f75f8bd4e601790fe84fbd01d77232f626 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Fri, 15 May 2026 11:00:31 +0100 Subject: [PATCH 45/45] Fix i02-1 gridscan unit test --- tests/unit_tests/beamlines/i02_1/test_i02_1_gridscan_plan.py | 1 + 1 file changed, 1 insertion(+) 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 a4ee1fd79d..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 @@ -185,6 +185,7 @@ def test_i02_1_flyscan_xray_centre_in_re( 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 )