From a94ab9819b04919a006f27d9f35a8e3406e60137 Mon Sep 17 00:00:00 2001 From: Oliver Silvester Date: Mon, 9 Feb 2026 17:35:55 +0000 Subject: [PATCH 01/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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 28cff713e2b79daef66a396e2574d0d7a87f7f20 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Tue, 5 May 2026 11:27:16 +0100 Subject: [PATCH 22/28] 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 23/28] 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 e5dcb4e287e710b8d8985f45c74a377795232236 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Wed, 13 May 2026 11:20:42 +0100 Subject: [PATCH 24/28] Make ruff happy --- .../beamlines/i02_1/test_setup_zebra.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/tests/unit_tests/beamlines/i02_1/test_setup_zebra.py b/tests/unit_tests/beamlines/i02_1/test_setup_zebra.py index cd9d02c37b..3596a83ffa 100644 --- a/tests/unit_tests/beamlines/i02_1/test_setup_zebra.py +++ b/tests/unit_tests/beamlines/i02_1/test_setup_zebra.py @@ -22,15 +22,20 @@ async def test_zebra_set_up_for_gridscan( 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, + 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, + lambda msg: ( + msg.command == "wait" + and msg.kwargs["group"] + == PlanGroupCheckpointConstants.SETUP_ZEBRA_FOR_GRIDSCAN + ), ) @@ -41,13 +46,18 @@ async def test_tidy_up_zebra_after_gridscan( 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, + 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, + lambda msg: ( + msg.command == "wait" + and msg.kwargs["group"] + == PlanGroupCheckpointConstants.TIDY_ZEBRA_AFTER_GRIDSCAN + ), ) From 7ede7d8736a01a11e69071f925414c6923ac9c03 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Wed, 13 May 2026 11:29:55 +0100 Subject: [PATCH 25/28] Update uv.lock --- uv.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uv.lock b/uv.lock index b121da86d7..b7fd9c53b3 100644 --- a/uv.lock +++ b/uv.lock @@ -807,8 +807,8 @@ wheels = [ [[package]] name = "dls-dodal" -version = "2.2.4.dev7+g0beeb3135" -source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#0beeb3135ae40248df69aa3c4d04ec43af055436" } +version = "2.3.1.dev4+gdd687cce0" +source = { git = "https://github.com/DiamondLightSource/dodal.git?rev=main#dd687cce03e1a18ba9a0698d7ebc1e8f6b460ad9" } dependencies = [ { name = "aiofiles", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "aiohttp", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, From a6e89af773547636932c1a5792fd58b454e28972 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Wed, 13 May 2026 12:56:14 +0100 Subject: [PATCH 26/28] Rename composite as suggested --- src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py | 10 +++++----- .../beamlines/i02_1/test_i02_1_gridscan_plan.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 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..3c7626e911 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 @@ -64,7 +64,7 @@ def create_gridscan_callbacks() -> tuple[ @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True}) -class FlyScanXRayCentreComposite(FlyScanEssentialDevices[GonioWithOmegaType]): +class I021FlyScanXRayCentreComposite(FlyScanEssentialDevices[GonioWithOmegaType]): """All devices which are directly or indirectly required by this plan""" zebra: Zebra @@ -77,7 +77,7 @@ class FlyScanXRayCentreComposite(FlyScanEssentialDevices[GonioWithOmegaType]): def construct_i02_1_specific_features( - fgs_composite: FlyScanXRayCentreComposite, + fgs_composite: I021FlyScanXRayCentreComposite, parameters: SpecifiedTwoDGridScan, ) -> BeamlineSpecificFGSFeatures: signals_to_read_pre_flyscan = [ @@ -111,12 +111,12 @@ def construct_i02_1_specific_features( ) -def _zebra_triggering_setup(fgs_composite: FlyScanXRayCentreComposite, _): +def _zebra_triggering_setup(fgs_composite: I021FlyScanXRayCentreComposite, _): yield from setup_zebra_for_gridscan(fgs_composite.zebra) def _tidy_plan( - fgs_composite: FlyScanXRayCentreComposite, group, wait=True + fgs_composite: I021FlyScanXRayCentreComposite, group, wait=True ) -> MsgGenerator: LOGGER.info("Tidying up Zebra") yield from tidy_up_zebra_after_gridscan(fgs_composite.zebra) @@ -124,7 +124,7 @@ def _tidy_plan( def i02_1_gridscan_plan( parameters: SpecifiedTwoDGridScan, - composite: FlyScanXRayCentreComposite = inject(""), + composite: I021FlyScanXRayCentreComposite = inject(""), ) -> MsgGenerator: """BlueAPI entry point for i02-1 grid scans""" 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..fb6dff13c5 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 @@ -15,7 +15,7 @@ from pydantic import ValidationError from mx_bluesky.beamlines.i02_1.i02_1_gridscan_plan import ( - FlyScanXRayCentreComposite, + I021FlyScanXRayCentreComposite, construct_i02_1_specific_features, i02_1_gridscan_plan, ) @@ -63,8 +63,8 @@ def fgs_composite( undulator: BaseUndulator, s4_slit_gaps: S4SlitGaps, zebra: Zebra, -) -> FlyScanXRayCentreComposite: - return FlyScanXRayCentreComposite( +) -> I021FlyScanXRayCentreComposite: + return I021FlyScanXRayCentreComposite( eiger, synchrotron, smargon, @@ -136,7 +136,7 @@ def test_i02_1_flyscan_xray_centre_in_re( mock_create_features: MagicMock, run_engine: RunEngine, fgs_params_two_d: SpecifiedTwoDGridScan, - fgs_composite: FlyScanXRayCentreComposite, + fgs_composite: I021FlyScanXRayCentreComposite, ): expected_features = construct_i02_1_specific_features( fgs_composite, fgs_params_two_d From d0a8f0c4afa4fd46a602dd4acc768ba974652c15 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Wed, 13 May 2026 13:06:54 +0100 Subject: [PATCH 27/28] Unpin dodal --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6d3b5b3812..6ade793d98 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@2024_decouple_interlock_from_shutter", + "dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@main", ] From dfc3863e7d3169bbc763e5268af53b109f49e3e9 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Wed, 13 May 2026 13:35:01 +0100 Subject: [PATCH 28/28] Add todo --- src/mx_bluesky/common/parameters/gridscan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index d709002833..184217687e 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -31,6 +31,7 @@ HardwareConstants, ) +# TODO Remove this https://github.com/DiamondLightSource/mx-bluesky/issues/1748 DETECTOR_SIZE_PER_BEAMLINE = { "test": EIGER2_X_16M_SIZE, "i02-1": EIGER2_X_4M_SIZE,