From 26bfe91f8237dcf917f83924f055bb3002234a1c Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Fri, 12 Jun 2026 15:41:55 +0100 Subject: [PATCH 1/4] Add GridScanParams, initial fixups --- .../beamlines/i02_1/i02_1_gridscan_plan.py | 18 +++- .../common_flyscan_xray_centre_plan.py | 17 +++- ...ommon_grid_detect_then_xray_centre_plan.py | 94 +++++++++++------- .../oav_grid_detection_plan.py | 6 +- .../common/grid_detection_callback.py | 26 ++--- src/mx_bluesky/common/parameters/gridscan.py | 82 +++++++++++++++- .../hyperion_flyscan_xray_centre_plan.py | 24 +++-- .../hyperion/parameters/gridscan.py | 96 ++++++++++--------- .../test_grid_detection_plan.py | 4 +- 9 files changed, 255 insertions(+), 112 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 cbeaaab8c2..6851bad8e2 100644 --- a/src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py +++ b/src/mx_bluesky/beamlines/i02_1/i02_1_gridscan_plan.py @@ -57,7 +57,7 @@ from mx_bluesky.common.parameters.device_composites import ( FlyScanEssentialDevices, ) -from mx_bluesky.common.parameters.gridscan import PositiveFloat +from mx_bluesky.common.parameters.gridscan import GridScanParams, PositiveFloat from mx_bluesky.common.utils.log import LOGGER DEFAULT_BOX_SIZE_UM = 2 @@ -127,7 +127,7 @@ def construct_i02_1_specific_features( ) -def _zebra_triggering_setup(fgs_composite: I021FlyScanXRayCentreComposite, _): +def _zebra_triggering_setup(fgs_composite: I021FlyScanXRayCentreComposite, _, __): yield from setup_zebra_for_gridscan(fgs_composite.zebra) @@ -210,10 +210,22 @@ def i02_1_gridscan_plan( beamline_specific = construct_i02_1_specific_features(composite, params) callbacks = create_gridscan_callbacks(params) + grid_scan_params = GridScanParams( + omega_starts_deg=params.omega_starts_deg, + x_start_um=params.x_start_um, + y_starts_um=params.y_starts_um, + z_starts_um=params.z_starts_um, + x_steps=params.x_steps, + y_steps=params.y_steps, + x_step_size_um=params.x_step_size_um, + y_step_sizes_um=params.y_step_sizes_um, + ) @bpp.subs_decorator(callbacks) @ispyb_activation_decorator(params) def decorated_flyscan_plan(): - yield from common_flyscan_xray_centre(composite, params, beamline_specific) + yield from common_flyscan_xray_centre( + composite, params, grid_scan_params, beamline_specific + ) yield from decorated_flyscan_plan() diff --git a/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py b/src/mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py index 751790f336..962f4d5822 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 @@ -31,6 +31,7 @@ GonioWithOmegaType, ) from mx_bluesky.common.parameters.gridscan import ( + GridScanParams, SpecifiedGrids, ) from mx_bluesky.common.utils.exceptions import ( @@ -117,6 +118,7 @@ def construct_beamline_specific_fast_gridscan_features( def common_flyscan_xray_centre( composite: FlyScanEssentialDevices[GonioWithOmegaType], parameters: SpecifiedGrids, + grid_scan_parameters: GridScanParams, beamline_specific: BeamlineSpecificFGSFeatures, ) -> MsgGenerator: """Main entry point of the MX-Bluesky x-ray centering flyscan @@ -125,6 +127,7 @@ def common_flyscan_xray_centre( composite (FlyScanEssentialDevices): Devices required to perform this plan. parameters (SpecifiedThreeDGridScan): Parameters required to perform this plan. + grid_scan_parameters (GridScanParams): Parameters defining the grid scan(s) to be carried out. beamline_specific (BeamlineSpecificFGSFeatures): Configure the beamline-specific version of this plan: For example triggering setup and tidy up plans, as well as what to do with the @@ -150,6 +153,7 @@ def _decorated_flyscan(): md={ "subplan_name": PlanNameConstants.GRIDSCAN_OUTER, "mx_bluesky_parameters": parameters.model_dump_json(), + "grid_scan_parameters": grid_scan_parameters, "activate_callbacks": [ "GridscanNexusFileCallback", ], @@ -161,10 +165,14 @@ def run_gridscan_and_tidy( params: SpecifiedGrids, beamline_specific: BeamlineSpecificFGSFeatures, ) -> MsgGenerator: - yield from beamline_specific.setup_trigger_plan(fgs_composite, parameters) + yield from beamline_specific.setup_trigger_plan( + fgs_composite, parameters, grid_scan_parameters + ) LOGGER.info("Starting grid scan") - yield from run_gridscan(fgs_composite, params, beamline_specific) + yield from run_gridscan( + fgs_composite, params, grid_scan_parameters, beamline_specific + ) LOGGER.info("Grid scan finished") @@ -177,12 +185,13 @@ def run_gridscan_and_tidy( def run_gridscan( fgs_composite: FlyScanEssentialDevices[GonioWithOmegaType], parameters: SpecifiedGrids, + grid_scan_params: GridScanParams, beamline_specific: BeamlineSpecificFGSFeatures, ): with TRACER.start_span("moving_omega_to_0"): yield from bps.abs_set( fgs_composite.gonio.wrapped_omega.phase, - parameters.omega_starts_deg[0], + grid_scan_params.omega_starts_deg[0], wait=True, ) @@ -209,7 +218,7 @@ def run_gridscan( beamline_specific.fgs_motors, fgs_composite.eiger, fgs_composite.synchrotron, - parameters.scan_points, + grid_scan_params.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 46d77f7ead..d7de6ef00e 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 @@ -43,7 +43,12 @@ FlyScanEssentialDevices, GridDetectThenXRayCentreComposite, ) -from mx_bluesky.common.parameters.gridscan import GenericGrid, SpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import ( + GenericGrid, + GridDetectionParams, + GridScanParams, + SpecifiedThreeDGridScan, +) from mx_bluesky.common.utils.log import LOGGER TFlyScanEssentialDevices = TypeVar( @@ -102,6 +107,9 @@ def detect_grid_and_do_gridscan( xrc_params_type: type[SpecifiedThreeDGridScan], construct_beamline_specific: ConstructBeamlineSpecificFeatures, ): + grid_detect_params = GridDetectionParams( + box_size_um=parameters.box_size_um, grid_width_um=parameters.grid_width_um + ) snapshot_template = f"{parameters.detector_params.prefix}_{parameters.detector_params.run_number}_{{angle}}" grid_params_callback = GridDetectionCallback() @@ -113,28 +121,6 @@ def detect_grid_and_do_gridscan( wait=True, ) - @bpp.subs_decorator([grid_params_callback]) - def run_grid_detection_plan( - oav_params, - snapshot_template, - snapshot_dir: Path, - ): - grid_detect_composite = OavGridDetectionComposite( - backlight=composite.backlight, - oav=composite.oav, - gonio=composite.gonio, - pin_tip_detection=composite.pin_tip_detection, - ) - - yield from grid_detection_plan( - grid_detect_composite, - oav_params, - snapshot_template, - str(snapshot_dir), - parameters.grid_width_um, - parameters.box_size_um, - ) - if parameters.selected_aperture: # Start moving the aperture/scatterguard into position without moving it in yield from bps.prepare( @@ -143,10 +129,15 @@ def run_grid_detection_plan( group=PlanGroupCheckpointConstants.PREPARE_APERTURE, ) - yield from run_grid_detection_plan( - oav_params, - snapshot_template, - parameters.snapshot_directory, + yield from bpp.subs_wrapper( + _run_grid_detection_plan( + composite, + grid_detect_params, + oav_params, + snapshot_template, + parameters.snapshot_directory, + ), + grid_params_callback, ) yield from bps.abs_set( @@ -161,13 +152,41 @@ def run_grid_detection_plan( parameters.selected_aperture, group=PlanGroupCheckpointConstants.GRID_READY_FOR_DC, ) - xrc_params = create_parameters_for_flyscan_xray_centre( + xrc_params, grid_scan_params = create_parameters_for_flyscan_xray_centre( parameters, grid_params_callback.get_grid_parameters(), xrc_params_type ) parameters.set_specified_grid_params(xrc_params) - beamline_specific = construct_beamline_specific(composite, xrc_params) + beamline_specific = construct_beamline_specific( + composite, xrc_params, grid_scan_params + ) + + yield from common_flyscan_xray_centre( + composite, xrc_params, grid_scan_params, beamline_specific + ) - yield from common_flyscan_xray_centre(composite, xrc_params, beamline_specific) + +def _run_grid_detection_plan( + composite: GridDetectThenXRayCentreComposite, + grid_detect_params: GridDetectionParams, + oav_params: OAVParameters, + snapshot_template: str, + snapshot_dir: Path, +): + grid_detect_composite = OavGridDetectionComposite( + backlight=composite.backlight, + oav=composite.oav, + gonio=composite.gonio, + pin_tip_detection=composite.pin_tip_detection, + ) + + yield from grid_detection_plan( + grid_detect_composite, + oav_params, + snapshot_template, + str(snapshot_dir), + grid_detect_params.grid_width_um, + grid_detect_params.box_size_um, + ) class ConstructBeamlineSpecificFeatures( @@ -177,6 +196,7 @@ def __call__( self, xrc_composite: TFlyScanEssentialDevices, xrc_parameters: TSpecifiedThreeDGridScan, + grid_scan_parameters: GridScanParams, ) -> BeamlineSpecificFGSFeatures: ... @@ -184,9 +204,19 @@ def create_parameters_for_flyscan_xray_centre( parameters: GenericGrid, grid_parameters: GridParamUpdate, xrc_params_type: type[SpecifiedThreeDGridScan], -) -> SpecifiedThreeDGridScan: +) -> tuple[SpecifiedThreeDGridScan, GridScanParams]: + grid_scan_params = GridScanParams( + omega_starts_deg=parameters.omega_starts_deg, + x_start_um=grid_parameters["x_start_um"], + y_starts_um=grid_parameters["y_starts_um"], + z_starts_um=grid_parameters["z_starts_um"], + x_steps=grid_parameters["x_steps"], + y_steps=grid_parameters["y_steps"], + x_step_size_um=grid_parameters["x_step_size_um"], + y_step_sizes_um=grid_parameters["y_step_sizes_um"], + ) params_json = parameters.model_dump() params_json.update(grid_parameters) flyscan_xray_centre_parameters = xrc_params_type(**params_json) LOGGER.info(f"Parameters for FGS: {flyscan_xray_centre_parameters}") - return flyscan_xray_centre_parameters + return flyscan_xray_centre_parameters, grid_scan_params diff --git a/src/mx_bluesky/common/experiment_plans/oav_grid_detection_plan.py b/src/mx_bluesky/common/experiment_plans/oav_grid_detection_plan.py index a018e43e1a..6eb55a5d97 100644 --- a/src/mx_bluesky/common/experiment_plans/oav_grid_detection_plan.py +++ b/src/mx_bluesky/common/experiment_plans/oav_grid_detection_plan.py @@ -64,7 +64,7 @@ def optimum_grid_detect_angles(smargon: Smargon) -> MsgGenerator[list[float]]: def grid_detection_plan( composite: OavGridDetectionComposite, - parameters: OAVParameters, + oav_parameters: OAVParameters, snapshot_template: str, snapshot_dir: str, grid_width_microns: float, @@ -76,7 +76,7 @@ def grid_detection_plan( Args: composite (OavGridDetectionComposite): Composite containing devices for doing a grid detection. - parameters (OAVParameters): Object containing parameters for setting up the OAV + oav_parameters (OAVParameters): Object containing parameters for setting up the OAV snapshot_template (str): A template for the name of the snapshots, expected to be filled in with an angle snapshot_dir (str): The location to save snapshots grid_width_microns (float): The width of the grid to scan in microns @@ -91,7 +91,7 @@ def grid_detection_plan( yield from bps.wait() # Set relevant PVs to whatever the config dictates. - yield from pre_centring_setup_oav(oav, parameters, pin_tip_detection) + yield from pre_centring_setup_oav(oav, oav_parameters, pin_tip_detection) LOGGER.info("OAV Centring: Camera set up") 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 77949d07ad..dfbd3ba6c6 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 @@ -12,21 +12,23 @@ class GridParamUpdate(TypedDict): """ - Grid parameters extracted from the grid detection. + Generate grid parameters extracted from the grid detection for two 2D gridscans, one in xy-plane and the + second in the xz-plane. + + Note, in this class y refers to both the y plane in absolute coordinate space, and to the secondary axis + for the 2D gridscan (i.e the y' axis where in the first gridscan x', y' = x, y, and in the + second gridscan x', y' = x, z) + Positions are in motor-space. Attributes: - x_start_um: x position of the centre of the first xy-gridscan box in microns - y_start_um: y position of the centre of the first xy-gridscan box in microns - y2_start_um: y position of the centre of the first xz-gridscan box in microns - z_start_um: z position of the centre of the first xy-gridscan box in microns - z2_start_um: z position of the centre of the first xz-gridscan box in microns - x_steps: Number of grid boxes in x-direction for xy- and xz- gridscans - y_steps: Number of grid boxes in y-direction for xy-gridscan - z_steps: Number of grid boxes in z-direction for xz-gridscan - x_step_size_um: x-dimension of the grid box - y_step_size_um: y-dimension of the grid box - z_step_size_um: z-dimension of the grid box + x_start_um: x position of the centre of the first xy-gridscan box in microns for all gridscans + y_starts_um: y positions of the centre of the first xy-gridscan and xz-gridscan box in microns respectively + z_starts_um: z position of the centre of the first xy-gridscan and xz-gridscan box in microns respectively + x_steps: Number of grid boxes in x'-direction for xy- and xz- gridscans + y_steps: Number of grid boxes in y'-direction for xy-gridscan and xz-gridscan respectively + x_step_size_um: x'-dimension of the grid box + y_step_sizes_um: y'-dimension of the grid box for xy-gridscan and xz-gridscan respectively """ x_start_um: float diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 9203252f58..700a7ab631 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -11,7 +11,7 @@ ZebraGridScanParamsThreeD, ) from dodal.utils import get_beamline_name -from pydantic import Field, PrivateAttr, model_validator +from pydantic import BaseModel, Field, PrivateAttr, model_validator from scanspec.core import AxesPoints from scanspec.core import Path as ScanPath from scanspec.specs import Concat, Line, Product, Static @@ -45,6 +45,80 @@ ) +class GridDetectionParams(BaseModel): + box_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM) + grid_width_um: float = Field(default=GridscanParamConstants.PIN_WIDTH_UM) + + +class GridScanParams(BaseModel): + omega_starts_deg: list[int] = Field( + default=[GridscanParamConstants.OMEGA_1, GridscanParamConstants.OMEGA_2] + ) + x_start_um: float + y_starts_um: list[float] + z_starts_um: list[float] + x_steps: PositiveInt # See https://github.com/DiamondLightSource/mx-bluesky/issues/1632 for this not being a list + y_steps: list[PositiveInt] + x_step_size_um: PositiveFloat = Field( + default=GridscanParamConstants.BOX_WIDTH_UM + ) # See https://github.com/DiamondLightSource/mx-bluesky/issues/1632 for this not being a list + # 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 + ) + + @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, + } + + 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() + ) + + raise ValueError("Fields must all have the same length:\n" + details) + + return self + + @property + def num_grids(self): + return len(self.y_steps) + + @property + 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_steps - 1) + y_end = self.y_starts_um[idx] + self.y_step_sizes_um[idx] * ( + 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]) + 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_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 + + class GenericGrid( DiffractionExperimentWithSample, OptionalGonioAngleStarts, @@ -57,8 +131,10 @@ class GenericGrid( Params in GenericGrid currently must be the same for each grid in the gridscan. """ + # TODO remove these - now used in GridDetectionParams box_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM) grid_width_um: float = Field(default=GridscanParamConstants.PIN_WIDTH_UM) + exposure_time_s: float = Field(default=GridscanParamConstants.EXPOSURE_TIME_S) ispyb_experiment_type: IspybExperimentType = Field( @@ -120,13 +196,13 @@ class SpecifiedGrids(GenericGrid, XyzStarts, WithScan, Generic[GridScanParamType # See https://github.com/DiamondLightSource/mx-bluesky/issues/1634 for a better structure for this # class + # TODO remove these - now used in GridScanParams omega_starts_deg: list[int] = Field( default=[GridscanParamConstants.OMEGA_1, GridscanParamConstants.OMEGA_2] ) x_step_size_um: PositiveFloat = Field( default=GridscanParamConstants.BOX_WIDTH_UM ) # See https://github.com/DiamondLightSource/mx-bluesky/issues/1632 for this not being a list - # 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( @@ -134,6 +210,7 @@ class SpecifiedGrids(GenericGrid, XyzStarts, WithScan, Generic[GridScanParamType ) 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) @model_validator(mode="after") @@ -213,6 +290,7 @@ def scan_spec(self) -> Product[str] | Concat[str]: ) return _scan_spec + # TODO remove this in favour of GridScanParams.scan_points @property def scan_points(self) -> list[AxesPoints[str]]: """A list of all the points in the scan_spec for each grid.""" diff --git a/src/mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py b/src/mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py index fb474106c5..3e092f1057 100755 --- a/src/mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py @@ -17,6 +17,7 @@ from mx_bluesky.common.experiment_plans.common_flyscan_xray_centre_plan import ( construct_beamline_specific_fast_gridscan_features, ) +from mx_bluesky.common.parameters.gridscan import GridScanParams from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.device_setup_plans.setup_panda import ( disarm_panda_for_gridscan, @@ -32,7 +33,11 @@ from mx_bluesky.hyperion.parameters.device_composites import ( HyperionFlyScanXRayCentreComposite, ) -from mx_bluesky.hyperion.parameters.gridscan import HyperionSpecifiedThreeDGridScan +from mx_bluesky.hyperion.parameters.gridscan import ( + HyperionSpecifiedThreeDGridScan, + fast_gridscan_params, + panda_fast_gridscan_params, +) class SmargonSpeedError(Exception): @@ -42,6 +47,7 @@ class SmargonSpeedError(Exception): def construct_hyperion_specific_features( xrc_composite: HyperionFlyScanXRayCentreComposite, xrc_parameters: HyperionSpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, ): """ Get all the information needed to do the Hyperion-specific parts of the XRC flyscan. @@ -70,10 +76,11 @@ def construct_hyperion_specific_features( if get_hyperion_feature_settings().USE_PANDA_FOR_GRIDSCAN: setup_trigger_plan = _panda_triggering_setup tidy_plan = partial(_panda_tidy, xrc_composite) + panda_fgs_params = panda_fast_gridscan_params(xrc_parameters, grid_scan_params) set_flyscan_params_plan = partial( set_fast_grid_scan_params, xrc_composite.panda_fast_grid_scan, - xrc_parameters.panda_fast_gridscan_params, + panda_fgs_params, ) fgs_motors = xrc_composite.panda_fast_grid_scan @@ -88,10 +95,11 @@ def construct_hyperion_specific_features( group="flyscan_zebra_tidy", wait=True, ) + zebra_fgs_params = fast_gridscan_params(xrc_parameters, grid_scan_params) set_flyscan_params_plan = partial( set_fast_grid_scan_params, xrc_composite.zebra_fast_grid_scan, - xrc_parameters.fast_gridscan_params, + zebra_fgs_params, ) fgs_motors = xrc_composite.zebra_fast_grid_scan return construct_beamline_specific_fast_gridscan_features( @@ -118,6 +126,7 @@ def _panda_tidy(xrc_composite: HyperionFlyScanXRayCentreComposite): def _panda_triggering_setup( xrc_composite: HyperionFlyScanXRayCentreComposite, parameters: HyperionSpecifiedThreeDGridScan, + grid_scan_parameters: GridScanParams, ) -> MsgGenerator: LOGGER.info("Setting up Panda for flyscan") @@ -131,15 +140,14 @@ def _panda_triggering_setup( smargon_speed_limit_mm_per_s = yield from bps.rd(xrc_composite.gonio.x.max_velocity) + panda_params = panda_fast_gridscan_params(parameters, grid_scan_parameters) sample_velocity_mm_per_s = ( - parameters.panda_fast_gridscan_params.x_step_size_mm - * 1e3 - / time_between_x_steps_ms + panda_params.x_step_size_mm * 1e3 / time_between_x_steps_ms ) if sample_velocity_mm_per_s > smargon_speed_limit_mm_per_s: raise SmargonSpeedError( f"Smargon speed was calculated from x step size\ - {parameters.panda_fast_gridscan_params.x_step_size_mm}mm and\ + {panda_params.x_step_size_mm}mm and\ time_between_x_steps_ms {time_between_x_steps_ms} as\ {sample_velocity_mm_per_s}mm/s. The smargon's speed limit is\ {smargon_speed_limit_mm_per_s}mm/s." @@ -160,7 +168,7 @@ def _panda_triggering_setup( yield from setup_panda_for_flyscan( xrc_composite.panda, - parameters.panda_fast_gridscan_params, + panda_params, xrc_composite.gonio, parameters.exposure_time_s, time_between_x_steps_ms, diff --git a/src/mx_bluesky/hyperion/parameters/gridscan.py b/src/mx_bluesky/hyperion/parameters/gridscan.py index 360ec5c04d..b098edd725 100644 --- a/src/mx_bluesky/hyperion/parameters/gridscan.py +++ b/src/mx_bluesky/hyperion/parameters/gridscan.py @@ -5,8 +5,10 @@ ZebraGridScanParamsThreeD, ) +from mx_bluesky.common.parameters.components import DiffractionExperiment from mx_bluesky.common.parameters.gridscan import ( GenericGrid, + GridScanParams, SpecifiedThreeDGridScan, ) from mx_bluesky.hyperion.external_interaction.config_server import ( @@ -38,53 +40,55 @@ def detector_params(self): params.enable_dev_shm = get_hyperion_feature_settings().USE_GPU_RESULTS return params - # Relative to common grid scan, stub offsets are defined by config server - @property - def fast_gridscan_params(self) -> ZebraGridScanParamsThreeD: - """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, - 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_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_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_feature_settings().SET_STUB_OFFSETS, - dwell_time_ms=self.exposure_time_s * 1000, - transmission_fraction=self.transmission_frac, - ) - @property - def panda_fast_gridscan_params(self) -> PandAGridScanParams: - 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[0], - z_steps=self.y_steps[1], - x_step_size_mm=self.x_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_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_feature_settings().SET_STUB_OFFSETS, - run_up_distance_mm=get_hyperion_feature_settings().PANDA_RUNUP_DISTANCE_MM, - transmission_fraction=self.transmission_frac, - ) +# Relative to common grid scan, stub offsets are defined by config server +def fast_gridscan_params( + expt_params: DiffractionExperiment, grid_scan_params: GridScanParams +) -> ZebraGridScanParamsThreeD: + """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=grid_scan_params.x_steps, + y_steps=grid_scan_params.y_steps[0], + z_steps=grid_scan_params.y_steps[1], + x_step_size_mm=grid_scan_params.x_step_size_um / 1000, + y_step_size_mm=grid_scan_params.y_step_sizes_um[0] / 1000, + z_step_size_mm=grid_scan_params.y_step_sizes_um[1] / 1000, + x_start_mm=grid_scan_params.x_start_um / 1000, + y1_start_mm=grid_scan_params.y_starts_um[0] / 1000, + z1_start_mm=grid_scan_params.z_starts_um[0] / 1000, + y2_start_mm=grid_scan_params.y_starts_um[1] / 1000, + z2_start_mm=grid_scan_params.z_starts_um[1] / 1000, + set_stub_offsets=get_hyperion_feature_settings().SET_STUB_OFFSETS, + dwell_time_ms=expt_params.exposure_time_s * 1000, + transmission_fraction=expt_params.transmission_frac, + ) + + +def panda_fast_gridscan_params( + expt_params: DiffractionExperiment, grid_scan_params: GridScanParams +) -> PandAGridScanParams: + if grid_scan_params.y_steps[0] % 2 and grid_scan_params.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=grid_scan_params.x_steps, + y_steps=grid_scan_params.y_steps[0], + z_steps=grid_scan_params.y_steps[1], + x_step_size_mm=grid_scan_params.x_step_size_um / 1000, + y_step_size_mm=grid_scan_params.y_step_sizes_um[0] / 1000, + z_step_size_mm=grid_scan_params.y_step_sizes_um[1] / 1000, + x_start_mm=grid_scan_params.x_start_um / 1000, + y1_start_mm=grid_scan_params.y_starts_um[0] / 1000, + z1_start_mm=grid_scan_params.z_starts_um[0] / 1000, + y2_start_mm=grid_scan_params.y_starts_um[1] / 1000, + z2_start_mm=grid_scan_params.z_starts_um[1] / 1000, + set_stub_offsets=get_hyperion_feature_settings().SET_STUB_OFFSETS, + run_up_distance_mm=get_hyperion_feature_settings().PANDA_RUNUP_DISTANCE_MM, + transmission_fraction=expt_params.transmission_frac, + ) class OddYStepsError(Exception): ... 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 d4c9394d59..94af973b26 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 @@ -100,7 +100,7 @@ def fake_devices( def do_grid_and_edge_detect(composite, parameters, tmp_dir): yield from grid_detection_plan( composite, - parameters=parameters, + oav_parameters=parameters, snapshot_dir=f"{tmp_dir}", snapshot_template="test_{angle}", grid_width_microns=161.2, @@ -179,7 +179,7 @@ async def test_given_when_grid_detect_then_start_position_as_expected( def decorated(): yield from grid_detection_plan( composite, - parameters=params, + oav_parameters=params, snapshot_dir=f"{tmp_path}", snapshot_template="test_{angle}", grid_width_microns=161.2, From 2954544eded1fc8b27702ae9c97120cf1e097f4d Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Mon, 15 Jun 2026 10:41:32 +0100 Subject: [PATCH 2/4] Refactor grid scan params - separate grid scan and grid detect params --- .../i04_grid_detect_then_xray_centre_plan.py | 9 ++- .../change_aperture_then_move_plan.py | 16 +++- ...ommon_grid_detect_then_xray_centre_plan.py | 30 +++++--- .../inner_plans/xrc_results_utils.py | 57 ++++++++++---- src/mx_bluesky/common/parameters/gridscan.py | 44 ++++++----- .../pin_centre_then_gridscan_plan.py | 18 +++-- tests/conftest.py | 19 ++++- .../callbacks/test_external_callbacks.py | 22 +++++- .../i02_1/test_i02_1_gridscan_plan.py | 4 +- ...t_i04_grid_detect_then_xray_centre_plan.py | 48 ++++++++++-- .../test_change_aperture_then_move_plan.py | 17 ++++- .../test_common_flyscan_xray_centre_plan.py | 75 ++++++++++++++++--- ...ommon_grid_detect_then_xray_centre_plan.py | 50 +++++++++---- .../common/utils/test_xrc_result.py | 33 ++++++-- .../hyperion/experiment_plans/conftest.py | 18 ++++- .../test_hyperion_flyscan_xray_centre_plan.py | 31 +++++++- .../test_pin_centre_then_xray_centre_plan.py | 14 +++- .../nexus/test_write_nexus.py | 33 +++++--- .../parameters/test_parameter_model.py | 15 +++- 19 files changed, 426 insertions(+), 127 deletions(-) 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 03f4d81e5a..ef37178107 100644 --- a/src/mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +++ b/src/mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py @@ -84,7 +84,9 @@ ) from mx_bluesky.common.parameters.gridscan import ( GenericGrid, + GridScanParams, SpecifiedThreeDGridScan, + fast_gridscan_params, ) from mx_bluesky.common.preprocessors.preprocessors import ( set_transmission_and_trigger_xbpm_feedback_before_collection_decorator, @@ -224,7 +226,7 @@ def _inner_grid_detect_then_xrc(): PlanNameConstants.GRIDSCAN_OUTER, ) def grid_detect_then_xray_centre_with_callbacks(): - yield from grid_detect_then_xray_centre( + grid_scan_params = yield from grid_detect_then_xray_centre( composite=composite, parameters=grid_common_params, xrc_params_type=SpecifiedThreeDGridScan, @@ -239,6 +241,7 @@ def grid_detect_then_xray_centre_with_callbacks(): yield from get_results_and_move_to_xtal( composite, grid_common_params.specified_grid_params, + grid_scan_params, flyscan_event_handler, ) except CrystalNotFoundError: @@ -297,6 +300,7 @@ def create_gridscan_callbacks() -> tuple[ def construct_i04_specific_features( xrc_composite: GridDetectThenXRayCentreComposite, xrc_parameters: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, ) -> BeamlineSpecificFGSFeatures: """ Get all the information needed to do the i04 XRC flyscan. @@ -327,10 +331,11 @@ def construct_i04_specific_features( group="flyscan_zebra_tidy", wait=True, ) + zebra_fgs_params = fast_gridscan_params(xrc_parameters, grid_scan_params) set_flyscan_params_plan = partial( set_fast_grid_scan_params, xrc_composite.zebra_fast_grid_scan, - xrc_parameters.fast_gridscan_params, + zebra_fgs_params, ) fgs_motors = xrc_composite.zebra_fast_grid_scan return construct_beamline_specific_fast_gridscan_features( diff --git a/src/mx_bluesky/common/experiment_plans/change_aperture_then_move_plan.py b/src/mx_bluesky/common/experiment_plans/change_aperture_then_move_plan.py index 860a5ed7a4..6f42c54b82 100644 --- a/src/mx_bluesky/common/experiment_plans/change_aperture_then_move_plan.py +++ b/src/mx_bluesky/common/experiment_plans/change_aperture_then_move_plan.py @@ -15,7 +15,10 @@ from mx_bluesky.common.parameters.device_composites import ( GridDetectThenXRayCentreComposite, ) -from mx_bluesky.common.parameters.gridscan import SpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import ( + GridScanParams, + SpecifiedThreeDGridScan, +) from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.common.utils.tracing import TRACER from mx_bluesky.common.utils.xrc_result import XRayCentreEventHandler, XRayCentreResult @@ -24,9 +27,12 @@ def _get_xrc_results( zocalo: ZocaloResults, parameters: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, flyscan_event_handler: XRayCentreEventHandler, ) -> Generator[Any, Any, Sequence[XRayCentreResult]]: - yield from fetch_xrc_results_from_zocalo(zocalo, parameters) + yield from fetch_xrc_results_from_zocalo( + zocalo, grid_scan_params, parameters.sample_id + ) flyscan_results = flyscan_event_handler.xray_centre_results assert flyscan_results, ( "Flyscan result event not received or no crystal found and exception not raised" @@ -37,10 +43,11 @@ def _get_xrc_results( def get_results_and_move_to_xtal( composite: GridDetectThenXRayCentreComposite, parameters: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, flyscan_event_handler: XRayCentreEventHandler, ): flyscan_results = yield from _get_xrc_results( - composite.zocalo, parameters, flyscan_event_handler + composite.zocalo, parameters, grid_scan_params, flyscan_event_handler ) yield from move_to_xtal(flyscan_results[0], composite.gonio) @@ -49,10 +56,11 @@ def get_results_and_move_to_xtal( def get_results_then_change_aperture_and_move_to_xtal( composite: GridDetectThenXRayCentreComposite, parameters: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, flyscan_event_handler: XRayCentreEventHandler, ): flyscan_results = yield from _get_xrc_results( - composite.zocalo, parameters, flyscan_event_handler + composite.zocalo, parameters, grid_scan_params, flyscan_event_handler ) yield from change_aperture(flyscan_results[0], composite.aperture_scatterguard) yield from move_to_xtal(flyscan_results[0], composite.gonio) 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 d7de6ef00e..07adf0c906 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 @@ -33,7 +33,7 @@ GridParamUpdate, ) from mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback import ( - ispyb_activation_wrapper, + ispyb_activation_decorator, ) from mx_bluesky.common.parameters.constants import ( OavConstants, @@ -65,7 +65,7 @@ def grid_detect_then_xray_centre( xrc_params_type: type[SpecifiedThreeDGridScan], construct_beamline_specific: ConstructBeamlineSpecificFeatures, oav_config: str = OavConstants.OAV_CONFIG_JSON, -) -> MsgGenerator: +) -> MsgGenerator[GridScanParams]: """ A plan which combines the collection of snapshots from the OAV and the determination of the grid dimensions to use for the following grid scan. @@ -77,16 +77,17 @@ def grid_detect_then_xray_centre( oav_params = OAVParameters(get_config_client(), "xrayCentring", oav_config) + grid_scan_params = None + + @ispyb_activation_decorator(parameters) def plan_to_perform(): - yield from ispyb_activation_wrapper( - detect_grid_and_do_gridscan( - composite, - parameters, - oav_params, - xrc_params_type, - construct_beamline_specific, - ), + nonlocal grid_scan_params + grid_scan_params = yield from detect_grid_and_do_gridscan( + composite, parameters, + oav_params, + xrc_params_type, + construct_beamline_specific, ) yield from start_preparing_data_collection_then_do_plan( @@ -98,6 +99,9 @@ def plan_to_perform(): group=PlanGroupCheckpointConstants.GRID_READY_FOR_DC, ) + assert grid_scan_params + return grid_scan_params + # 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( @@ -106,7 +110,7 @@ def detect_grid_and_do_gridscan( oav_params: OAVParameters, xrc_params_type: type[SpecifiedThreeDGridScan], construct_beamline_specific: ConstructBeamlineSpecificFeatures, -): +) -> MsgGenerator[GridScanParams]: grid_detect_params = GridDetectionParams( box_size_um=parameters.box_size_um, grid_width_um=parameters.grid_width_um ) @@ -164,6 +168,8 @@ def detect_grid_and_do_gridscan( composite, xrc_params, grid_scan_params, beamline_specific ) + return grid_scan_params + def _run_grid_detection_plan( composite: GridDetectThenXRayCentreComposite, @@ -196,7 +202,7 @@ def __call__( self, xrc_composite: TFlyScanEssentialDevices, xrc_parameters: TSpecifiedThreeDGridScan, - grid_scan_parameters: GridScanParams, + grid_scan_params: GridScanParams, ) -> BeamlineSpecificFGSFeatures: ... 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 634f1a838b..f34112ffb4 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 @@ -19,7 +19,7 @@ GridscanParamConstants, PlanNameConstants, ) -from mx_bluesky.common.parameters.gridscan import SpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import GridScanParams from mx_bluesky.common.utils.exceptions import ( CrystalNotFoundError, ) @@ -29,7 +29,8 @@ def fetch_xrc_results_from_zocalo( zocalo_results: ZocaloResults, - parameters: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, + sample_id: int | None, ) -> MsgGenerator: """ Get XRC results from the ZocaloResults device which was staged during a grid scan, @@ -55,21 +56,23 @@ def fetch_xrc_results_from_zocalo( LOGGER.info(f"Removed {discarded_count} results because below threshold") if filtered_results: flyscan_results = [ - _xrc_result_in_boxes_to_result_in_mm(xr, parameters) + _xrc_result_in_boxes_to_result_in_mm(xr, grid_scan_params) for xr in filtered_results ] else: commissioning_mode = yield from read_commissioning_mode() if commissioning_mode: LOGGER.info("Commissioning mode enabled, returning dummy result") - flyscan_results = [_generate_dummy_xrc_result(parameters)] + flyscan_results = [_generate_dummy_xrc_result(grid_scan_params, sample_id)] else: LOGGER.warning("No X-ray centre received") raise CrystalNotFoundError() yield from _fire_xray_centre_result_event(flyscan_results) -def _generate_dummy_xrc_result(params: SpecifiedThreeDGridScan) -> XRayCentreResult: +def _generate_dummy_xrc_result( + params: GridScanParams, sample_id: int | None +) -> XRayCentreResult: coms = [] assert params.num_grids % 2 == 0, ( "XRC results in commissioning mode currently only works for an even number of grids" @@ -96,18 +99,44 @@ def _generate_dummy_xrc_result(params: SpecifiedThreeDGridScan) -> XRayCentreRes n_voxels=1, max_count=10000, total_count=100000, - sample_id=params.sample_id, + sample_id=sample_id, ), params, ) +def grid_position_to_motor_position( + grid_scan_params: GridScanParams, grid_pos: np.ndarray +) -> np.ndarray: + assert grid_scan_params.num_grids == 2 + assert grid_scan_params.y_starts_um[0] == grid_scan_params.y_starts_um[1] + assert grid_scan_params.z_starts_um[0] == grid_scan_params.z_starts_um[1] + return ( + np.array( + [ + ( + grid_scan_params.x_start_um + + grid_pos[0] * grid_scan_params.x_step_size_um + ), + ( + grid_scan_params.y_starts_um[0] + + grid_pos[1] * grid_scan_params.y_step_sizes_um[0] + ), + ( + grid_scan_params.z_starts_um[0] + + grid_pos[2] * grid_scan_params.y_step_sizes_um[1] + ), + ] + ) + / 1000 + ) + + def _xrc_result_in_boxes_to_result_in_mm( - xrc_result: XrcResult, parameters: SpecifiedThreeDGridScan + xrc_result: XrcResult, grid_scan_params: GridScanParams ) -> XRayCentreResult: - fgs_params = parameters.fast_gridscan_params - xray_centre = fgs_params.grid_position_to_motor_position( - np.array(xrc_result["centre_of_mass"]) + xray_centre = grid_position_to_motor_position( + grid_scan_params, np.array(xrc_result["centre_of_mass"]) ) # A correction is applied to the bounding box to map discrete grid coordinates to # the corners of the box in motor-space; we do not apply this correction @@ -118,11 +147,11 @@ def _xrc_result_in_boxes_to_result_in_mm( return XRayCentreResult( centre_of_mass_mm=xray_centre, bounding_box_mm=( - fgs_params.grid_position_to_motor_position( - np.array(xrc_result["bounding_box"][0]) - 0.5 + grid_position_to_motor_position( + grid_scan_params, np.array(xrc_result["bounding_box"][0]) - 0.5 ), - fgs_params.grid_position_to_motor_position( - np.array(xrc_result["bounding_box"][1]) - 0.5 + grid_position_to_motor_position( + grid_scan_params, np.array(xrc_result["bounding_box"][1]) - 0.5 ), ), max_count=xrc_result["max_count"], diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 700a7ab631..b7bf512d0d 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 -from abc import abstractmethod from typing import Annotated, Generic, TypeVar from dodal.devices.aperturescatterguard import ApertureValue @@ -235,10 +234,6 @@ def _check_lengths_are_same(self): return self - @property - @abstractmethod - def fast_gridscan_params(self) -> GridScanParamType: ... - def do_set_stub_offsets(self, value: bool): self._set_stub_offsets = value @@ -330,21 +325,24 @@ def validate_y_and_z_axes(self): return self - @property - def fast_gridscan_params(self) -> ZebraGridScanParamsThreeD: - return ZebraGridScanParamsThreeD( - x_steps=self.x_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_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_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, - ) + +def fast_gridscan_params( + expt_params: SpecifiedGrids, grid_scan_params: GridScanParams +) -> ZebraGridScanParamsThreeD: + return ZebraGridScanParamsThreeD( + x_steps=grid_scan_params.x_steps, + y_steps=grid_scan_params.y_steps[0], + z_steps=grid_scan_params.y_steps[1], + x_step_size_mm=grid_scan_params.x_step_size_um / 1000, + y_step_size_mm=grid_scan_params.y_step_sizes_um[0] / 1000, + z_step_size_mm=grid_scan_params.y_step_sizes_um[1] / 1000, + x_start_mm=grid_scan_params.x_start_um / 1000, + y1_start_mm=grid_scan_params.y_starts_um[0] / 1000, + z1_start_mm=grid_scan_params.z_starts_um[0] / 1000, + y2_start_mm=grid_scan_params.y_starts_um[1] / 1000, + z2_start_mm=grid_scan_params.z_starts_um[1] / 1000, + # TODO remove this private access + set_stub_offsets=expt_params._set_stub_offsets, # noqa: SLF001 + dwell_time_ms=expt_params.exposure_time_s * 1000, + transmission_fraction=expt_params.transmission_frac, + ) diff --git a/src/mx_bluesky/hyperion/experiment_plans/pin_centre_then_gridscan_plan.py b/src/mx_bluesky/hyperion/experiment_plans/pin_centre_then_gridscan_plan.py index 22edfd77eb..5f1df74804 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/pin_centre_then_gridscan_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/pin_centre_then_gridscan_plan.py @@ -96,20 +96,22 @@ def _pin_centre_then_gridscan_and_xrc(): PlanNameConstants.GRIDSCAN_OUTER, ) def _grid_detect_and_gridscan_plan(): - yield from detect_grid_and_do_gridscan( - composite, - grid_detect_params, - oav_params, - HyperionSpecifiedThreeDGridScan, - construct_hyperion_specific_features, + return ( + yield from detect_grid_and_do_gridscan( + composite, + grid_detect_params, + oav_params, + HyperionSpecifiedThreeDGridScan, + construct_hyperion_specific_features, + ) ) - yield from _grid_detect_and_gridscan_plan() + grid_scan_params = yield from _grid_detect_and_gridscan_plan() assert isinstance( grid_detect_params.specified_grid_params, SpecifiedThreeDGridScan ), "Specified grid params couldn't be found after grid detection" yield from fetch_xrc_results_from_zocalo( - composite.zocalo, grid_detect_params.specified_grid_params + composite.zocalo, grid_scan_params, parameters.sample_id ) yield from ispyb_activation_wrapper(_pin_centre_then_gridscan_and_xrc(), parameters) diff --git a/tests/conftest.py b/tests/conftest.py index 5b34e9b5c5..185c954572 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -83,7 +83,10 @@ EnvironmentConstants, PlanNameConstants, ) -from mx_bluesky.common.parameters.gridscan import SpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import ( + GridScanParams, + SpecifiedThreeDGridScan, +) from mx_bluesky.common.parameters.rotation import RotationScan from mx_bluesky.common.utils.exceptions import CrystalNotFoundError from mx_bluesky.common.utils.log import ( @@ -363,6 +366,20 @@ def test_three_d_grid_params(tmp_path, patch_beamline_env_variable): ) +@pytest.fixture +def three_d_grid_scan_params(test_three_d_grid_params): + return GridScanParams( + omega_starts_deg=test_three_d_grid_params.omega_starts_deg, + x_steps=test_three_d_grid_params.x_steps, + y_steps=test_three_d_grid_params.y_steps, + x_start_um=test_three_d_grid_params.x_start_um, + y_starts_um=test_three_d_grid_params.y_starts_um, + z_starts_um=test_three_d_grid_params.z_starts_um, + x_step_size_um=test_three_d_grid_params.x_step_size_um, + y_step_sizes_um=test_three_d_grid_params.y_step_sizes_um, + ) + + @pytest.fixture def eiger(): eiger = i03.eiger.build(mock=True) diff --git a/tests/system_tests/hyperion/external_interaction/callbacks/test_external_callbacks.py b/tests/system_tests/hyperion/external_interaction/callbacks/test_external_callbacks.py index f7a28afbdd..8842fdadae 100644 --- a/tests/system_tests/hyperion/external_interaction/callbacks/test_external_callbacks.py +++ b/tests/system_tests/hyperion/external_interaction/callbacks/test_external_callbacks.py @@ -42,6 +42,7 @@ ispyb_activation_decorator, ) from mx_bluesky.common.parameters.components import WithSnapshot +from mx_bluesky.common.parameters.gridscan import GridScanParams from mx_bluesky.common.parameters.rotation import ( RotationScan, ) @@ -257,9 +258,19 @@ async def test_external_callbacks_handle_gridscan_ispyb_and_zocalo( doc_catcher = DocumentCatcher() run_engine.subscribe(doc_catcher) + grid_scan_params = GridScanParams( + omega_starts_deg=dummy_params.omega_starts_deg, + x_start_um=dummy_params.x_start_um, + y_starts_um=dummy_params.y_starts_um, + z_starts_um=dummy_params.z_starts_um, + x_steps=dummy_params.x_steps, + y_steps=dummy_params.y_steps, + x_step_size_um=dummy_params.x_step_size_um, + y_step_sizes_um=dummy_params.y_step_sizes_um, + ) # Run the xray centring plan beamline_specific = construct_hyperion_specific_features( - fgs_composite_for_fake_zocalo, dummy_params + fgs_composite_for_fake_zocalo, dummy_params, grid_scan_params ) @zocalo_stage_decorator(fgs_composite_for_fake_zocalo.zocalo) @@ -267,10 +278,15 @@ async def test_external_callbacks_handle_gridscan_ispyb_and_zocalo( def wrapped_xray_centre(): yield from fake_grid_snapshot_plan(smargon, oav_for_system_test) yield from common_flyscan_xray_centre( - fgs_composite_for_fake_zocalo, dummy_params, beamline_specific + fgs_composite_for_fake_zocalo, + dummy_params, + grid_scan_params, + beamline_specific, ) yield from fetch_xrc_results_from_zocalo( - fgs_composite_for_fake_zocalo.zocalo, dummy_params + fgs_composite_for_fake_zocalo.zocalo, + grid_scan_params, + dummy_params.sample_id, ) run_engine(wrapped_xray_centre()) 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 6ce0c06f9f..708ef01fb5 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 @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock, patch +from unittest.mock import ANY, MagicMock, patch import pytest from bluesky.run_engine import RunEngine @@ -194,7 +194,7 @@ def test_i02_1_flyscan_xray_centre_in_re( run_engine(i02_1_gridscan_plan(entry_params, fgs_composite)) mock_common_scan.assert_called_once_with( - fgs_composite, expected_fgs_params, specific_features + fgs_composite, expected_fgs_params, ANY, specific_features ) 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 aca16c4fd9..addd2337a6 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 @@ -46,6 +46,7 @@ from mx_bluesky.common.parameters.constants import PlanNameConstants from mx_bluesky.common.parameters.gridscan import ( GenericGrid, + GridScanParams, SpecifiedThreeDGridScan, ) from mx_bluesky.common.utils.exceptions import CrystalNotFoundError @@ -130,6 +131,20 @@ def i04_grid_detect_then_xrc_default_params( ) +@pytest.fixture +def i04_grid_scan_params() -> GridScanParams: + return GridScanParams( + omega_starts_deg=[0, 90], + x_start_um=0, + y_starts_um=[0, 0], + z_starts_um=[0, 0], + x_steps=10, + y_steps=[10, 10], + x_step_size_um=10, + y_step_sizes_um=[10, 10], + ) + + @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 @@ -264,11 +279,15 @@ def test_i04_default_grid_detect_and_xray_centre_sets_transmission_triggers_xbpm sim_run_engine: RunEngineSimulator, zocalo: ZocaloResults, test_three_d_grid_params, + three_d_grid_scan_params: GridScanParams, 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 = test_three_d_grid_params + mock_create_parameters.return_value = ( + test_three_d_grid_params, + three_d_grid_scan_params, + ) sim_run_engine.add_handler( "locate", lambda msg: {"readback": np.array([0, 0])}, @@ -346,9 +365,10 @@ def test_i04_default_grid_detect_and_xray_centre_does_undulator_check_before_col mock_create_gridscan_callbacks: MagicMock, run_engine: RunEngine, test_three_d_grid_params, + i04_grid_scan_params, i04_grid_detect_then_xrc_default_params: partial[MsgGenerator], ): - mock_create_parameters.return_value = test_three_d_grid_params + mock_create_parameters.return_value = test_three_d_grid_params, i04_grid_scan_params mock_run_gridscan.side_effect = CompleteError with pytest.raises(CompleteError): run_engine(i04_grid_detect_then_xrc_default_params()) @@ -579,9 +599,13 @@ def test_grid_detect_then_xrc_stages_and_unstages_zocalo_and_gets_results( ) +def null_decorator(plan): + return plan + + @patch( - "mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan.ispyb_activation_wrapper", - lambda plan, _: plan, + "mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan.ispyb_activation_decorator", + lambda parameters: null_decorator, ) @patch( "mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan.grid_detection_plan", @@ -607,8 +631,12 @@ def test_detect_grid_and_do_gridscan_gives_params_specified_grid( test_three_d_grid_params: SpecifiedThreeDGridScan, run_engine: RunEngine, i04_grid_detect_then_xrc_default_params: partial[MsgGenerator], + i04_grid_scan_params, ): - mock_create_flyscan_params.return_value = test_three_d_grid_params + mock_create_flyscan_params.return_value = ( + test_three_d_grid_params, + i04_grid_scan_params, + ) run_engine( i04_grid_detect_then_xrc_default_params( udc=False, @@ -622,8 +650,8 @@ def test_detect_grid_and_do_gridscan_gives_params_specified_grid( @patch( - "mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan.ispyb_activation_wrapper", - lambda plan, _: plan, + "mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan.ispyb_activation_decorator", + lambda parameters: null_decorator, ) @patch( "mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan.grid_detection_plan", @@ -649,6 +677,7 @@ def test_i04_grid_detect_then_xrc_only_sets_aperture_at_start_of_plan( test_three_d_grid_params: SpecifiedThreeDGridScan, run_engine: RunEngine, i04_grid_detect_then_xrc_default_params: partial[MsgGenerator], + i04_grid_scan_params: GridScanParams, ): aperture_scatterguard: ApertureScatterguard = ( i04_grid_detect_then_xrc_default_params.keywords["aperture_scatterguard"] @@ -673,7 +702,10 @@ def fake_get_xrc_results_plan(): mock_get_xrc_results.return_value = fake_get_xrc_results_plan() - mock_create_flyscan_params.return_value = test_three_d_grid_params + mock_create_flyscan_params.return_value = ( + test_three_d_grid_params, + i04_grid_scan_params, + ) run_engine( i04_grid_detect_then_xrc_default_params( udc=False, diff --git a/tests/unit_tests/common/experiment_plans/test_change_aperture_then_move_plan.py b/tests/unit_tests/common/experiment_plans/test_change_aperture_then_move_plan.py index 467e0a6195..c862cccabf 100644 --- a/tests/unit_tests/common/experiment_plans/test_change_aperture_then_move_plan.py +++ b/tests/unit_tests/common/experiment_plans/test_change_aperture_then_move_plan.py @@ -15,7 +15,10 @@ from mx_bluesky.common.parameters.device_composites import ( GridDetectThenXRayCentreComposite, ) -from mx_bluesky.common.parameters.gridscan import SpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import ( + GridScanParams, + SpecifiedThreeDGridScan, +) from mx_bluesky.common.utils.xrc_result import XRayCentreEventHandler, XRayCentreResult @@ -100,11 +103,21 @@ def test_get_results_then_change_aperture_and_move_to_xtal_calls_expected_plans( ): mock_flyscan_event_handler = MagicMock(spec=XRayCentreEventHandler) mock_flyscan_event_handler.xray_centre_results = [0] - + grid_scan_params = GridScanParams( + omega_starts_deg=test_three_d_grid_params.omega_starts_deg, + x_steps=test_three_d_grid_params.x_steps, + y_steps=test_three_d_grid_params.y_steps, + x_step_size_um=test_three_d_grid_params.x_step_size_um, + y_step_sizes_um=test_three_d_grid_params.y_step_sizes_um, + x_start_um=test_three_d_grid_params.x_start_um, + y_starts_um=test_three_d_grid_params.y_starts_um, + z_starts_um=test_three_d_grid_params.z_starts_um, + ) run_engine( get_results_then_change_aperture_and_move_to_xtal( grid_detect_xrc_devices, test_three_d_grid_params, + grid_scan_params, mock_flyscan_event_handler, ) ) 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 a5c367cd46..7fc95d4c1a 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 @@ -48,7 +48,11 @@ IspybIds, ) from mx_bluesky.common.parameters.constants import DocDescriptorNames -from mx_bluesky.common.parameters.gridscan import SpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import ( + GridScanParams, + SpecifiedThreeDGridScan, + fast_gridscan_params, +) from mx_bluesky.common.utils.exceptions import ( WarningError, ) @@ -93,6 +97,20 @@ def run_engine_with_subs_snapshots_already_taken(run_engine_with_subs, test_even return run_engine, subscriptions +@pytest.fixture +def grid_scan_params(test_three_d_grid_params) -> GridScanParams: + return GridScanParams( + omega_starts_deg=test_three_d_grid_params.omega_starts_deg, + x_start_um=test_three_d_grid_params.x_start_um, + y_starts_um=test_three_d_grid_params.y_starts_um, + z_starts_um=test_three_d_grid_params.z_starts_um, + x_steps=test_three_d_grid_params.x_steps, + y_steps=test_three_d_grid_params.y_steps, + x_step_size_um=test_three_d_grid_params.x_step_size_um, + y_step_sizes_um=test_three_d_grid_params.y_step_sizes_um, + ) + + @patch( "mx_bluesky.common.external_interaction.callbacks.grid.grid_detect_and_scan.ispyb_callback.StoreInIspyb", modified_store_grid_scan_mock, @@ -112,7 +130,7 @@ def test_eiger2_x_16_detector_specified( def test_when_run_gridscan_called_then_generator_returned( self, ): - plan = run_gridscan(MagicMock(), MagicMock(), MagicMock()) + plan = run_gridscan(MagicMock(), MagicMock(), MagicMock(), MagicMock()) assert isinstance(plan, types.GeneratorType) def test_when_run_gridscan_called_ispyb_deposition_made_and_records_errors( @@ -120,6 +138,7 @@ def test_when_run_gridscan_called_ispyb_deposition_made_and_records_errors( run_engine: RunEngine, fake_fgs_composite: FlyScanEssentialDevices, test_three_d_grid_params: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, beamline_specific: BeamlineSpecificFGSFeatures, ): ispyb_callback = GridDetectAndScanISPyBCallback( @@ -137,6 +156,7 @@ def test_when_run_gridscan_called_ispyb_deposition_made_and_records_errors( common_flyscan_xray_centre( fake_fgs_composite, test_three_d_grid_params, + grid_scan_params, beamline_specific, ), test_three_d_grid_params, @@ -158,14 +178,14 @@ def test_results_passed_to_move_motors( self, bps_abs_set: MagicMock, test_three_d_grid_params: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, fake_fgs_composite: FlyScanEssentialDevices, run_engine: RunEngine, ): from mx_bluesky.common.device_setup_plans.manipulate_sample import move_x_y_z - motor_position = test_three_d_grid_params.fast_gridscan_params.grid_position_to_motor_position( - np.array([1, 2, 3]) - ) + fgs_params = fast_gridscan_params(test_three_d_grid_params, grid_scan_params) + motor_position = fgs_params.grid_position_to_motor_position(np.array([1, 2, 3])) run_engine(move_x_y_z(fake_fgs_composite.gonio, *motor_position)) bps_abs_set.assert_called_with( fake_fgs_composite.gonio, @@ -186,6 +206,7 @@ def test_individual_plans_triggered_once_and_only_once_in_composite_run( run_engine_with_subs: ReWithSubs, fake_fgs_composite: FlyScanEssentialDevices, test_three_d_grid_params: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, beamline_specific: BeamlineSpecificFGSFeatures, ): run_engine, _ = run_engine_with_subs @@ -194,6 +215,7 @@ def wrapped_gridscan_and_move(): yield from common_flyscan_xray_centre( fake_fgs_composite, test_three_d_grid_params, + grid_scan_params, beamline_specific, ) @@ -245,11 +267,13 @@ def test_if_gridscan_prepare_fails_with_invalid_grid_then_sample_exception_raise fake_fgs_composite: FlyScanEssentialDevices, beamline_specific: BeamlineSpecificFGSFeatures, test_three_d_grid_params: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, ): + fgs_params = fast_gridscan_params(test_three_d_grid_params, grid_scan_params) beamline_specific.set_flyscan_params_plan = partial( set_fast_grid_scan_params, beamline_specific.fgs_motors, - test_three_d_grid_params.fast_gridscan_params, + fgs_params, ) set_mock_value(beamline_specific.fgs_motors.device_scan_invalid, 1.0) # type: ignore @@ -257,7 +281,10 @@ def test_if_gridscan_prepare_fails_with_invalid_grid_then_sample_exception_raise with pytest.raises(WarningError): run_engine( run_gridscan( - fake_fgs_composite, test_three_d_grid_params, beamline_specific + fake_fgs_composite, + test_three_d_grid_params, + grid_scan_params, + beamline_specific, ) ) @@ -271,6 +298,7 @@ def test_if_gridscan_prepare_fails_with_other_exception_then_plan_re_raised( fake_fgs_composite: FlyScanEssentialDevices, beamline_specific: BeamlineSpecificFGSFeatures, test_three_d_grid_params: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, ): exception = FailedStatus() exception.__cause__ = Exception() @@ -280,7 +308,10 @@ 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_three_d_grid_params, beamline_specific + fake_fgs_composite, + test_three_d_grid_params, + grid_scan_params, + beamline_specific, ) ) @@ -323,6 +354,7 @@ def test_when_grid_scan_ran_then_eiger_disarmed_before_zocalo_end( mock_abs_set, fake_fgs_composite: FlyScanEssentialDevices, test_three_d_grid_params: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, run_engine_with_subs_snapshots_already_taken: tuple[ RunEngine, tuple[GridscanNexusFileCallback, GridDetectAndScanISPyBCallback], @@ -331,6 +363,8 @@ def test_when_grid_scan_ran_then_eiger_disarmed_before_zocalo_end( ): test_three_d_grid_params.x_steps = 9 test_three_d_grid_params.y_steps = [10, 12] + grid_scan_params.x_steps = 9 + grid_scan_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() @@ -355,7 +389,10 @@ 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_three_d_grid_params, beamline_specific + fake_fgs_composite, + test_three_d_grid_params, + grid_scan_params, + beamline_specific, ), test_three_d_grid_params, ) @@ -389,13 +426,17 @@ def test_fgs_arms_eiger_without_grid_detect( mock_wait, fake_fgs_composite: FlyScanEssentialDevices, test_three_d_grid_params: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, run_engine: RunEngine, beamline_specific: BeamlineSpecificFGSFeatures, ): fake_fgs_composite.eiger.unstage = MagicMock(side_effect=completed_status) run_engine( run_gridscan( - fake_fgs_composite, test_three_d_grid_params, beamline_specific + fake_fgs_composite, + test_three_d_grid_params, + grid_scan_params, + beamline_specific, ) ) fake_fgs_composite.eiger.stage.assert_called_once() # type: ignore @@ -425,6 +466,7 @@ def test_when_grid_scan_fails_with_exception_then_detector_disarmed_and_correct_ mock_kickoff, fake_fgs_composite: FlyScanEssentialDevices, test_three_d_grid_params: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, run_engine: RunEngine, beamline_specific: BeamlineSpecificFGSFeatures, ): @@ -451,7 +493,10 @@ def test_when_grid_scan_fails_with_exception_then_detector_disarmed_and_correct_ run_engine( bpp.run_wrapper( run_gridscan( - fake_fgs_composite, test_three_d_grid_params, beamline_specific + fake_fgs_composite, + test_three_d_grid_params, + grid_scan_params, + beamline_specific, ) ) ) @@ -536,6 +581,7 @@ def test_read_hardware_during_collection_occurs_after_eiger_arm( self, fake_fgs_composite: FlyScanEssentialDevices, test_three_d_grid_params: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, sim_run_engine: RunEngineSimulator, beamline_specific: BeamlineSpecificFGSFeatures, ): @@ -556,7 +602,10 @@ def test_read_hardware_during_collection_occurs_after_eiger_arm( ) msgs = sim_run_engine.simulate_plan( run_gridscan( - fake_fgs_composite, test_three_d_grid_params, beamline_specific + fake_fgs_composite, + test_three_d_grid_params, + grid_scan_params, + beamline_specific, ) ) msgs = assert_message_and_return_remaining( @@ -591,6 +640,7 @@ def test_when_gridscan_succeeds_and_results_fetched_ispyb_comment_appended_to( tuple[GridscanNexusFileCallback, GridDetectAndScanISPyBCallback], ], test_three_d_grid_params: SpecifiedThreeDGridScan, + grid_scan_params: GridScanParams, fake_fgs_composite: FlyScanEssentialDevices, beamline_specific: BeamlineSpecificFGSFeatures, ): @@ -601,6 +651,7 @@ def _wrapped_gridscan_and_move(): yield from common_flyscan_xray_centre( fake_fgs_composite, test_three_d_grid_params, + grid_scan_params, beamline_specific, ) diff --git a/tests/unit_tests/common/experiment_plans/test_common_grid_detect_then_xray_centre_plan.py b/tests/unit_tests/common/experiment_plans/test_common_grid_detect_then_xray_centre_plan.py index ed020c32ad..6a3247c109 100644 --- a/tests/unit_tests/common/experiment_plans/test_common_grid_detect_then_xray_centre_plan.py +++ b/tests/unit_tests/common/experiment_plans/test_common_grid_detect_then_xray_centre_plan.py @@ -32,7 +32,11 @@ DocDescriptorNames, PlanGroupCheckpointConstants, ) -from mx_bluesky.common.parameters.gridscan import SpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import ( + GridScanParams, + SpecifiedThreeDGridScan, + fast_gridscan_params, +) from mx_bluesky.hyperion.parameters.device_composites import ( GridDetectThenXRayCentreComposite, ) @@ -59,7 +63,7 @@ def _fake_flyscan(*args): def construct_beamline_specific( beamline_specific: BeamlineSpecificFGSFeatures, ) -> ConstructBeamlineSpecificFeatures: - return lambda xrc_composite, xrc_parameters: beamline_specific + return lambda xrc_composite, xrc_parameters, grid_scan_params: beamline_specific @pytest.mark.timeout(2) @@ -111,17 +115,13 @@ async def test_detect_grid_and_do_gridscan_in_real_run_engine( ) # Check we called out to underlying fast grid scan plan - mock_flyscan.assert_called_once_with(ANY, ANY, ANY) + mock_flyscan.assert_called_once_with(ANY, ANY, ANY, ANY) @patch( "mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan.GridDetectionCallback", autospec=True, ) -@patch( - "mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan.create_parameters_for_flyscan_xray_centre", - autospec=True, -) @patch( "mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan.common_flyscan_xray_centre", autospec=True, @@ -138,7 +138,6 @@ def test_detect_grid_and_do_gridscan_sets_up_beamline_for_oav( mock_setup_beamline_for_oav: MagicMock, mock_grid_detect: MagicMock, mock_flyscan: MagicMock, - mock_create_params: MagicMock, mock_grid_detect_callback: MagicMock, grid_detect_xrc_devices: GridDetectThenXRayCentreComposite, sim_run_engine: RunEngineSimulator, @@ -146,6 +145,15 @@ def test_detect_grid_and_do_gridscan_sets_up_beamline_for_oav( test_config_files: dict, construct_beamline_specific: ConstructBeamlineSpecificFeatures, ): + mock_grid_detect_callback.return_value.get_grid_parameters.return_value = { + "x_start_um": 0, + "y_starts_um": [0, 0], + "z_starts_um": [0, 0], + "x_steps": 10, + "y_steps": [10, 10], + "x_step_size_um": 10, + "y_step_sizes_um": [10, 10], + } sim_run_engine.add_handler_for_callback_subscribes() sim_run_engine.simulate_plan( grid_detect_then_xray_centre( @@ -156,7 +164,6 @@ def test_detect_grid_and_do_gridscan_sets_up_beamline_for_oav( xrc_params_type=SpecifiedThreeDGridScan, ), ) - mock_setup_beamline_for_oav.assert_called_once() @@ -210,10 +217,11 @@ def test_when_full_grid_scan_run_then_parameters_sent_to_fgs_as_expected( ) params: HyperionSpecifiedThreeDGridScan = mock_flyscan.call_args[0][1] - + grid_scan_params: GridScanParams = mock_flyscan.call_args[0][2] assert params.detector_params.num_triggers == 180 - assert params.fast_gridscan_params.x_axis.full_steps == 15 - assert params.fast_gridscan_params.y_axis.end == pytest.approx(-0.06329, 0.001) + fgs_params = fast_gridscan_params(params, grid_scan_params) + assert fgs_params.x_axis.full_steps == 15 + assert fgs_params.y_axis.end == pytest.approx(-0.06329, 0.001) # Parameters can be serialized params.model_dump_json() @@ -395,9 +403,21 @@ def test_grid_detect_then_xray_centre_plan_moves_beamstop_into_place( construct_beamline_specific: ConstructBeamlineSpecificFeatures, test_config_files: dict, ): - mock_grid_detect_then_xray_centre.return_value = iter( - [Msg("grid_detect_then_xray_centre")] - ) + def mock_grid_detect_then_xrc_plan(*args, **kwargs): + yield Msg("grid_detect_then_xray_centre") + return GridScanParams( + omega_starts_deg=[0, 90], + x_steps=10, + y_steps=[10, 10], + x_start_um=0, + y_starts_um=[0, 0], + z_starts_um=[0, 0], + x_step_size_um=10, + y_step_sizes_um=[10, 10], + ) + + mock_grid_detect_then_xray_centre.side_effect = mock_grid_detect_then_xrc_plan + msgs = sim_run_engine.simulate_plan( grid_detect_then_xray_centre( grid_detect_xrc_devices, diff --git a/tests/unit_tests/common/utils/test_xrc_result.py b/tests/unit_tests/common/utils/test_xrc_result.py index f261fc1fe1..e841bd6d0a 100644 --- a/tests/unit_tests/common/utils/test_xrc_result.py +++ b/tests/unit_tests/common/utils/test_xrc_result.py @@ -14,7 +14,10 @@ fetch_xrc_results_from_zocalo, zocalo_stage_decorator, ) -from mx_bluesky.common.parameters.gridscan import SpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import ( + GridScanParams, + SpecifiedThreeDGridScan, +) from mx_bluesky.common.utils.exceptions import ( CrystalNotFoundError, ) @@ -34,13 +37,18 @@ async def test_results_adjusted_and_event_raised( test_three_d_grid_params: SpecifiedThreeDGridScan, + three_d_grid_scan_params: GridScanParams, 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_three_d_grid_params)) + run_engine( + fetch_xrc_results_from_zocalo( + zocalo, three_d_grid_scan_params, test_three_d_grid_params.sample_id + ) + ) actual = x_ray_centre_event_handler.xray_centre_results expected = XRayCentreResult( @@ -61,6 +69,7 @@ async def test_results_adjusted_and_event_raised( def test_fetch_results_discards_results_below_threshold( test_three_d_grid_params: SpecifiedThreeDGridScan, + three_d_grid_scan_params: GridScanParams, run_engine: RunEngine, zocalo: ZocaloResults, ): @@ -73,7 +82,11 @@ 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_three_d_grid_params)) + run_engine( + fetch_xrc_results_from_zocalo( + zocalo, three_d_grid_scan_params, test_three_d_grid_params.sample_id + ) + ) 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] @@ -82,17 +95,23 @@ def test_fetch_results_discards_results_below_threshold( def test_no_xtal_found_raises_exception( run_engine: RunEngine, test_three_d_grid_params: SpecifiedThreeDGridScan, + three_d_grid_scan_params: GridScanParams, zocalo: ZocaloResults, ): mock_zocalo_trigger(zocalo, []) with pytest.raises(CrystalNotFoundError): - run_engine(fetch_xrc_results_from_zocalo(zocalo, test_three_d_grid_params)) + run_engine( + fetch_xrc_results_from_zocalo( + zocalo, three_d_grid_scan_params, test_three_d_grid_params.sample_id + ) + ) def test_dummy_result_returned_when_no_xtal_and_commissioning_mode_enabled( run_engine: RunEngine, test_three_d_grid_params: SpecifiedThreeDGridScan, + three_d_grid_scan_params: GridScanParams, fake_fgs_composite: FlyScanEssentialDevices, beamline_specific: BeamlineSpecificFGSFeatures, zocalo: ZocaloResults, @@ -103,7 +122,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_three_d_grid_params)) + run_engine( + fetch_xrc_results_from_zocalo( + zocalo, three_d_grid_scan_params, test_three_d_grid_params.sample_id + ) + ) results = xrc_event_handler.xray_centre_results or [] assert len(results) == 1 result = results[0] diff --git a/tests/unit_tests/hyperion/experiment_plans/conftest.py b/tests/unit_tests/hyperion/experiment_plans/conftest.py index 9a9cd61fcc..74c733ae59 100644 --- a/tests/unit_tests/hyperion/experiment_plans/conftest.py +++ b/tests/unit_tests/hyperion/experiment_plans/conftest.py @@ -17,6 +17,7 @@ IspybIds, StoreInIspyb, ) +from mx_bluesky.common.parameters.gridscan import GridScanParams from mx_bluesky.common.utils.xrc_result import XRayCentreResult from mx_bluesky.hyperion.experiment_plans.hyperion_flyscan_xray_centre_plan import ( construct_hyperion_specific_features, @@ -293,11 +294,26 @@ def grid_detection_callback_with_detected_grid(): yield callback +@pytest.fixture +def hyperion_grid_scan_params(hyperion_fgs_params: HyperionSpecifiedThreeDGridScan): + return GridScanParams( + omega_starts_deg=hyperion_fgs_params.omega_starts_deg, + x_start_um=hyperion_fgs_params.x_start_um, + y_starts_um=hyperion_fgs_params.y_starts_um, + z_starts_um=hyperion_fgs_params.z_starts_um, + x_steps=hyperion_fgs_params.x_steps, + y_steps=hyperion_fgs_params.y_steps, + x_step_size_um=hyperion_fgs_params.x_step_size_um, + y_step_sizes_um=hyperion_fgs_params.y_step_sizes_um, + ) + + @pytest.fixture def beamline_specific( hyperion_flyscan_xrc_composite: HyperionFlyScanXRayCentreComposite, hyperion_fgs_params: HyperionSpecifiedThreeDGridScan, + hyperion_grid_scan_params: GridScanParams, ) -> BeamlineSpecificFGSFeatures: return construct_hyperion_specific_features( - hyperion_flyscan_xrc_composite, hyperion_fgs_params + hyperion_flyscan_xrc_composite, hyperion_fgs_params, hyperion_grid_scan_params ) diff --git a/tests/unit_tests/hyperion/experiment_plans/test_hyperion_flyscan_xray_centre_plan.py b/tests/unit_tests/hyperion/experiment_plans/test_hyperion_flyscan_xray_centre_plan.py index c4dcfeb5c4..c1686924c7 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_hyperion_flyscan_xray_centre_plan.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_hyperion_flyscan_xray_centre_plan.py @@ -28,6 +28,7 @@ DeviceSettingsConstants, ) from mx_bluesky.common.parameters.device_composites import FlyScanEssentialDevices +from mx_bluesky.common.parameters.gridscan import GridScanParams from mx_bluesky.hyperion.experiment_plans.hyperion_flyscan_xray_centre_plan import ( SmargonSpeedError, ) @@ -98,6 +99,7 @@ def test_results_adjusted_and_passed_to_move_xyz( run_gridscan: MagicMock, hyperion_flyscan_xrc_composite: HyperionFlyScanXRayCentreComposite, hyperion_fgs_params: HyperionSpecifiedThreeDGridScan, + hyperion_grid_scan_params: GridScanParams, run_engine_with_subs: ReWithSubs, beamline_specific: BeamlineSpecificFGSFeatures, ): @@ -113,6 +115,7 @@ def test_results_adjusted_and_passed_to_move_xyz( common_flyscan_xray_centre( hyperion_flyscan_xrc_composite, hyperion_fgs_params, + hyperion_grid_scan_params, beamline_specific, ) ) @@ -150,6 +153,7 @@ async def test_when_gridscan_finished_then_dev_shm_disabled( run_gridscan: MagicMock, sim_run_engine: RunEngineSimulator, hyperion_fgs_params: HyperionSpecifiedThreeDGridScan, + hyperion_grid_scan_params: GridScanParams, hyperion_flyscan_xrc_composite: FlyScanEssentialDevices, beamline_specific: BeamlineSpecificFGSFeatures, zocalo: ZocaloResults, @@ -170,6 +174,7 @@ async def test_when_gridscan_finished_then_dev_shm_disabled( common_flyscan_xray_centre( hyperion_flyscan_xrc_composite, hyperion_fgs_params, + hyperion_grid_scan_params, beamline_specific, ) ) @@ -202,6 +207,16 @@ def test_if_smargon_speed_over_limit_then_log_error( run_engine: RunEngine, ): fgs_params_use_panda.x_step_size_um = 10000 + grid_scan_params = GridScanParams( + omega_starts_deg=fgs_params_use_panda.omega_starts_deg, + x_start_um=fgs_params_use_panda.x_start_um, + y_starts_um=fgs_params_use_panda.y_starts_um, + z_starts_um=fgs_params_use_panda.z_starts_um, + x_steps=fgs_params_use_panda.x_steps, + y_steps=fgs_params_use_panda.y_steps, + x_step_size_um=10000, + y_step_sizes_um=fgs_params_use_panda.y_step_sizes_um, + ) fgs_params_use_panda.detector_params.exposure_time_s = 0.01 # this exception should only be raised if we're using the panda @@ -210,6 +225,7 @@ def test_if_smargon_speed_over_limit_then_log_error( common_flyscan_xray_centre( hyperion_flyscan_xrc_composite, fgs_params_use_panda, + grid_scan_params, beamline_specific, ) ) @@ -241,6 +257,16 @@ def test_flyscan_xray_centre_sets_directory_stages_arms_disarms_unstages_the_pan beamline_specific: BeamlineSpecificFGSFeatures, tmp_path: Path, ): + grid_scan_params = GridScanParams( + omega_starts_deg=fgs_params_use_panda.omega_starts_deg, + x_start_um=fgs_params_use_panda.x_start_um, + y_starts_um=fgs_params_use_panda.y_starts_um, + z_starts_um=fgs_params_use_panda.z_starts_um, + x_steps=fgs_params_use_panda.x_steps, + y_steps=fgs_params_use_panda.y_steps, + x_step_size_um=fgs_params_use_panda.x_step_size_um, + y_step_sizes_um=fgs_params_use_panda.y_step_sizes_um, + ) sim_run_engine.add_read_handler_for( fgs_composite_with_panda_pcap.gonio.x.max_velocity, 10 ) @@ -250,7 +276,10 @@ def test_flyscan_xray_centre_sets_directory_stages_arms_disarms_unstages_the_pan msgs = sim_run_engine.simulate_plan( common_flyscan_xray_centre( - fgs_composite_with_panda_pcap, fgs_params_use_panda, beamline_specific + fgs_composite_with_panda_pcap, + fgs_params_use_panda, + grid_scan_params, + beamline_specific, ) ) 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 b63c61beb6..e6d9387bd2 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 @@ -16,7 +16,10 @@ from mx_bluesky.common.experiment_plans.inner_plans.do_fgs import ZOCALO_STAGE_GROUP from mx_bluesky.common.parameters.constants import OavConstants, PlanNameConstants -from mx_bluesky.common.parameters.gridscan import SpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import ( + GridScanParams, + SpecifiedThreeDGridScan, +) from mx_bluesky.hyperion.blueapi.mixins import TopNByMaxCountSelection from mx_bluesky.hyperion.experiment_plans.pin_centre_then_gridscan_plan import ( create_parameters_for_grid_detection, @@ -413,10 +416,14 @@ def test_detect_grid_and_do_gridscan_gives_params_specified_grid( test_pin_centre_then_xray_centre_params: PinTipCentreThenXrayCentre, hyperion_grid_detect_xrc_devices: HyperionGridDetectThenXRayCentreComposite, test_three_d_grid_params: SpecifiedThreeDGridScan, + three_d_grid_scan_params: GridScanParams, test_config_files, run_engine: RunEngine, ): - mock_create_flyscan_params.return_value = test_three_d_grid_params + mock_create_flyscan_params.return_value = ( + test_three_d_grid_params, + three_d_grid_scan_params, + ) run_engine( pin_centre_then_gridscan_plan( hyperion_grid_detect_xrc_devices, @@ -425,7 +432,8 @@ 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 + grid_scan_params = mock_fetch_xrc_results.call_args[0][1] + assert grid_scan_params == three_d_grid_scan_params @patch( 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 5bb9f813b8..73f2f515eb 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 @@ -24,7 +24,11 @@ create_beam_and_attenuator_parameters, ) from mx_bluesky.common.external_interaction.nexus.write_nexus import NexusWriter -from mx_bluesky.hyperion.parameters.gridscan import HyperionSpecifiedThreeDGridScan +from mx_bluesky.common.parameters.gridscan import GridScanParams +from mx_bluesky.hyperion.parameters.gridscan import ( + HyperionSpecifiedThreeDGridScan, + fast_gridscan_params, +) """It's hard to effectively unit test the nexus writing so these are really system tests that confirms that we're passing the right sorts of data to nexgen to get a sensible output. @@ -128,8 +132,19 @@ def test_given_dummy_data_then_datafile_written_correctly( dummy_nexus_writers: tuple[NexusWriter, NexusWriter], ): nexus_writer_1, nexus_writer_2 = dummy_nexus_writers - grid_scan_params: ZebraGridScanParamsThreeD = ( - test_three_d_grid_params.fast_gridscan_params + + grid_scan_params = GridScanParams( + omega_starts_deg=test_three_d_grid_params.omega_starts_deg, + x_steps=test_three_d_grid_params.x_steps, + y_steps=test_three_d_grid_params.y_steps, + x_start_um=test_three_d_grid_params.x_start_um, + y_starts_um=test_three_d_grid_params.y_starts_um, + z_starts_um=test_three_d_grid_params.z_starts_um, + x_step_size_um=test_three_d_grid_params.x_step_size_um, + y_step_sizes_um=test_three_d_grid_params.y_step_sizes_um, + ) + zebra_grid_scan_params: ZebraGridScanParamsThreeD = fast_gridscan_params( + test_three_d_grid_params, grid_scan_params ) nexus_writer_1.create_nexus_file(np.uint16) @@ -139,14 +154,14 @@ def test_given_dummy_data_then_datafile_written_correctly( data_path := written_nexus_file["/entry/data"], h5py.Group ) assert_x_data_stride_correct( - data_path, grid_scan_params, grid_scan_params.y_steps + data_path, zebra_grid_scan_params, zebra_grid_scan_params.y_steps ) assert isinstance(sam_y := data_path["sam_y"], h5py.Dataset) assert_varying_axis_stride_correct( - sam_y[:], grid_scan_params, grid_scan_params.y_axis + sam_y[:], zebra_grid_scan_params, zebra_grid_scan_params.y_axis ) assert_axis_data_fixed( - written_nexus_file, "z", grid_scan_params.z1_start_mm + written_nexus_file, "z", zebra_grid_scan_params.z1_start_mm ) assert isinstance( flux := written_nexus_file["/entry/instrument/beam/total_flux"], @@ -194,14 +209,14 @@ def test_given_dummy_data_then_datafile_written_correctly( data_path := written_nexus_file["/entry/data"], h5py.Group ) assert_x_data_stride_correct( - data_path, grid_scan_params, grid_scan_params.z_steps + data_path, zebra_grid_scan_params, zebra_grid_scan_params.z_steps ) assert isinstance(sam_y := data_path["sam_y"], h5py.Dataset) assert_varying_axis_stride_correct( - sam_y[:], grid_scan_params, grid_scan_params.z_axis + sam_y[:], zebra_grid_scan_params, zebra_grid_scan_params.z_axis ) assert_axis_data_fixed( - written_nexus_file, "z", grid_scan_params.y2_start_mm + written_nexus_file, "z", zebra_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 65cf2e2874..9f80f65da6 100644 --- a/tests/unit_tests/hyperion/parameters/test_parameter_model.py +++ b/tests/unit_tests/hyperion/parameters/test_parameter_model.py @@ -10,12 +10,14 @@ GridParamUpdate, ) from mx_bluesky.common.parameters.constants import GridscanParamConstants +from mx_bluesky.common.parameters.gridscan import GridScanParams from mx_bluesky.common.parameters.rotation import ( SingleRotationScan, ) from mx_bluesky.hyperion.parameters.gridscan import ( HyperionSpecifiedThreeDGridScan, OddYStepsError, + panda_fast_gridscan_params, ) from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect from mx_bluesky.hyperion.parameters.robot_load import RobotLoadThenCentre @@ -82,9 +84,18 @@ def test_minimal_3d_gridscan_params(minimal_3d_gridscan_params): def test_cant_do_panda_fgs_with_odd_y_steps(minimal_3d_gridscan_params): test_params = HyperionSpecifiedThreeDGridScan(**minimal_3d_gridscan_params) + grid_scan_params = GridScanParams( + omega_starts_deg=test_params.omega_starts_deg, + x_steps=test_params.x_steps, + y_steps=test_params.y_steps, + x_step_size_um=test_params.x_step_size_um, + y_step_sizes_um=test_params.y_step_sizes_um, + x_start_um=test_params.x_start_um, + y_starts_um=test_params.y_starts_um, + z_starts_um=test_params.z_starts_um, + ) with pytest.raises(OddYStepsError): - _ = test_params.panda_fast_gridscan_params - assert test_params.fast_gridscan_params + _ = panda_fast_gridscan_params(test_params, grid_scan_params) def test_serialise_deserialise(minimal_3d_gridscan_params): From e58352161c5038efb5d4021d7892bae217102078 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Wed, 1 Jul 2026 15:27:16 +0100 Subject: [PATCH 3/4] Fixup axis bounds checking on xrc results --- .../inner_plans/xrc_results_utils.py | 13 ++++++++++++- .../test_load_centre_collect_full_plan.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) 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 f34112ffb4..b649f70268 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 @@ -111,7 +111,7 @@ def grid_position_to_motor_position( assert grid_scan_params.num_grids == 2 assert grid_scan_params.y_starts_um[0] == grid_scan_params.y_starts_um[1] assert grid_scan_params.z_starts_um[0] == grid_scan_params.z_starts_um[1] - return ( + motor_pos = ( np.array( [ ( @@ -130,6 +130,17 @@ def grid_position_to_motor_position( ) / 1000 ) + if not _is_inside_3d_grid(grid_pos, grid_scan_params): + raise IndexError(f"{motor_pos} is outside the bounds of the grid") + return motor_pos + + +def _is_inside_3d_grid(grid_pos: np.ndarray, grid_scan_params: GridScanParams): + return ( + (-0.5 <= grid_pos[0] <= grid_scan_params.x_steps - 0.5) + and (-0.5 <= grid_pos[1] <= grid_scan_params.y_steps[0] - 0.5) + and (-0.5 <= grid_pos[2] <= grid_scan_params.y_steps[1] - 0.5) + ) def _xrc_result_in_boxes_to_result_in_mm( 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 963460ba58..403cbfadf1 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 @@ -34,6 +34,7 @@ from mx_bluesky.common.external_interaction.callbacks.sample_handling.sample_handling_callback import ( SampleHandlingCallback, ) +from mx_bluesky.common.parameters.gridscan import GridScanParams from mx_bluesky.common.utils.exceptions import ( CrystalNotFoundError, WarningError, @@ -1096,8 +1097,10 @@ def throw_on_third_call_wrapper(plan, *args, **kwargs): @pytest.fixture def patch_detect_grid_and_do_gridscan_with_detected_pin_position( load_centre_collect_composite: LoadCentreCollectComposite, + grid_detect_for_snapshot_generation: GridParamUpdate, ): wrapped = detect_grid_and_do_gridscan + grid_parameters = grid_detect_for_snapshot_generation # Before we do the grid scan, pretend we detected the pin at this position and move to it # This is the base snapshot position @@ -1112,6 +1115,16 @@ def wrapper(*args, **kwargs): ) yield from wrapped(*args, **kwargs) + return GridScanParams( + omega_starts_deg=[0, 90], + x_start_um=grid_parameters["x_start_um"], + y_starts_um=grid_parameters["y_starts_um"], + z_starts_um=grid_parameters["z_starts_um"], + x_steps=grid_parameters["x_steps"], + y_steps=grid_parameters["y_steps"], + x_step_size_um=grid_parameters["x_step_size_um"], + y_step_sizes_um=grid_parameters["y_step_sizes_um"], + ) with patch( "mx_bluesky.hyperion.experiment_plans.pin_centre_then_gridscan_plan.detect_grid_and_do_gridscan", From 83a3db6ffc4d9e4f2951a7c5f1e1662e90cb9a79 Mon Sep 17 00:00:00 2001 From: Robert Tuck Date: Wed, 1 Jul 2026 17:09:01 +0100 Subject: [PATCH 4/4] Additional docs for GridScanParams Minor fix to grid_position_to_motor_position Remove assertions in grid_position_to_motor_position More clearly define semantics of omega_starts_deg --- ...ommon_grid_detect_then_xray_centre_plan.py | 2 +- .../inner_plans/xrc_results_utils.py | 4 +- src/mx_bluesky/common/parameters/gridscan.py | 26 +++++++ .../common/utils/test_xrc_result.py | 70 +++++++++++++++++++ 4 files changed, 98 insertions(+), 4 deletions(-) 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 07adf0c906..c66f52125c 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 @@ -212,7 +212,7 @@ def create_parameters_for_flyscan_xray_centre( xrc_params_type: type[SpecifiedThreeDGridScan], ) -> tuple[SpecifiedThreeDGridScan, GridScanParams]: grid_scan_params = GridScanParams( - omega_starts_deg=parameters.omega_starts_deg, + omega_starts_deg=[0, 90], x_start_um=grid_parameters["x_start_um"], y_starts_um=grid_parameters["y_starts_um"], z_starts_um=grid_parameters["z_starts_um"], 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 b649f70268..2d17874d06 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 @@ -109,8 +109,6 @@ def grid_position_to_motor_position( grid_scan_params: GridScanParams, grid_pos: np.ndarray ) -> np.ndarray: assert grid_scan_params.num_grids == 2 - assert grid_scan_params.y_starts_um[0] == grid_scan_params.y_starts_um[1] - assert grid_scan_params.z_starts_um[0] == grid_scan_params.z_starts_um[1] motor_pos = ( np.array( [ @@ -123,7 +121,7 @@ def grid_position_to_motor_position( + grid_pos[1] * grid_scan_params.y_step_sizes_um[0] ), ( - grid_scan_params.z_starts_um[0] + grid_scan_params.z_starts_um[1] + grid_pos[2] * grid_scan_params.y_step_sizes_um[1] ), ] diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index b7bf512d0d..5de9b1841a 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -45,11 +45,37 @@ class GridDetectionParams(BaseModel): + """ + Parameters that specify the characteristic dimensions of one or more grids to be detected. + + Attributes: + box_size_um: The dimensions of the boxes in the grid in microns. + grid_width_um: The suggested width of the grid to be detected. + """ + box_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM) grid_width_um: float = Field(default=GridscanParamConstants.PIN_WIDTH_UM) class GridScanParams(BaseModel): + """ + Parameters that specify a series of grid scans to be carried out. + The grids to be scanned must all originate at the same x-coordinate, and have the same number of steps in x'. + + Note for the purposes of this description, x, y, z are coordinates in sample space, whereas x', y' are coordinates + in grid space for the respective grid scans. + + Attributes: + omega_starts_deg: value of omega for each of the scans, which defines the projection of y' onto the y, z axes. + x_start_um: sample x position at the origin of all grid scans in microns. + y_starts_um: sample y positions at the origins of each successive grid scan in microns. + z_starts_um: samply z positions at the origins of each successive grid scan in microns. + x_steps: number of grid steps in the x'-coordinate for each grid scan. + y_steps: number of grid steps in the y'-coordinate for each grid scan. + x_step_size_um: Size of the steps in the x'-coordinate in microns + y_step_sizes_um: Sizes of the steps in the y'-coordinate for each grid scan in microns. + """ + omega_starts_deg: list[int] = Field( default=[GridscanParamConstants.OMEGA_1, GridscanParamConstants.OMEGA_2] ) diff --git a/tests/unit_tests/common/utils/test_xrc_result.py b/tests/unit_tests/common/utils/test_xrc_result.py index e841bd6d0a..c3ca9ffc47 100644 --- a/tests/unit_tests/common/utils/test_xrc_result.py +++ b/tests/unit_tests/common/utils/test_xrc_result.py @@ -1,3 +1,5 @@ +from contextlib import nullcontext + import bluesky.plan_stubs as bps import numpy as np import pytest @@ -12,6 +14,7 @@ from mx_bluesky.common.experiment_plans.inner_plans.do_fgs import ZOCALO_STAGE_GROUP from mx_bluesky.common.experiment_plans.inner_plans.xrc_results_utils import ( fetch_xrc_results_from_zocalo, + grid_position_to_motor_position, zocalo_stage_decorator, ) from mx_bluesky.common.parameters.gridscan import ( @@ -31,6 +34,20 @@ from ....conftest import TestData + +@pytest.fixture +def grid_scan_params(): + yield GridScanParams( + x_steps=10, + y_steps=[15, 20], + x_step_size_um=300, + y_step_sizes_um=[200, 100], + x_start_um=0, + y_starts_um=[1000, 2000], + z_starts_um=[3000, 4000], + ) + + """ Below are tests for getting zocalo results from the zocalo device, which is done post gridscan """ @@ -159,3 +176,56 @@ def test_plan(): msgs, predicate=lambda msg: msg.command == "unstage" and msg.obj.name == "zocalo", ) + + +@pytest.mark.parametrize( + "grid_position, expected", + [ + [np.array([-1, 2, 4]), pytest.raises(IndexError)], + [np.array([11, 2, 4]), pytest.raises(IndexError)], + [np.array([1, 17, 4]), pytest.raises(IndexError)], + [np.array([1, 5, 22]), pytest.raises(IndexError)], + [np.array([0, 0, 0]), nullcontext(np.array([0, 1, 4]))], + [np.array([1, 1, 1]), nullcontext(np.array([0.3, 1.2, 4.1]))], + [np.array([2, 11, 16]), nullcontext(np.array([0.6, 3.2, 5.6]))], + [np.array([6, 5, 5]), nullcontext(np.array([1.8, 2.0, 4.5]))], + [np.array([-0.51, 5, 5]), pytest.raises(IndexError)], + [ + np.array([-0.5, 5, 5]), + nullcontext(np.array([-0.5 * 0.3, 1 + 5 * 0.2, 4 + 5 * 0.1])), + ], + [np.array([5, -0.51, 5]), pytest.raises(IndexError)], + [ + np.array([5, -0.5, 5]), + nullcontext(np.array([5 * 0.3, 1 - 0.5 * 0.2, 4 + 5 * 0.1])), + ], + [np.array([5, 5, -0.51]), pytest.raises(IndexError)], + [ + np.array([5, 5, -0.5]), + nullcontext(np.array([5 * 0.3, 1 + 5 * 0.2, 4 - 0.5 * 0.1])), + ], + [np.array([9.51, 5, 5]), pytest.raises(IndexError)], + [ + np.array([9.5, 5, 5]), + nullcontext(np.array([9.5 * 0.3, 1 + 5 * 0.2, 4 + 5 * 0.1])), + ], + [np.array([5, 14.51, 5]), pytest.raises(IndexError)], + [ + np.array([5, 14.5, 5]), + nullcontext(np.array([5 * 0.3, 1 + 14.5 * 0.2, 4 + 5 * 0.1])), + ], + [np.array([5, 5, 19.51]), pytest.raises(IndexError)], + [ + np.array([5, 5, 19.5]), + nullcontext(np.array([5 * 0.3, 1 + 5 * 0.2, 4 + 19.5 * 0.1])), + ], + ], +) +def test_given_x_y_z_out_of_range_then_converting_to_motor_coords_raises( + grid_scan_params: GridScanParams, grid_position, expected +): + with expected as expected_value: + motor_position = grid_position_to_motor_position( + grid_scan_params, grid_position + ) + assert np.allclose(motor_position, expected_value)