diff --git a/.gitignore b/.gitignore index 76a7369b3..861b9129e 100644 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,6 @@ edm_serial/ **/parameters/fixed_target/*/*.map # idea project files .idea/ + +# Generated PlantUML diagrams +docs/developer/hyperion/reference/param_hierarchy.puml diff --git a/docs/conf.py b/docs/conf.py index 9b0b18626..76b71cb01 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -49,6 +49,8 @@ "sphinx_design", # For markdown "myst_parser", + # For plantUML diagrams + "plantweb.directive", ] myst_enable_extensions = [ diff --git a/docs/developer/hyperion/reference/gridscan.puml b/docs/developer/hyperion/reference/gridscan.puml new file mode 100644 index 000000000..c541be14f --- /dev/null +++ b/docs/developer/hyperion/reference/gridscan.puml @@ -0,0 +1,109 @@ +@startuml +title Gridscan Parameter Relationships + +class DiffractionExperiment +class DiffractionExperimentWithSample +class GridCommon { + grid_width_um + exposure_time_s +} +class GridScanWithEdgeDetect { + box_size_um +} +class HyperionGridCommon { + enable_dev_shm +} +class HyperionThreeDGridScan { + x_step_size_um + y_step_size_um + z_step_size_um + y2_start_um + z2_start_um + -- + grid_1_spec + grid_2_spec + scan_indices + scan_spec + scan_points + scan_points_first_grid + scan_points_second_grid + num_images + FGS_Params + panda_FGS_Params +} +class MxBlueSkyParameters +class SpecifiedGrid +class XyzStarts { + x_start_um + y_start_um + z_start_um +} +class OptionalXYZStarts { + x_start_um + y_start_um + z_start_um +} +class RotationScanPerSweep + +MxBlueSkyParameters <|-- DiffractionExperiment +DiffractionExperiment <|-- DiffractionExperimentWithSample +DiffractionExperimentWithSample <|-- GridCommon +GridCommon <|-- GridScanWithEdgeDetect +GridCommon <|-- HyperionGridCommon +HyperionGridCommon <|-- HyperionThreeDGridScan +SpecifiedGrid <|-- HyperionThreeDGridScan +XyzStarts <|-- SpecifiedGrid +OptionalXYZStarts <|-- RotationScanPerSweep +class GridParamUpdate { + x_start_um + y_start_um + y2_start_um + z_start_um + z2_start_um + x_steps + y_steps + z_steps + x_step_size_um + y_step_size_um + z_step_size_um +} + +class GridDetectionCallback { + get_grid_parameters() -> GridParamUpdate +} + +GridDetectionCallback --> GridParamUpdate : generates from event. Adds 0.5 to get box-centres +GridParamUpdate --> HyperionThreeDGridScan : combines with GridScanWithEdgeDetect + +class experiment_plans { + grid_detect_then_xray_centre() + flyscan_xray_centre_no_move() + create_parameters_for_flyscan_xray_centre(GridScanWithEdgeDetect, GridParamUpdate) -> HyperionThreeDGridScan +} + +class AbstractExperimentBase +class AbstractExperimentWithBeamParams +class GridScanParamsCommon { + x_steps + y_steps + z_steps + x_step_size_mm + y_step_size_mm + z_step_size_mm + x_start_mm + y1_start_mm + y2_start_mm + z1_start_mm + z2_start_mm +} +class PandAGridScanParams +class ZebraGridScanParams + +AbstractExperimentBase <|-- AbstractExperimentWithBeamParams +AbstractExperimentWithBeamParams <|-- GridScanParamsCommon +GridScanParamsCommon <|-- PandAGridScanParams +GridScanParamsCommon <|-- ZebraGridScanParams + +HyperionThreeDGridScan --> ZebraGridScanParams : generates +HyperionThreeDGridScan --> PandAGridScanParams : generates +@enduml diff --git a/docs/developer/hyperion/reference/param-hierarchy.rst b/docs/developer/hyperion/reference/param-hierarchy.rst index 9e59cbc5f..1147d1b81 100644 --- a/docs/developer/hyperion/reference/param-hierarchy.rst +++ b/docs/developer/hyperion/reference/param-hierarchy.rst @@ -1,4 +1,4 @@ Hyperion Parameter class hierarchy ================================== -TODO: automate including the param hierarchy here +.. uml:: param_hierarchy.puml diff --git a/docs/developer/hyperion/reference/param_hierarchy.puml b/docs/developer/hyperion/reference/param_hierarchy.puml deleted file mode 100644 index d0b7da636..000000000 --- a/docs/developer/hyperion/reference/param_hierarchy.puml +++ /dev/null @@ -1,86 +0,0 @@ -@startuml hyperion_parameter_model -'https://plantuml.com/class-diagram -title Hyperion Parameter Model - -abstract class BaseModel - -package Mixins { - class WithSample - class WithScan - class WithOavCentring - class WithOptionalEnergyChange - class WithSnapshot - class WithVisit - class OptionalXyzStarts - class XyzStarts - class OptionalGonioAngleStarts - class SplitScan - class RotationScanPerSweep - class RotationExperiment -} - -package Experiments { - class DiffractionExperiment - class DiffractionExperimentWithSample - class GridCommon - class GridScanWithEdgeDetect - class LoadCentreCollect - class PinTipCentreThenXrayCentre - class RotationScan - class MultiRotationScan - class RobotLoadAndEnergyChange - class RobotLoadThenCentre - class SpecifiedGridScan - class HyperionThreeDGridScan -} - -class HyperionParameters -note top: Base class for all experiment parameter models - -BaseModel <|-- HyperionParameters -BaseModel <|-- SplitScan -BaseModel <|-- OptionalGonioAngleStarts -BaseModel <|-- OptionalXyzStarts -BaseModel <|-- WithOavCentring -BaseModel <|-- WithOptionalEnergyChange -BaseModel <|-- WithSnapshot -BaseModel <|-- WithSample -BaseModel <|-- WithScan -BaseModel <|-- WithVisit -BaseModel <|-- XyzStarts - -OptionalGonioAngleStarts <|-- RotationScanPerSweep -OptionalXyzStarts <|-- RotationScanPerSweep -DiffractionExperimentWithSample <|-- RotationExperiment -HyperionParameters <|-- DiffractionExperiment -WithSnapshot <|-- DiffractionExperiment -WithOptionalEnergyChange <|-- DiffractionExperiment -WithVisit <|-- DiffractionExperiment -DiffractionExperiment <|-- DiffractionExperimentWithSample -WithSample <|-- DiffractionExperimentWithSample -DiffractionExperimentWithSample <|-- GridCommon -GridCommon <|-- GridScanWithEdgeDetect -GridCommon <|-- PinTipCentreThenXrayCentre -GridCommon <|-- RobotLoadThenCentre -RobotLoadThenCentre *-- RobotLoadAndEnergyChange -RobotLoadThenCentre *-- PinTipCentreThenXrayCentre -GridCommon <|-- SpecifiedGridScan -WithScan <|-- SpecifiedGridScan -SpecifiedGridScan <|-- HyperionThreeDGridScan -SplitScan <|-- HyperionThreeDGridScan -WithOptionalEnergyChange <|-- HyperionThreeDGridScan -WithOavCentring <|-- GridCommon -WithScan <|-- RotationScan -RotationScanPerSweep <|-- RotationScan -MultiRotationScan *-- RotationScanPerSweep -RotationExperiment <|-- RotationScan -RotationExperiment <|-- MultiRotationScan -SplitScan <|-- MultiRotationScan -XyzStarts <|-- SpecifiedGridScan -OptionalGonioAngleStarts <|-- GridCommon -OptionalGonioAngleStarts <|-- RotationScan -HyperionParameters <|-- RobotLoadAndEnergyChange -WithSample <|-- RobotLoadAndEnergyChange -WithSnapshot <|-- RobotLoadAndEnergyChange -WithOptionalEnergyChange <|-- RobotLoadAndEnergyChange -@enduml diff --git a/pyproject.toml b/pyproject.toml index aaab9bf81..95613c085 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ "ophyd == 1.9.0", "ophyd-async >= 0.8a5", "bluesky >= 1.13.0a4", - "dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@6295feafb43acd158ed80b30f06de2bf29fe4b16", + "dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@076d23f5e2902b7775bda37d221a04557f09bd9d", ] @@ -68,6 +68,7 @@ dev = [ "mypy", "myst-parser", "pipdeptree", + "plantweb", "pre-commit", "pydata-sphinx-theme>=0.12", "pyright", @@ -78,7 +79,6 @@ dev = [ "ruff", "sphinx-autobuild", "sphinx-copybutton", - "sphinxcontrib-plantuml", "sphinx-design", "tox-direct", "tox", @@ -180,6 +180,9 @@ commands = type-checking: pyright src tests {posargs} tests: pytest --cov=mx_bluesky --cov-report term --cov-report xml:cov.xml {posargs} docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html +commands_pre = + docs: /usr/bin/bash -c "{toxinidir}/utility_scripts/generate_plantuml.py > \ + docs/developer/hyperion/reference/param_hierarchy.puml" """ [tool.ruff] diff --git a/src/mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py b/src/mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py index f939278fc..466912aac 100755 --- a/src/mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +++ b/src/mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py @@ -16,7 +16,7 @@ import bluesky.preprocessors as bpp from bluesky.utils import MsgGenerator from dodal.common import inject -from dodal.devices.attenuator import ReadOnlyAttenuator +from dodal.devices.attenuator.attenuator import ReadOnlyAttenuator from dodal.devices.hutch_shutter import HutchShutter, ShutterDemand from dodal.devices.i24.aperture import Aperture from dodal.devices.i24.beam_center import DetectorBeamCenter @@ -38,6 +38,7 @@ log_on_entry, ) from mx_bluesky.beamlines.i24.serial.parameters import ExtruderParameters +from mx_bluesky.beamlines.i24.serial.parameters.constants import BEAM_CENTER_LUT_FILES from mx_bluesky.beamlines.i24.serial.setup_beamline import Pilatus, caget, caput, pv from mx_bluesky.beamlines.i24.serial.setup_beamline import setup_beamline as sup from mx_bluesky.beamlines.i24.serial.setup_beamline.setup_detector import ( @@ -206,10 +207,14 @@ def main_extruder_plan( dcid: DCID, start_time: datetime, ) -> MsgGenerator: + beam_center_pixels = sup.compute_beam_center_position_from_lut( + BEAM_CENTER_LUT_FILES[parameters.detector_name], + parameters.detector_distance_mm, + parameters.detector_size_constants, + ) yield from sup.set_detector_beam_center_plan( beam_center_device, - parameters.detector_params, - parameters.detector_distance_mm, + beam_center_pixels, ) # Setting up the beamline @@ -283,7 +288,6 @@ def main_extruder_plan( SSX_LOGGER.info("Using Eiger detector") SSX_LOGGER.debug(f"Creating the directory for the collection in {filepath}.") - # NOTE Directory now created by the parameter model caput(pv.eiger_seqID, int(caget(pv.eiger_seqID)) + 1) SSX_LOGGER.info(f"Eiger quickshot setup: filepath {filepath}") @@ -490,6 +494,8 @@ def run_extruder_plan( parameters: ExtruderParameters = yield from read_parameters( detector_stage, attenuator ) + # Create collection directory + parameters.collection_directory.mkdir(parents=True, exist_ok=True) beam_center_device = sup.get_beam_center_device(parameters.detector_name) diff --git a/src/mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py b/src/mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py index 55505b40e..710b08f15 100755 --- a/src/mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +++ b/src/mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py @@ -10,7 +10,7 @@ import bluesky.preprocessors as bpp from bluesky.utils import MsgGenerator from dodal.common import inject -from dodal.devices.attenuator import ReadOnlyAttenuator +from dodal.devices.attenuator.attenuator import ReadOnlyAttenuator from dodal.devices.hutch_shutter import HutchShutter, ShutterDemand from dodal.devices.i24.aperture import Aperture from dodal.devices.i24.beam_center import DetectorBeamCenter @@ -38,6 +38,7 @@ ) from mx_bluesky.beamlines.i24.serial.log import SSX_LOGGER, log_on_entry from mx_bluesky.beamlines.i24.serial.parameters import FixedTargetParameters +from mx_bluesky.beamlines.i24.serial.parameters.constants import BEAM_CENTER_LUT_FILES from mx_bluesky.beamlines.i24.serial.setup_beamline import caget, cagetstring, caput, pv from mx_bluesky.beamlines.i24.serial.setup_beamline import setup_beamline as sup from mx_bluesky.beamlines.i24.serial.setup_beamline.setup_zebra_plans import ( @@ -410,7 +411,6 @@ def start_i24( SSX_LOGGER.info("Using Eiger detector") SSX_LOGGER.debug(f"Creating the directory for the collection in {filepath}.") - # NOTE Directory now created by the parameter model SSX_LOGGER.info(f"Triggered Eiger setup: filepath {filepath}") SSX_LOGGER.info(f"Triggered Eiger setup: filename {filename}") @@ -547,10 +547,14 @@ def main_fixed_target_plan( ) -> MsgGenerator: SSX_LOGGER.info("Running a chip collection on I24") + beam_center_pixels = sup.compute_beam_center_position_from_lut( + BEAM_CENTER_LUT_FILES[parameters.detector_name], + parameters.detector_distance_mm, + parameters.detector_size_constants, + ) yield from sup.set_detector_beam_center_plan( beam_center_device, - parameters.detector_params, - parameters.detector_distance_mm, + beam_center_pixels, ) SSX_LOGGER.info("Getting Program Dictionary") @@ -699,6 +703,9 @@ def run_fixed_target_plan( detector_stage, attenuator ) + # Create collection directory + parameters.collection_directory.mkdir(parents=True, exist_ok=True) + if parameters.chip_map: upload_chip_map_to_geobrick(pmac, parameters.chip_map) diff --git a/src/mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py b/src/mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py index 9c4545f0a..ced6bfcc1 100755 --- a/src/mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +++ b/src/mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py @@ -15,7 +15,7 @@ import numpy as np from bluesky.utils import MsgGenerator from dodal.common import inject -from dodal.devices.attenuator import ReadOnlyAttenuator +from dodal.devices.attenuator.attenuator import ReadOnlyAttenuator from dodal.devices.i24.beamstop import Beamstop, BeamstopPositions from dodal.devices.i24.dual_backlight import BacklightPositions, DualBacklight from dodal.devices.i24.i24_detector_motion import DetectorMotion diff --git a/src/mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py b/src/mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py index d993b3d7d..8607037f5 100644 --- a/src/mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py +++ b/src/mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py @@ -3,8 +3,11 @@ from pathlib import Path import numpy as np -from dodal.devices.detector import DetectorParams, TriggerMode -from dodal.devices.detector.det_dim_constants import EIGER2_X_9M_SIZE, PILATUS_6M_SIZE +from dodal.devices.detector.det_dim_constants import ( + EIGER2_X_9M_SIZE, + PILATUS_6M_SIZE, + DetectorSizeConstants, +) from pydantic import BaseModel, ConfigDict, computed_field, field_validator from mx_bluesky.beamlines.i24.serial.fixed_target.ft_utils import ( @@ -13,7 +16,6 @@ PumpProbeSetting, ) from mx_bluesky.beamlines.i24.serial.parameters.constants import ( - BEAM_CENTER_LUT_FILES, DetectorName, SSXType, ) @@ -40,9 +42,16 @@ def _parse_visit(cls, visit: str | Path): @property def collection_directory(self) -> Path: directory = Path(self.visit) / self.directory - directory.mkdir(parents=True, exist_ok=True) return directory + @property + def detector_size_constants(self) -> DetectorSizeConstants: + return ( + EIGER2_X_9M_SIZE + if self.detector_name is DetectorName.EIGER + else PILATUS_6M_SIZE + ) + class LaserExperiment(BaseModel): """Laser settings for pump probe serial collections.""" @@ -69,10 +78,6 @@ def nexgen_experiment_type(self) -> str: def ispyb_experiment_type(self) -> SSXType: pass - @property - @abstractmethod - def detector_params(self) -> DetectorParams: ... - class ExtruderParameters(SerialAndLaserExperiment): """Extruder parameter model.""" @@ -88,32 +93,6 @@ def nexgen_experiment_type(self) -> str: def ispyb_experiment_type(self) -> SSXType: return SSXType.EXTRUDER - @property - def detector_params(self): - det_dist_to_beam_lut = BEAM_CENTER_LUT_FILES[self.detector_name] - det_size_constants = ( - EIGER2_X_9M_SIZE - if self.detector_name is DetectorName.EIGER - else PILATUS_6M_SIZE - ) - - self.collection_directory.mkdir(parents=True, exist_ok=True) - - return DetectorParams( - detector_size_constants=det_size_constants, - exposure_time=self.exposure_time_s, - directory=self.collection_directory.as_posix(), - prefix=self.filename, - detector_distance=self.detector_distance_mm, - omega_start=0.0, - omega_increment=0.0, - num_images_per_trigger=1, - num_triggers=self.num_images, - det_dist_to_beam_converter_path=det_dist_to_beam_lut.as_posix(), - use_roi_mode=False, # Dasabled - trigger_mode=TriggerMode.SET_FRAMES, # For now... - ) - class ChipDescription(BaseModel): """Parameters defining the chip in use for FT collection.""" @@ -169,32 +148,6 @@ def nexgen_experiment_type(self) -> str: def ispyb_experiment_type(self) -> SSXType: return SSXType.FIXED - @property - def detector_params(self): - det_dist_to_beam_lut = BEAM_CENTER_LUT_FILES[self.detector_name] - det_size_constants = ( - EIGER2_X_9M_SIZE - if self.detector_name is DetectorName.EIGER - else PILATUS_6M_SIZE - ) - - self.collection_directory.mkdir(parents=True, exist_ok=True) - - return DetectorParams( - detector_size_constants=det_size_constants, - exposure_time=self.exposure_time_s, - directory=self.collection_directory.as_posix(), - prefix=self.filename, - detector_distance=self.detector_distance_mm, - omega_start=0.0, - omega_increment=0.0, - num_images_per_trigger=self.num_exposures, - num_triggers=self.total_num_images, - det_dist_to_beam_converter_path=det_dist_to_beam_lut.as_posix(), - use_roi_mode=False, # Dasabled - trigger_mode=TriggerMode.SET_FRAMES, # For now... - ) - @computed_field # type: ignore # Mypy doesn't like it @property def total_num_images(self) -> int: diff --git a/src/mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py b/src/mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py index 74da59c24..890066b9c 100644 --- a/src/mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +++ b/src/mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py @@ -1,13 +1,18 @@ +from pathlib import Path from time import sleep import bluesky.plan_stubs as bps from dodal.beamlines import i24 -from dodal.devices.detector.detector import DetectorParams +from dodal.devices.detector.det_dim_constants import DetectorSizeConstants from dodal.devices.i24.aperture import Aperture, AperturePositions from dodal.devices.i24.beam_center import DetectorBeamCenter from dodal.devices.i24.beamstop import Beamstop, BeamstopPositions from dodal.devices.i24.dual_backlight import BacklightPositions, DualBacklight from dodal.devices.i24.i24_detector_motion import DetectorMotion +from dodal.devices.util.lookup_tables import ( + linear_interpolation_lut, + parse_lookup_table, +) from mx_bluesky.beamlines.i24.serial.log import SSX_LOGGER from mx_bluesky.beamlines.i24.serial.setup_beamline import pv @@ -21,6 +26,35 @@ def get_beam_center_device(detector_in_use: str) -> DetectorBeamCenter: return i24.pilatus_beam_center() +def compute_beam_center_position_from_lut( + lut_path: Path, + detector_distance_mm: float, + det_size_constants: DetectorSizeConstants, +) -> tuple[float, float]: + """Calculate the beam center position for the detector distance \ + using the values in the lookup table for the conversion. + """ + lut_values = parse_lookup_table(lut_path.as_posix()) + + calc_x = linear_interpolation_lut(lut_values[0], lut_values[1]) + beam_x_mm = calc_x(detector_distance_mm) + beam_x = ( + beam_x_mm + * det_size_constants.det_size_pixels.width + / det_size_constants.det_dimension.width + ) + + calc_y = linear_interpolation_lut(lut_values[0], lut_values[2]) + beam_y_mm = calc_y(detector_distance_mm) + beam_y = ( + beam_y_mm + * det_size_constants.det_size_pixels.height + / det_size_constants.det_dimension.height + ) + + return beam_x, beam_y + + def setup_beamline_for_collection_plan( aperture: Aperture, backlight: DualBacklight, @@ -55,17 +89,14 @@ def move_detector_stage_to_position_plan( def set_detector_beam_center_plan( beam_center_device: DetectorBeamCenter, - detector_params: DetectorParams, - detector_distace: float, + beam_center_pixels: tuple[float, float], group: str = "set_beamcenter", wait: bool = True, ): """A small temporary plan to set up the beam center on the detector in use.""" # NOTE This will be removed once the detectors are using ophyd_async devices # See https://github.com/DiamondLightSource/mx-bluesky/issues/62 - beam_position_x, beam_position_y = detector_params.get_beam_position_pixels( - detector_distace - ) + beam_position_x, beam_position_y = beam_center_pixels SSX_LOGGER.info(f"Setting beam center to: {beam_position_x}, {beam_position_y}") yield from bps.abs_set(beam_center_device.beam_x, beam_position_x, group=group) yield from bps.abs_set(beam_center_device.beam_y, beam_position_y, group=group) diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/__init__.py b/src/mx_bluesky/common/external_interaction/__init__.py similarity index 100% rename from src/mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/__init__.py rename to src/mx_bluesky/common/external_interaction/__init__.py diff --git a/src/mx_bluesky/hyperion/external_interaction/ispyb/__init__.py b/src/mx_bluesky/common/external_interaction/callbacks/common/__init__.py similarity index 100% rename from src/mx_bluesky/hyperion/external_interaction/ispyb/__init__.py rename to src/mx_bluesky/common/external_interaction/callbacks/common/__init__.py diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/common/abstract_event.py b/src/mx_bluesky/common/external_interaction/callbacks/common/abstract_event.py similarity index 100% rename from src/mx_bluesky/hyperion/external_interaction/callbacks/common/abstract_event.py rename to src/mx_bluesky/common/external_interaction/callbacks/common/abstract_event.py diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/aperture_change_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/common/aperture_change_callback.py similarity index 94% rename from src/mx_bluesky/hyperion/external_interaction/callbacks/aperture_change_callback.py rename to src/mx_bluesky/common/external_interaction/callbacks/common/aperture_change_callback.py index 593e13e10..f86f32b52 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/aperture_change_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/common/aperture_change_callback.py @@ -1,7 +1,7 @@ from bluesky.callbacks import CallbackBase from event_model.documents.run_start import RunStart -from mx_bluesky.hyperion.log import LOGGER +from mx_bluesky.common.utils.log import LOGGER from .logging_callback import format_doc_for_log diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py similarity index 75% rename from src/mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py rename to src/mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py index e131c5122..7db7e3482 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py @@ -5,10 +5,28 @@ from dodal.devices.oav.utils import calculate_x_y_z_of_pixel from event_model.documents import Event -from mx_bluesky.hyperion.log import LOGGER +from mx_bluesky.common.utils.log import LOGGER class GridParamUpdate(TypedDict): + """ + Grid parameters extracted from the grid detection. + 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: float y_start_um: float y2_start_um: float diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/ispyb_callback_base.py b/src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py similarity index 79% rename from src/mx_bluesky/hyperion/external_interaction/callbacks/ispyb_callback_base.py rename to src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py index f3050d571..581237953 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/ispyb_callback_base.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py @@ -9,26 +9,26 @@ from dodal.devices.detector.det_resolution import resolution from dodal.devices.synchrotron import SynchrotronMode -from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample -from mx_bluesky.common.utils.log import set_dcgid_tag -from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import ( +from mx_bluesky.common.external_interaction.callbacks.common.logging_callback import ( + format_doc_for_log, +) +from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import ( PlanReactiveCallback, ) -from mx_bluesky.hyperion.external_interaction.ispyb.data_model import ( +from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionInfo, DataCollectionPositionInfo, ScanDataInfo, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import ( +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( IspybIds, StoreInIspyb, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_utils import get_ispyb_config -from mx_bluesky.hyperion.log import ISPYB_LOGGER -from mx_bluesky.hyperion.parameters.constants import CONST -from mx_bluesky.hyperion.utils.utils import convert_eV_to_angstrom - -from .logging_callback import format_doc_for_log +from mx_bluesky.common.external_interaction.ispyb.ispyb_utils import get_ispyb_config +from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample +from mx_bluesky.common.parameters.constants import DocDescriptorNames, SimConstants +from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, set_dcgid_tag +from mx_bluesky.common.utils.utils import convert_eV_to_angstrom D = TypeVar("D") if TYPE_CHECKING: @@ -63,25 +63,25 @@ def __init__( """Subclasses should run super().__init__() with parameters, then set self.ispyb to the type of ispyb relevant to the experiment and define the type for self.ispyb_ids.""" - ISPYB_LOGGER.debug("Initialising ISPyB callback") - super().__init__(log=ISPYB_LOGGER, emit=emit) + ISPYB_ZOCALO_CALLBACK_LOGGER.debug("Initialising ISPyB callback") + super().__init__(log=ISPYB_ZOCALO_CALLBACK_LOGGER, emit=emit) self._oav_snapshot_event_idx: int = 0 self.params: DiffractionExperimentWithSample | None = None self.ispyb: StoreInIspyb self.descriptors: dict[str, EventDescriptor] = {} self.ispyb_config = get_ispyb_config() if ( - self.ispyb_config == CONST.SIM.ISPYB_CONFIG - or self.ispyb_config == CONST.SIM.DEV_ISPYB_DATABASE_CFG + self.ispyb_config == SimConstants.ISPYB_CONFIG + or self.ispyb_config == SimConstants.DEV_ISPYB_DATABASE_CFG ): - ISPYB_LOGGER.warning( + ISPYB_ZOCALO_CALLBACK_LOGGER.warning( f"{self.__class__} using dev ISPyB config: {self.ispyb_config}. If you" "want to use the real database, please set the ISPYB_CONFIG_PATH " "environment variable." ) self.uid_to_finalize_on: str | None = None self.ispyb_ids: IspybIds = IspybIds() - self.log = ISPYB_LOGGER + self.log = ISPYB_ZOCALO_CALLBACK_LOGGER def activity_gated_start(self, doc: RunStart): self._oav_snapshot_event_idx = 0 @@ -94,31 +94,33 @@ def activity_gated_descriptor(self, doc: EventDescriptor): def activity_gated_event(self, doc: Event) -> Event: """Subclasses should extend this to add a call to set_dcig_tag from hyperion.log""" - ISPYB_LOGGER.debug("ISPyB handler received event document.") + ISPYB_ZOCALO_CALLBACK_LOGGER.debug("ISPyB handler received event document.") assert self.ispyb is not None, "ISPyB deposition wasn't initialised!" assert self.params is not None, "ISPyB handler didn't receive parameters!" event_descriptor = self.descriptors.get(doc["descriptor"]) if event_descriptor is None: - ISPYB_LOGGER.warning( + ISPYB_ZOCALO_CALLBACK_LOGGER.warning( f"Ispyb handler {self} received event doc {format_doc_for_log(doc)} and " "has no corresponding descriptor record" ) return doc match event_descriptor.get("name"): - case CONST.DESCRIPTORS.HARDWARE_READ_PRE: + case DocDescriptorNames.HARDWARE_READ_PRE: scan_data_infos = self._handle_ispyb_hardware_read(doc) - case CONST.DESCRIPTORS.HARDWARE_READ_DURING: + case DocDescriptorNames.HARDWARE_READ_DURING: scan_data_infos = self._handle_ispyb_transmission_flux_read(doc) case _: return self._tag_doc(doc) self.ispyb_ids = self.ispyb.update_deposition(self.ispyb_ids, scan_data_infos) - ISPYB_LOGGER.info(f"Received ISPYB IDs: {self.ispyb_ids}") + ISPYB_ZOCALO_CALLBACK_LOGGER.info(f"Received ISPYB IDs: {self.ispyb_ids}") return self._tag_doc(doc) def _handle_ispyb_hardware_read(self, doc) -> Sequence[ScanDataInfo]: assert self.params, "Event handled before activity_gated_start received params" - ISPYB_LOGGER.info("ISPyB handler received event from read hardware") + ISPYB_ZOCALO_CALLBACK_LOGGER.info( + "ISPyB handler received event from read hardware" + ) assert isinstance( synchrotron_mode := doc["data"]["synchrotron-synchrotron_mode"], SynchrotronMode, @@ -141,7 +143,9 @@ def _handle_ispyb_hardware_read(self, doc) -> Sequence[ScanDataInfo]: scan_data_infos = self.populate_info_for_update( hwscan_data_collection_info, hwscan_position_info, self.params ) - ISPYB_LOGGER.info("Updating ispyb data collection after hardware read.") + ISPYB_ZOCALO_CALLBACK_LOGGER.info( + "Updating ispyb data collection after hardware read." + ) return scan_data_infos def _handle_ispyb_transmission_flux_read(self, doc) -> Sequence[ScanDataInfo]: @@ -167,7 +171,9 @@ def _handle_ispyb_transmission_flux_read(self, doc) -> Sequence[ScanDataInfo]: scan_data_infos = self.populate_info_for_update( hwscan_data_collection_info, None, self.params ) - ISPYB_LOGGER.info("Updating ispyb data collection after flux read.") + ISPYB_ZOCALO_CALLBACK_LOGGER.info( + "Updating ispyb data collection after flux read." + ) self.append_to_comment(f"Aperture: {aperture}. ") return scan_data_infos @@ -183,10 +189,10 @@ def populate_info_for_update( def activity_gated_stop(self, doc: RunStop) -> RunStop: """Subclasses must check that they are recieving a stop document for the correct uid to use this method!""" - assert ( - self.ispyb is not None - ), "ISPyB handler received stop document, but deposition object doesn't exist!" - ISPYB_LOGGER.debug("ISPyB handler received stop document.") + assert self.ispyb is not None, ( + "ISPyB handler received stop document, but deposition object doesn't exist!" + ) + ISPYB_ZOCALO_CALLBACK_LOGGER.debug("ISPyB handler received stop document.") exit_status = ( doc.get("exit_status") or "Exit status not available in stop document!" ) @@ -195,7 +201,7 @@ def activity_gated_stop(self, doc: RunStop) -> RunStop: try: self.ispyb.end_deposition(self.ispyb_ids, exit_status, reason) except Exception as e: - ISPYB_LOGGER.warning( + ISPYB_ZOCALO_CALLBACK_LOGGER.warning( f"Failed to finalise ISPyB deposition on stop document: {format_doc_for_log(doc)} with exception: {e}" ) return self._tag_doc(doc) @@ -205,7 +211,7 @@ def _append_to_comment(self, id: int, comment: str) -> None: try: self.ispyb.append_to_comment(id, comment) except TypeError: - ISPYB_LOGGER.warning( + ISPYB_ZOCALO_CALLBACK_LOGGER.warning( "ISPyB deposition not initialised, can't update comment." ) diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py b/src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py similarity index 91% rename from src/mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py rename to src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py index 30694bf5d..bc7f43a64 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py @@ -1,17 +1,17 @@ from __future__ import annotations -from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample -from mx_bluesky.hyperion.external_interaction.ispyb.data_model import ( +from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionGroupInfo, DataCollectionInfo, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import ( +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( EIGER_FILE_SUFFIX, I03_EIGER_DETECTOR, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_utils import ( +from mx_bluesky.common.external_interaction.ispyb.ispyb_utils import ( get_current_time_string, ) +from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample def populate_data_collection_group(params: DiffractionExperimentWithSample): diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/log_uid_tag_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/common/log_uid_tag_callback.py similarity index 100% rename from src/mx_bluesky/hyperion/external_interaction/callbacks/log_uid_tag_callback.py rename to src/mx_bluesky/common/external_interaction/callbacks/common/log_uid_tag_callback.py diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/logging_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/common/logging_callback.py similarity index 93% rename from src/mx_bluesky/hyperion/external_interaction/callbacks/logging_callback.py rename to src/mx_bluesky/common/external_interaction/callbacks/common/logging_callback.py index 71ee07381..be43c8acc 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/logging_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/common/logging_callback.py @@ -2,7 +2,7 @@ from bluesky.callbacks import CallbackBase -from mx_bluesky.hyperion.log import LOGGER +from mx_bluesky.common.utils.log import LOGGER class _BestEffortEncoder(json.JSONEncoder): diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/plan_reactive_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/common/plan_reactive_callback.py similarity index 100% rename from src/mx_bluesky/hyperion/external_interaction/callbacks/plan_reactive_callback.py rename to src/mx_bluesky/common/external_interaction/callbacks/common/plan_reactive_callback.py diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/zocalo_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/common/zocalo_callback.py similarity index 82% rename from src/mx_bluesky/hyperion/external_interaction/callbacks/zocalo_callback.py rename to src/mx_bluesky/common/external_interaction/callbacks/common/zocalo_callback.py index 0c034bd14..255a401df 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/zocalo_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/common/zocalo_callback.py @@ -5,10 +5,13 @@ from bluesky.callbacks import CallbackBase from dodal.devices.zocalo import ZocaloStartInfo, ZocaloTrigger -from mx_bluesky.hyperion.external_interaction.exceptions import ISPyBDepositionNotMade -from mx_bluesky.hyperion.log import ISPYB_LOGGER -from mx_bluesky.hyperion.parameters.constants import CONST -from mx_bluesky.hyperion.utils.utils import number_of_frames_from_scan_spec +from mx_bluesky.common.parameters.constants import ( + DocDescriptorNames, + TriggerConstants, +) +from mx_bluesky.common.utils.exceptions import ISPyBDepositionNotMade +from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER +from mx_bluesky.common.utils.utils import number_of_frames_from_scan_spec if TYPE_CHECKING: from event_model.documents import Event, EventDescriptor, RunStart, RunStop @@ -39,11 +42,13 @@ def __init__( self._reset_state() def start(self, doc: RunStart): - ISPYB_LOGGER.info("Zocalo handler received start document.") - if triggering_plan := doc.get(CONST.TRIGGER.ZOCALO): + ISPYB_ZOCALO_CALLBACK_LOGGER.info("Zocalo handler received start document.") + if triggering_plan := doc.get(TriggerConstants.ZOCALO): self.triggering_plan = triggering_plan assert isinstance(zocalo_environment := doc.get("zocalo_environment"), str) - ISPYB_LOGGER.info(f"Zocalo environment set to {zocalo_environment}.") + ISPYB_ZOCALO_CALLBACK_LOGGER.info( + f"Zocalo environment set to {zocalo_environment}." + ) self.zocalo_interactor = ZocaloTrigger(zocalo_environment) if self.triggering_plan and doc.get("subplan_name") == self.triggering_plan: @@ -73,7 +78,7 @@ def descriptor(self, doc: EventDescriptor): def event(self, doc: Event) -> Event: event_descriptor = self.descriptors[doc["descriptor"]] - if event_descriptor.get("name") == CONST.DESCRIPTORS.ZOCALO_HW_READ: + if event_descriptor.get("name") == DocDescriptorNames.ZOCALO_HW_READ: filename = doc["data"]["eiger_odin_file_writer_id"] for start_info in self.zocalo_info: start_info.filename = filename @@ -83,7 +88,7 @@ def event(self, doc: Event) -> Event: def stop(self, doc: RunStop): if doc.get("run_start") == self.run_uid: - ISPYB_LOGGER.info( + ISPYB_ZOCALO_CALLBACK_LOGGER.info( f"Zocalo handler received stop document, for run {doc.get('run_start')}." ) assert self.zocalo_interactor is not None diff --git a/src/mx_bluesky/hyperion/external_interaction/nexus/__init__.py b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/__init__.py similarity index 100% rename from src/mx_bluesky/hyperion/external_interaction/nexus/__init__.py rename to src/mx_bluesky/common/external_interaction/callbacks/xray_centre/__init__.py diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py similarity index 84% rename from src/mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py rename to src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py index 1242179ea..82d79af25 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py @@ -12,41 +12,39 @@ get_processing_results_from_event, ) -from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample -from mx_bluesky.common.parameters.constants import PlanNameConstants -from mx_bluesky.common.utils.log import set_dcgid_tag -from mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping import ( +from mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base import ( + BaseISPyBCallback, +) +from mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping import ( populate_data_collection_group, populate_remaining_data_collection_info, ) -from mx_bluesky.hyperion.external_interaction.callbacks.ispyb_callback_base import ( - BaseISPyBCallback, -) -from mx_bluesky.hyperion.external_interaction.callbacks.logging_callback import ( +from mx_bluesky.common.external_interaction.callbacks.common.logging_callback import ( format_doc_for_log, ) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_mapping import ( +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping import ( construct_comment_for_gridscan, populate_xy_data_collection_info, populate_xz_data_collection_info, ) -from mx_bluesky.hyperion.external_interaction.exceptions import ISPyBDepositionNotMade -from mx_bluesky.hyperion.external_interaction.ispyb.data_model import ( +from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionGridInfo, DataCollectionInfo, DataCollectionPositionInfo, Orientation, ScanDataInfo, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import ( +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( IspybIds, StoreInIspyb, ) -from mx_bluesky.hyperion.log import ISPYB_LOGGER -from mx_bluesky.hyperion.parameters.constants import CONST -from mx_bluesky.hyperion.parameters.gridscan import ( +from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample +from mx_bluesky.common.parameters.constants import DocDescriptorNames, PlanNameConstants +from mx_bluesky.common.parameters.gridscan import ( GridCommon, ) +from mx_bluesky.common.utils.exceptions import ISPyBDepositionNotMade +from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, set_dcgid_tag if TYPE_CHECKING: from event_model import Event, RunStart, RunStop @@ -58,11 +56,11 @@ def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters): plan_generator, md={ "activate_callbacks": ["GridscanISPyBCallback"], - "subplan_name": CONST.PLAN.GRID_DETECT_AND_DO_GRIDSCAN, - "hyperion_parameters": parameters.model_dump_json(), + "subplan_name": PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN, + "mx_bluesky_parameters": parameters.model_dump_json(), }, ), - CONST.PLAN.ISPYB_ACTIVATION, + PlanNameConstants.ISPYB_ACTIVATION, ) @@ -95,15 +93,15 @@ def __init__( def activity_gated_start(self, doc: RunStart): if doc.get("subplan_name") == PlanNameConstants.DO_FGS: self._start_of_fgs_uid = doc.get("uid") - if doc.get("subplan_name") == CONST.PLAN.GRID_DETECT_AND_DO_GRIDSCAN: + if doc.get("subplan_name") == PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN: self.uid_to_finalize_on = doc.get("uid") - ISPYB_LOGGER.info( + ISPYB_ZOCALO_CALLBACK_LOGGER.info( "ISPyB callback received start document with experiment parameters and " f"uid: {self.uid_to_finalize_on}" ) - hyperion_params = doc.get("hyperion_parameters") - assert isinstance(hyperion_params, str) - self.params = GridCommon.model_validate_json(hyperion_params) + mx_bluesky_parameters = doc.get("mx_bluesky_parameters") + assert isinstance(mx_bluesky_parameters, str) + self.params = GridCommon.model_validate_json(mx_bluesky_parameters) self.ispyb = StoreInIspyb(self.ispyb_config) data_collection_group_info = populate_data_collection_group(self.params) @@ -140,7 +138,7 @@ def activity_gated_event(self, doc: Event): descriptor_name = self.descriptors[doc["descriptor"]].get("name") if descriptor_name == ZOCALO_READING_PLAN_NAME: self._handle_zocalo_read_event(doc) - elif descriptor_name == CONST.DESCRIPTORS.OAV_GRID_SNAPSHOT_TRIGGERED: + elif descriptor_name == DocDescriptorNames.OAV_GRID_SNAPSHOT_TRIGGERED: scan_data_infos = self._handle_oav_grid_snapshot_triggered(doc) self.ispyb_ids = self.ispyb.update_deposition( self.ispyb_ids, scan_data_infos @@ -154,7 +152,7 @@ def _handle_zocalo_read_event(self, doc): proc_time = time() - self._processing_start_time crystal_summary = f"Zocalo processing took {proc_time:.2f} s. " bboxes: list[np.ndarray] = [] - ISPYB_LOGGER.info( + ISPYB_ZOCALO_CALLBACK_LOGGER.info( f"Amending comment based on Zocalo reading doc: {format_doc_for_log(doc)}" ) @@ -176,9 +174,9 @@ def _handle_zocalo_read_event(self, doc): ) else: crystal_summary += "Zocalo found no crystals in this gridscan." - assert ( - self.ispyb_ids.data_collection_ids - ), "No data collection to add results to" + assert self.ispyb_ids.data_collection_ids, ( + "No data collection to add results to" + ) self.ispyb.append_to_comment( self.ispyb_ids.data_collection_ids[0], crystal_summary ) @@ -225,7 +223,9 @@ def _handle_oav_grid_snapshot_triggered(self, doc) -> Sequence[ScanDataInfo]: data_collection_id=data_collection_id, data_collection_grid_info=data_collection_grid_info, ) - ISPYB_LOGGER.info("Updating ispyb data collection after oav snapshot.") + ISPYB_ZOCALO_CALLBACK_LOGGER.info( + "Updating ispyb data collection after oav snapshot." + ) self._oav_snapshot_event_idx += 1 return [scan_data_info] @@ -245,9 +245,9 @@ def populate_info_for_update( event_sourced_position_info: DataCollectionPositionInfo | None, params: DiffractionExperimentWithSample, ) -> Sequence[ScanDataInfo]: - assert ( - self.ispyb_ids.data_collection_ids - ), "Expect at least one valid data collection to record scan data" + assert self.ispyb_ids.data_collection_ids, ( + "Expect at least one valid data collection to record scan data" + ) xy_scan_data_info = ScanDataInfo( data_collection_info=event_sourced_data_collection_info, data_collection_id=self.ispyb_ids.data_collection_ids[0], @@ -270,7 +270,7 @@ def activity_gated_stop(self, doc: RunStop) -> RunStop: if doc.get("run_start") == self._start_of_fgs_uid: self._processing_start_time = time() if doc.get("run_start") == self.uid_to_finalize_on: - ISPYB_LOGGER.info( + ISPYB_ZOCALO_CALLBACK_LOGGER.info( "ISPyB callback received stop document corresponding to start document " f"with uid: {self.uid_to_finalize_on}." ) diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_mapping.py b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_mapping.py similarity index 92% rename from src/mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_mapping.py rename to src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_mapping.py index ff166fc50..e19dff88d 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_mapping.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_mapping.py @@ -4,7 +4,7 @@ from dodal.devices.detector import DetectorParams from dodal.devices.oav import utils as oav_utils -from mx_bluesky.hyperion.external_interaction.ispyb.data_model import ( +from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionGridInfo, DataCollectionInfo, ) @@ -43,7 +43,7 @@ def construct_comment_for_gridscan(grid_info: DataCollectionGridInfo) -> str: grid_info.microns_per_pixel_y, ) return ( - "Hyperion: Xray centring - Diffraction grid scan of " + "MX-Bluesky: Xray centring - Diffraction grid scan of " f"{grid_info.steps_x} by " f"{grid_info.steps_y} images in " f"{(grid_info.dx_in_mm * 1e3):.1f} um by " diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py similarity index 81% rename from src/mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py rename to src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py index 2dfe6bcfd..9c5e8cce0 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py +++ b/src/mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py @@ -2,17 +2,17 @@ from typing import TYPE_CHECKING -from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import ( +from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import ( PlanReactiveCallback, ) -from mx_bluesky.hyperion.external_interaction.nexus.nexus_utils import ( +from mx_bluesky.common.external_interaction.nexus.nexus_utils import ( create_beam_and_attenuator_parameters, vds_type_based_on_bit_depth, ) -from mx_bluesky.hyperion.external_interaction.nexus.write_nexus import NexusWriter -from mx_bluesky.hyperion.log import NEXUS_LOGGER -from mx_bluesky.hyperion.parameters.constants import CONST -from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan +from mx_bluesky.common.external_interaction.nexus.write_nexus import NexusWriter +from mx_bluesky.common.parameters.constants import DocDescriptorNames, PlanNameConstants +from mx_bluesky.common.parameters.gridscan import ThreeDGridScan +from mx_bluesky.common.utils.log import NEXUS_LOGGER if TYPE_CHECKING: from event_model.documents import Event, EventDescriptor, RunStart @@ -44,13 +44,13 @@ def __init__(self) -> None: self.log = NEXUS_LOGGER def activity_gated_start(self, doc: RunStart): - if doc.get("subplan_name") == CONST.PLAN.GRIDSCAN_OUTER: - hyperion_params = doc.get("hyperion_parameters") - assert isinstance(hyperion_params, str) + if doc.get("subplan_name") == PlanNameConstants.GRIDSCAN_OUTER: + mx_bluesky_parameters = doc.get("mx_bluesky_parameters") + assert isinstance(mx_bluesky_parameters, str) NEXUS_LOGGER.info( - f"Nexus writer received start document with experiment parameters {hyperion_params}" + f"Nexus writer received start document with experiment parameters {mx_bluesky_parameters}" ) - parameters = HyperionThreeDGridScan.model_validate_json(hyperion_params) + parameters = ThreeDGridScan.model_validate_json(mx_bluesky_parameters) d_size = parameters.detector_params.detector_size_constants.det_size_pixels grid_n_img_1 = parameters.scan_indices[1] grid_n_img_2 = parameters.num_images - grid_n_img_1 @@ -75,7 +75,7 @@ def activity_gated_descriptor(self, doc: EventDescriptor): def activity_gated_event(self, doc: Event) -> Event | None: assert (event_descriptor := self.descriptors.get(doc["descriptor"])) is not None - if event_descriptor.get("name") == CONST.DESCRIPTORS.HARDWARE_READ_DURING: + if event_descriptor.get("name") == DocDescriptorNames.HARDWARE_READ_DURING: data = doc["data"] for nexus_writer in [self.nexus_writer_1, self.nexus_writer_2]: assert nexus_writer, "Nexus callback did not receive start doc" diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/common/__init__.py b/src/mx_bluesky/common/external_interaction/ispyb/__init__.py similarity index 100% rename from tests/unit_tests/hyperion/external_interaction/callbacks/common/__init__.py rename to src/mx_bluesky/common/external_interaction/ispyb/__init__.py diff --git a/src/mx_bluesky/hyperion/external_interaction/ispyb/data_model.py b/src/mx_bluesky/common/external_interaction/ispyb/data_model.py similarity index 97% rename from src/mx_bluesky/hyperion/external_interaction/ispyb/data_model.py rename to src/mx_bluesky/common/external_interaction/ispyb/data_model.py index 01542ca9a..d42bf3739 100644 --- a/src/mx_bluesky/hyperion/external_interaction/ispyb/data_model.py +++ b/src/mx_bluesky/common/external_interaction/ispyb/data_model.py @@ -70,6 +70,8 @@ class DataCollectionPositionInfo: @dataclass class DataCollectionGridInfo: + """This information is used by Zocalo gridscan per-image-analysis""" + dx_in_mm: float dy_in_mm: float steps_x: int diff --git a/src/mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py b/src/mx_bluesky/common/external_interaction/ispyb/exp_eye_store.py similarity index 95% rename from src/mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py rename to src/mx_bluesky/common/external_interaction/ispyb/exp_eye_store.py index bd16df710..7a914fbad 100644 --- a/src/mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +++ b/src/mx_bluesky/common/external_interaction/ispyb/exp_eye_store.py @@ -5,11 +5,11 @@ from requests import patch, post from requests.auth import AuthBase -from mx_bluesky.hyperion.external_interaction.exceptions import ISPyBDepositionNotMade -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_utils import ( +from mx_bluesky.common.external_interaction.ispyb.ispyb_utils import ( get_current_time_string, get_ispyb_config, ) +from mx_bluesky.common.utils.exceptions import ISPyBDepositionNotMade RobotActionID = int @@ -54,9 +54,9 @@ class BLSampleStatus(StrEnum): ERROR_BEAMLINE = "ERROR - beamline" -assert all( - len(value) <= 20 for value in BLSampleStatus -), "Column size limit of 20 for BLSampleStatus" +assert all(len(value) <= 20 for value in BLSampleStatus), ( + "Column size limit of 20 for BLSampleStatus" +) class ExpeyeInteraction: diff --git a/src/mx_bluesky/hyperion/external_interaction/ispyb/ispyb_store.py b/src/mx_bluesky/common/external_interaction/ispyb/ispyb_store.py similarity index 90% rename from src/mx_bluesky/hyperion/external_interaction/ispyb/ispyb_store.py rename to src/mx_bluesky/common/external_interaction/ispyb/ispyb_store.py index ad953cbea..d01ff3177 100755 --- a/src/mx_bluesky/hyperion/external_interaction/ispyb/ispyb_store.py +++ b/src/mx_bluesky/common/external_interaction/ispyb/ispyb_store.py @@ -11,18 +11,18 @@ from ispyb.strictordereddict import StrictOrderedDict from pydantic import BaseModel -from mx_bluesky.common.utils.tracing import TRACER -from mx_bluesky.hyperion.external_interaction.ispyb.data_model import ( +from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionGridInfo, DataCollectionGroupInfo, DataCollectionInfo, ScanDataInfo, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_utils import ( +from mx_bluesky.common.external_interaction.ispyb.ispyb_utils import ( get_current_time_string, get_session_id_from_visit, ) -from mx_bluesky.hyperion.log import ISPYB_LOGGER +from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER +from mx_bluesky.common.utils.tracing import TRACER if TYPE_CHECKING: pass @@ -62,12 +62,12 @@ def update_deposition( ispyb_ids, scan_data_infos: Sequence[ScanDataInfo], ) -> IspybIds: - assert ( - ispyb_ids.data_collection_group_id - ), "Attempted to store scan data without a collection group" - assert ( - ispyb_ids.data_collection_ids - ), "Attempted to store scan data without a collection" + assert ispyb_ids.data_collection_group_id, ( + "Attempted to store scan data without a collection group" + ) + assert ispyb_ids.data_collection_ids, ( + "Attempted to store scan data without a collection" + ) return self._begin_or_update_deposition(ispyb_ids, None, scan_data_infos) def _begin_or_update_deposition( @@ -87,7 +87,9 @@ def _begin_or_update_deposition( ) ) else: - assert ispyb_ids.data_collection_group_id, "Attempt to update data collection without a data collection group ID" + assert ispyb_ids.data_collection_group_id, ( + "Attempt to update data collection without a data collection group ID" + ) grid_ids = list(ispyb_ids.grid_ids) data_collection_ids_out = list(ispyb_ids.data_collection_ids) @@ -116,15 +118,15 @@ def _begin_or_update_deposition( return ispyb_ids def end_deposition(self, ispyb_ids: IspybIds, success: str, reason: str): - assert ( - ispyb_ids.data_collection_ids - ), "Can't end ISPyB deposition, data_collection IDs are missing" - assert ( - ispyb_ids.data_collection_group_id is not None - ), "Cannot end ISPyB deposition without data collection group ID" + assert ispyb_ids.data_collection_ids, ( + "Can't end ISPyB deposition, data_collection IDs are missing" + ) + assert ispyb_ids.data_collection_group_id is not None, ( + "Cannot end ISPyB deposition without data collection group ID" + ) for id_ in ispyb_ids.data_collection_ids: - ISPYB_LOGGER.info( + ISPYB_ZOCALO_CALLBACK_LOGGER.info( f"End ispyb deposition with status '{success}' and reason '{reason}'." ) if success == "fail" or success == "abort": diff --git a/src/mx_bluesky/hyperion/external_interaction/ispyb/ispyb_utils.py b/src/mx_bluesky/common/external_interaction/ispyb/ispyb_utils.py similarity index 81% rename from src/mx_bluesky/hyperion/external_interaction/ispyb/ispyb_utils.py rename to src/mx_bluesky/common/external_interaction/ispyb/ispyb_utils.py index b523ffcda..d27bfd301 100644 --- a/src/mx_bluesky/hyperion/external_interaction/ispyb/ispyb_utils.py +++ b/src/mx_bluesky/common/external_interaction/ispyb/ispyb_utils.py @@ -7,11 +7,11 @@ from ispyb.connector.mysqlsp.main import ISPyBMySQLSPConnector as Connector from ispyb.sp.core import Core -from mx_bluesky.hyperion.parameters.constants import CONST +from mx_bluesky.common.parameters.constants import SimConstants def get_ispyb_config(): - return os.environ.get("ISPYB_CONFIG_PATH", CONST.SIM.ISPYB_CONFIG) + return os.environ.get("ISPYB_CONFIG_PATH", SimConstants.ISPYB_CONFIG) def get_session_id_from_visit(conn: Connector, visit: str): diff --git a/tests/unit_tests/hyperion/external_interaction/ispyb/__init__.py b/src/mx_bluesky/common/external_interaction/nexus/__init__.py similarity index 100% rename from tests/unit_tests/hyperion/external_interaction/ispyb/__init__.py rename to src/mx_bluesky/common/external_interaction/nexus/__init__.py diff --git a/src/mx_bluesky/hyperion/external_interaction/nexus/nexus_utils.py b/src/mx_bluesky/common/external_interaction/nexus/nexus_utils.py similarity index 86% rename from src/mx_bluesky/hyperion/external_interaction/nexus/nexus_utils.py rename to src/mx_bluesky/common/external_interaction/nexus/nexus_utils.py index 73ba10085..646d3da57 100644 --- a/src/mx_bluesky/hyperion/external_interaction/nexus/nexus_utils.py +++ b/src/mx_bluesky/common/external_interaction/nexus/nexus_utils.py @@ -2,16 +2,27 @@ import time from datetime import UTC, datetime, timedelta +from enum import Enum import numpy as np from dodal.devices.detector import DetectorParams -from dodal.devices.zebra import RotationDirection from nexgen.nxs_utils import Attenuator, Axis, Beam, Detector, EigerDetector, Goniometer from nexgen.nxs_utils.axes import TransformationType from numpy.typing import DTypeLike -from mx_bluesky.hyperion.log import NEXUS_LOGGER -from mx_bluesky.hyperion.utils.utils import convert_eV_to_angstrom +from mx_bluesky.common.utils.log import NEXUS_LOGGER +from mx_bluesky.common.utils.utils import convert_eV_to_angstrom + + +class AxisDirection(Enum): + """ + Identifies whether the omega axis of rotation is on the positive x-axis or + negative x-axis as per the Nexus standard: + https://journals.iucr.org/m/issues/2020/05/00/ti5018/ti5018.pdf + """ + + POSITIVE = 1 + NEGATIVE = -1 def vds_type_based_on_bit_depth(detector_bit_depth: int) -> DTypeLike: @@ -35,8 +46,8 @@ def create_goniometer_axes( x_y_z_increments: tuple[float, float, float] = (0.0, 0.0, 0.0), chi: float = 0.0, phi: float = 0.0, - rotation_direction: RotationDirection = RotationDirection.NEGATIVE, -): + omega_axis_direction: AxisDirection = AxisDirection.NEGATIVE, +) -> Goniometer: """Returns a Nexgen 'Goniometer' object with the dependency chain of I03's Smargon goniometer. If scan points is provided these values will be used in preference to those from the params object. @@ -50,13 +61,17 @@ def create_goniometer_axes( x_y_z_increments: optionally, specify the increments between each image for the x, y, and z axes. Will be ignored if scan_points is provided. + omega_axis_direction: The direction of the omega axis, this determines the + coordinate space parity + Returns: + The created Goniometer object """ gonio_axes = [ Axis( "omega", ".", TransformationType.ROTATION, - (1.0 * rotation_direction.multiplier, 0.0, 0.0), + (1.0 * omega_axis_direction.value, 0.0, 0.0), omega_start, ), Axis( diff --git a/src/mx_bluesky/hyperion/external_interaction/nexus/write_nexus.py b/src/mx_bluesky/common/external_interaction/nexus/write_nexus.py similarity index 93% rename from src/mx_bluesky/hyperion/external_interaction/nexus/write_nexus.py rename to src/mx_bluesky/common/external_interaction/nexus/write_nexus.py index 2bb606b0f..4ebdc5334 100644 --- a/src/mx_bluesky/hyperion/external_interaction/nexus/write_nexus.py +++ b/src/mx_bluesky/common/external_interaction/nexus/write_nexus.py @@ -8,19 +8,19 @@ import math from pathlib import Path -from dodal.devices.zebra import RotationDirection from dodal.utils import get_beamline_name from nexgen.nxs_utils import Attenuator, Beam, Detector, Goniometer, Source from nexgen.nxs_write.nxmx_writer import NXmxFileWriter from numpy.typing import DTypeLike from scanspec.core import AxesPoints -from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample -from mx_bluesky.hyperion.external_interaction.nexus.nexus_utils import ( +from mx_bluesky.common.external_interaction.nexus.nexus_utils import ( + AxisDirection, create_detector_parameters, create_goniometer_axes, get_start_and_predicted_end_time, ) +from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample class NexusWriter: @@ -39,7 +39,7 @@ def __init__( # detector arming event: full_num_of_images: int | None = None, meta_data_run_number: int | None = None, - rotation_direction: RotationDirection = RotationDirection.NEGATIVE, + axis_direction: AxisDirection = AxisDirection.NEGATIVE, ) -> None: self.beam: Beam | None = None self.attenuator: Attenuator | None = None @@ -69,7 +69,7 @@ def __init__( self.scan_points, chi=chi_start_deg, phi=phi_start_deg, - rotation_direction=rotation_direction, + omega_axis_direction=axis_direction, ) def create_nexus_file(self, bit_depth: DTypeLike): diff --git a/src/mx_bluesky/common/parameters/components.py b/src/mx_bluesky/common/parameters/components.py index fe7359f7d..d239cf494 100644 --- a/src/mx_bluesky/common/parameters/components.py +++ b/src/mx_bluesky/common/parameters/components.py @@ -105,12 +105,12 @@ def __hash__(self) -> int: @field_validator("parameter_model_version") @classmethod def _validate_version(cls, version: SemanticVersion): - assert ( - version >= SemanticVersion(major=PARAMETER_VERSION.major) - ), f"Parameter version too old! This version of hyperion uses {PARAMETER_VERSION}" - assert ( - version <= SemanticVersion(major=PARAMETER_VERSION.major + 1) - ), f"Parameter version too new! This version of hyperion uses {PARAMETER_VERSION}" + assert version >= SemanticVersion(major=PARAMETER_VERSION.major), ( + f"Parameter version too old! This version of hyperion uses {PARAMETER_VERSION}" + ) + assert version <= SemanticVersion(major=PARAMETER_VERSION.major + 1), ( + f"Parameter version too new! This version of hyperion uses {PARAMETER_VERSION}" + ) return version diff --git a/src/mx_bluesky/common/parameters/constants.py b/src/mx_bluesky/common/parameters/constants.py index 4ec504e8b..3adc0a9f9 100644 --- a/src/mx_bluesky/common/parameters/constants.py +++ b/src/mx_bluesky/common/parameters/constants.py @@ -73,6 +73,7 @@ class HardwareConstants: PANDA_FGS_RUN_UP_DEFAULT = 0.17 CRYOJET_MARGIN_MM = 0.2 THAWING_TIME = 20 + TIP_OFFSET_UM = 0 @dataclass(frozen=True) diff --git a/src/mx_bluesky/common/parameters/gridscan.py b/src/mx_bluesky/common/parameters/gridscan.py index 2fc50caa5..7e791d3a7 100644 --- a/src/mx_bluesky/common/parameters/gridscan.py +++ b/src/mx_bluesky/common/parameters/gridscan.py @@ -6,12 +6,19 @@ from dodal.devices.detector import ( DetectorParams, ) -from pydantic import Field +from dodal.devices.fast_grid_scan import ( + ZebraGridScanParams, +) +from pydantic import Field, PrivateAttr +from scanspec.core import Path as ScanPath +from scanspec.specs import Line, Static from mx_bluesky.common.parameters.components import ( DiffractionExperimentWithSample, IspybExperimentType, OptionalGonioAngleStarts, + SplitScan, + WithOptionalEnergyChange, WithScan, XyzStarts, ) @@ -45,9 +52,9 @@ def detector_params(self): optional_args = {} if self.run_number: optional_args["run_number"] = self.run_number - assert ( - self.detector_distance_mm is not None - ), "Detector distance must be filled before generating DetectorParams" + assert self.detector_distance_mm is not None, ( + "Detector distance must be filled before generating DetectorParams" + ) os.makedirs(self.storage_directory, exist_ok=True) return DetectorParams( detector_size_constants=DetectorParamConstants.DETECTOR, @@ -69,6 +76,7 @@ def detector_params(self): class RobotLoadThenCentre(GridCommon): thawing_time: float = Field(default=HardwareConstants.THAWING_TIME) + tip_offset_um: float = Field(default=HardwareConstants.TIP_OFFSET_UM) def robot_load_params(self): my_params = self.model_dump() @@ -92,3 +100,98 @@ class SpecifiedGrid(XyzStarts, WithScan): """A specified grid is one which has defined values for the start position, grid and box sizes, etc., as opposed to parameters for a plan which will create those parameters at some point (e.g. through optical pin detection).""" + + +class ThreeDGridScan( + GridCommon, + SpecifiedGrid, + SplitScan, + WithOptionalEnergyChange, +): + """Parameters representing a so-called 3D grid scan, which consists of doing a + gridscan in X and Y, followed by one in X and Z.""" + + grid1_omega_deg: float = Field(default=GridscanParamConstants.OMEGA_1) # type: ignore + grid2_omega_deg: float = Field(default=GridscanParamConstants.OMEGA_2) + x_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM) + y_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM) + z_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM) + y2_start_um: float + z2_start_um: float + x_steps: int = Field(gt=0) + y_steps: int = Field(gt=0) + z_steps: int = Field(gt=0) + _set_stub_offsets: bool = PrivateAttr(default_factory=lambda: False) + + @property + def FGS_params(self) -> ZebraGridScanParams: + return ZebraGridScanParams( + x_steps=self.x_steps, + y_steps=self.y_steps, + z_steps=self.z_steps, + x_step_size_mm=self.x_step_size_um / 1000, + y_step_size_mm=self.y_step_size_um / 1000, + z_step_size_mm=self.z_step_size_um / 1000, + x_start_mm=self.x_start_um / 1000, + y1_start_mm=self.y_start_um / 1000, + z1_start_mm=self.z_start_um / 1000, + y2_start_mm=self.y2_start_um / 1000, + z2_start_mm=self.z2_start_um / 1000, + set_stub_offsets=self._set_stub_offsets, + dwell_time_ms=self.exposure_time_s * 1000, + transmission_fraction=self.transmission_frac, + ) + + def do_set_stub_offsets(self, value: bool): + self._set_stub_offsets = value + + @property + def grid_1_spec(self): + x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1) + y1_end = self.y_start_um + self.y_step_size_um * (self.y_steps - 1) + grid_1_x = Line("sam_x", self.x_start_um, x_end, self.x_steps) + grid_1_y = Line("sam_y", self.y_start_um, y1_end, self.y_steps) + grid_1_z = Static("sam_z", self.z_start_um) + return grid_1_y.zip(grid_1_z) * ~grid_1_x + + @property + def grid_2_spec(self): + x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1) + z2_end = self.z2_start_um + self.z_step_size_um * (self.z_steps - 1) + grid_2_x = Line("sam_x", self.x_start_um, x_end, self.x_steps) + grid_2_z = Line("sam_z", self.z2_start_um, z2_end, self.z_steps) + grid_2_y = Static("sam_y", self.y2_start_um) + return grid_2_z.zip(grid_2_y) * ~grid_2_x + + @property + def scan_indices(self): + """The first index of each gridscan, useful for writing nexus files/VDS""" + return [ + 0, + len(ScanPath(self.grid_1_spec.calculate()).consume().midpoints["sam_x"]), + ] + + @property + def scan_spec(self): + """A fully specified ScanSpec object representing both grids, with x, y, z and + omega positions.""" + return self.grid_1_spec.concat(self.grid_2_spec) + + @property + def scan_points(self): + """A list of all the points in the scan_spec.""" + return ScanPath(self.scan_spec.calculate()).consume().midpoints + + @property + def scan_points_first_grid(self): + """A list of all the points in the first grid scan.""" + return ScanPath(self.grid_1_spec.calculate()).consume().midpoints + + @property + def scan_points_second_grid(self): + """A list of all the points in the second grid scan.""" + return ScanPath(self.grid_2_spec.calculate()).consume().midpoints + + @property + def num_images(self) -> int: + return len(self.scan_points["sam_x"]) diff --git a/src/mx_bluesky/common/plans/do_fgs.py b/src/mx_bluesky/common/plans/do_fgs.py index 8c6cd3faa..e5f20765a 100644 --- a/src/mx_bluesky/common/plans/do_fgs.py +++ b/src/mx_bluesky/common/plans/do_fgs.py @@ -59,7 +59,7 @@ def _wait_for_zocalo_to_stage_then_do_fgs( yield from bps.complete(grid_scan_device, wait=True) # Remove this logging statement once metrics have been added LOGGER.info( - f"Grid scan motion program took {round(time()-gridscan_start_time,2)} to complete" + f"Grid scan motion program took {round(time() - gridscan_start_time, 2)} to complete" ) @@ -89,9 +89,9 @@ def kickoff_and_complete_gridscan( zocalo_environment (Optional, str) Used for zocalo connection """ - assert len(scan_points) == len( - scan_start_indices - ), "scan_points and scan_start_indices must be lists of the same length!" + assert len(scan_points) == len(scan_start_indices), ( + "scan_points and scan_start_indices must be lists of the same length!" + ) plan_name = PlanNameConstants.DO_FGS diff --git a/src/mx_bluesky/hyperion/exceptions.py b/src/mx_bluesky/common/utils/exceptions.py similarity index 76% rename from src/mx_bluesky/hyperion/exceptions.py rename to src/mx_bluesky/common/utils/exceptions.py index 9a0c9e380..c242df11f 100644 --- a/src/mx_bluesky/hyperion/exceptions.py +++ b/src/mx_bluesky/common/utils/exceptions.py @@ -1,3 +1,4 @@ +import re from collections.abc import Callable, Generator from typing import TypeVar @@ -13,10 +14,23 @@ class WarningException(Exception): pass +class ISPyBDepositionNotMade(Exception): + """Raised when the ISPyB or Zocalo callbacks can't access ISPyB deposition numbers.""" + + pass + + class SampleException(WarningException): """An exception which identifies an issue relating to the sample.""" - pass + def __str__(self): + class_name = type(self).__name__ + return f"[{class_name}]: {super().__str__()}" + + @classmethod + def type_and_message_from_reason(cls, reason: str) -> tuple[str, str]: + match = re.match(r"\[(\S*)?]: (.*)", reason) + return (match.group(1), match.group(2)) if match else (None, None) T = TypeVar("T") diff --git a/src/mx_bluesky/common/utils/log.py b/src/mx_bluesky/common/utils/log.py index 6a8731953..903507a8f 100644 --- a/src/mx_bluesky/common/utils/log.py +++ b/src/mx_bluesky/common/utils/log.py @@ -12,7 +12,17 @@ ) from dodal.log import LOGGER as dodal_logger -LOGGER = logging.getLogger("mx-bluesky") +LOGGER = logging.getLogger("MX-Bluesky") +LOGGER.setLevel("DEBUG") +LOGGER.parent = dodal_logger + +ISPYB_ZOCALO_CALLBACK_LOGGER = logging.getLogger("ISPyB and Zocalo callbacks") +ISPYB_ZOCALO_CALLBACK_LOGGER.setLevel(logging.DEBUG) + +NEXUS_LOGGER = logging.getLogger("NeXus callbacks") +NEXUS_LOGGER.setLevel(logging.DEBUG) + +ALL_LOGGERS = [LOGGER, ISPYB_ZOCALO_CALLBACK_LOGGER, NEXUS_LOGGER] __logger_handlers: DodalLogHandlers | None = None @@ -76,18 +86,18 @@ def do_default_logging_setup( def _get_debug_handler() -> CircularMemoryHandler: - assert ( - __logger_handlers is not None - ), "You can only use this after running the default logging setup" + assert __logger_handlers is not None, ( + "You can only use this after running the default logging setup" + ) return __logger_handlers["debug_memory_handler"] def flush_debug_handler() -> str: """Writes the contents of the circular debug log buffer to disk and returns the written filename""" handler = _get_debug_handler() - assert isinstance( - handler.target, TimedRotatingFileHandler - ), "Circular memory handler doesn't have an appropriate fileHandler target" + assert isinstance(handler.target, TimedRotatingFileHandler), ( + "Circular memory handler doesn't have an appropriate fileHandler target" + ) handler.flush() return handler.target.baseFilename diff --git a/src/mx_bluesky/hyperion/utils/utils.py b/src/mx_bluesky/common/utils/utils.py similarity index 100% rename from src/mx_bluesky/hyperion/utils/utils.py rename to src/mx_bluesky/common/utils/utils.py diff --git a/src/mx_bluesky/hyperion/__main__.py b/src/mx_bluesky/hyperion/__main__.py index 0c346dc7a..3b58a6d99 100755 --- a/src/mx_bluesky/hyperion/__main__.py +++ b/src/mx_bluesky/hyperion/__main__.py @@ -15,11 +15,24 @@ from flask_restful import Api, Resource from pydantic.dataclasses import dataclass +from mx_bluesky.common.external_interaction.callbacks.common.aperture_change_callback import ( + ApertureChangeCallback, +) +from mx_bluesky.common.external_interaction.callbacks.common.log_uid_tag_callback import ( + LogUidTaggingCallback, +) +from mx_bluesky.common.external_interaction.callbacks.common.logging_callback import ( + VerbosePlanExecutionLoggingCallback, +) from mx_bluesky.common.parameters.components import MxBlueskyParameters from mx_bluesky.common.parameters.constants import Actions, Status -from mx_bluesky.common.utils.log import do_default_logging_setup, flush_debug_handler +from mx_bluesky.common.utils.exceptions import WarningException +from mx_bluesky.common.utils.log import ( + LOGGER, + do_default_logging_setup, + flush_debug_handler, +) from mx_bluesky.common.utils.tracing import TRACER -from mx_bluesky.hyperion.exceptions import WarningException from mx_bluesky.hyperion.experiment_plans.experiment_registry import ( PLAN_REGISTRY, PlanNotFound, @@ -27,21 +40,9 @@ from mx_bluesky.hyperion.external_interaction.callbacks.__main__ import ( setup_logging as setup_callback_logging, ) -from mx_bluesky.hyperion.external_interaction.callbacks.aperture_change_callback import ( - ApertureChangeCallback, -) from mx_bluesky.hyperion.external_interaction.callbacks.common.callback_util import ( CallbacksFactory, ) -from mx_bluesky.hyperion.external_interaction.callbacks.log_uid_tag_callback import ( - LogUidTaggingCallback, -) -from mx_bluesky.hyperion.external_interaction.callbacks.logging_callback import ( - VerbosePlanExecutionLoggingCallback, -) -from mx_bluesky.hyperion.log import ( - LOGGER, -) from mx_bluesky.hyperion.parameters.cli import parse_cli_args from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.utils.context import setup_context diff --git a/src/mx_bluesky/hyperion/device_setup_plans/check_beamstop.py b/src/mx_bluesky/hyperion/device_setup_plans/check_beamstop.py new file mode 100644 index 000000000..8c9611311 --- /dev/null +++ b/src/mx_bluesky/hyperion/device_setup_plans/check_beamstop.py @@ -0,0 +1,27 @@ +import bluesky.plan_stubs as bps +from dodal.devices.i03.beamstop import Beamstop, BeamstopPositions + +from mx_bluesky.common.utils.log import LOGGER + + +class BeamstopException(Exception): + pass + + +def check_beamstop(beamstop: Beamstop): + """ + Check the current position of the beamstop to ensure it is in position for data collection + Args: + beamstop: The beamstop device + + Raises: + BeamstopException: If the beamstop is in any other position than DATA_COLLECTION + """ + current_pos = yield from bps.rd(beamstop.selected_pos) + if current_pos != BeamstopPositions.DATA_COLLECTION: + LOGGER.info(f"Beamstop check failed: position {current_pos}") + raise BeamstopException( + f"Beamstop is not DATA_COLLECTION, current state is {current_pos}" + ) + + LOGGER.info("Beamstop check ok") diff --git a/src/mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py b/src/mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py index 9396033e8..8e10176d2 100644 --- a/src/mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +++ b/src/mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py @@ -10,10 +10,11 @@ from dodal.devices.util.adjuster_plans import lookup_table_adjuster from dodal.devices.util.lookup_tables import ( linear_interpolation_lut, + parse_lookup_table, ) -from mx_bluesky.hyperion.log import LOGGER -from mx_bluesky.hyperion.utils.utils import ( +from mx_bluesky.common.utils.log import LOGGER +from mx_bluesky.common.utils.utils import ( energy_to_bragg_angle, ) @@ -106,7 +107,9 @@ def adjust_dcm_pitch_roll_vfm_from_lut( bragg_deg = energy_to_bragg_angle(energy_kev, d_spacing_a) LOGGER.info(f"Target Bragg angle = {bragg_deg} degrees") dcm_pitch_adjuster = lookup_table_adjuster( - linear_interpolation_lut(undulator_dcm.pitch_energy_table_path), + linear_interpolation_lut( + *parse_lookup_table(undulator_dcm.pitch_energy_table_path) + ), dcm.pitch_in_mrad, bragg_deg, ) @@ -116,7 +119,9 @@ def adjust_dcm_pitch_roll_vfm_from_lut( # DCM Roll dcm_roll_adjuster = lookup_table_adjuster( - linear_interpolation_lut(undulator_dcm.roll_energy_table_path), + linear_interpolation_lut( + *parse_lookup_table(undulator_dcm.roll_energy_table_path) + ), dcm.roll_in_mrad, bragg_deg, ) diff --git a/src/mx_bluesky/hyperion/device_setup_plans/manipulate_sample.py b/src/mx_bluesky/hyperion/device_setup_plans/manipulate_sample.py index 858ceadfd..1744e412d 100644 --- a/src/mx_bluesky/hyperion/device_setup_plans/manipulate_sample.py +++ b/src/mx_bluesky/hyperion/device_setup_plans/manipulate_sample.py @@ -9,7 +9,7 @@ from dodal.devices.detector.detector_motion import DetectorMotion from dodal.devices.smargon import Smargon -from mx_bluesky.hyperion.log import LOGGER +from mx_bluesky.common.utils.log import LOGGER LOWER_DETECTOR_SHUTTER_AFTER_SCAN = True diff --git a/src/mx_bluesky/hyperion/device_setup_plans/position_detector.py b/src/mx_bluesky/hyperion/device_setup_plans/position_detector.py index a21bbfd62..3c84a870d 100644 --- a/src/mx_bluesky/hyperion/device_setup_plans/position_detector.py +++ b/src/mx_bluesky/hyperion/device_setup_plans/position_detector.py @@ -1,7 +1,7 @@ from bluesky import plan_stubs as bps from dodal.devices.detector.detector_motion import DetectorMotion, ShutterState -from mx_bluesky.hyperion.log import LOGGER +from mx_bluesky.common.utils.log import LOGGER def set_detector_z_position( diff --git a/src/mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py b/src/mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py index 2552d594a..10ddabcfb 100644 --- a/src/mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py +++ b/src/mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py @@ -2,7 +2,7 @@ import bluesky.plan_stubs as bps from dodal.devices.aperturescatterguard import ApertureScatterguard -from dodal.devices.attenuator import Attenuator +from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator from dodal.devices.dcm import DCM from dodal.devices.eiger import EigerDetector from dodal.devices.flux import Flux @@ -11,7 +11,7 @@ from dodal.devices.synchrotron import Synchrotron from dodal.devices.undulator import Undulator -from mx_bluesky.hyperion.log import LOGGER +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.parameters.constants import CONST @@ -39,7 +39,7 @@ def read_hardware_pre_collection( def read_hardware_during_collection( aperture_scatterguard: ApertureScatterguard, - attenuator: Attenuator, + attenuator: BinaryFilterAttenuator, flux: Flux, dcm: DCM, detector: EigerDetector, diff --git a/src/mx_bluesky/hyperion/device_setup_plans/setup_panda.py b/src/mx_bluesky/hyperion/device_setup_plans/setup_panda.py index 55aa170d7..32dc77ee3 100644 --- a/src/mx_bluesky/hyperion/device_setup_plans/setup_panda.py +++ b/src/mx_bluesky/hyperion/device_setup_plans/setup_panda.py @@ -7,6 +7,7 @@ from bluesky.utils import MsgGenerator from dodal.common.beamlines.beamline_utils import get_path_provider from dodal.devices.fast_grid_scan import PandAGridScanParams +from dodal.devices.smargon import Smargon from ophyd_async.core import load_device from ophyd_async.fastcs.panda import ( HDFPanda, @@ -15,7 +16,7 @@ ) import mx_bluesky.hyperion.resources.panda as panda_resource -from mx_bluesky.hyperion.log import LOGGER +from mx_bluesky.common.utils.log import LOGGER MM_TO_ENCODER_COUNTS = 200000 GENERAL_TIMEOUT = 60 @@ -114,7 +115,7 @@ def _get_seq_table( def setup_panda_for_flyscan( panda: HDFPanda, parameters: PandAGridScanParams, - initial_x: float, + smargon: Smargon, exposure_time_s: float, time_between_x_steps_ms: float, sample_velocity_mm_per_s: float, @@ -127,7 +128,7 @@ def setup_panda_for_flyscan( Args: panda (HDFPanda): The PandA Ophyd device parameters (PandAGridScanParams): Grid parameters - initial_x (float): Motor positions at time of PandA setup + smargon (Smargon): The Smargon Ophyd device exposure_time_s (float): Detector exposure time per trigger time_between_x_steps_ms (float): Time, in ms, between each trigger. Equal to deadtime + exposure time sample_velocity_mm_per_s (float): Velocity of the sample in mm/s = x_step_size_mm * 1000 / @@ -149,13 +150,29 @@ def setup_panda_for_flyscan( ) as config_yaml_path: yield from load_device(panda, str(config_yaml_path)) - # Home the PandA X encoder using current motor position + initial_x = yield from bps.rd(smargon.x.user_readback) + initial_y = yield from bps.rd(smargon.y.user_readback) + initial_z = yield from bps.rd(smargon.z.user_readback) + + # Home the PandA X, Y, and Z encoders using current motor position yield from bps.abs_set( panda.inenc[1].setp, # type: ignore initial_x * MM_TO_ENCODER_COUNTS, wait=True, ) + yield from bps.abs_set( + panda.inenc[2].setp, # type: ignore + initial_y * MM_TO_ENCODER_COUNTS, + wait=True, + ) + + yield from bps.abs_set( + panda.inenc[3].setp, # type: ignore + initial_z * MM_TO_ENCODER_COUNTS, + wait=True, + ) + yield from bps.abs_set(panda.pulse[1].width, exposure_time_s, group="panda-config") exposure_distance_mm = sample_velocity_mm_per_s * exposure_time_s diff --git a/src/mx_bluesky/hyperion/device_setup_plans/setup_zebra.py b/src/mx_bluesky/hyperion/device_setup_plans/setup_zebra.py index eb0912db5..9576bbe64 100644 --- a/src/mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +++ b/src/mx_bluesky/hyperion/device_setup_plans/setup_zebra.py @@ -26,7 +26,7 @@ ) from dodal.devices.zebra_controlled_shutter import ZebraShutter, ZebraShutterControl -from mx_bluesky.hyperion.log import LOGGER +from mx_bluesky.common.utils.log import LOGGER ZEBRA_STATUS_TIMEOUT = 30 diff --git a/src/mx_bluesky/hyperion/device_setup_plans/smargon.py b/src/mx_bluesky/hyperion/device_setup_plans/smargon.py index c18c5b198..3da4e706c 100644 --- a/src/mx_bluesky/hyperion/device_setup_plans/smargon.py +++ b/src/mx_bluesky/hyperion/device_setup_plans/smargon.py @@ -2,7 +2,7 @@ from bluesky import plan_stubs as bps from dodal.devices.smargon import Smargon -from mx_bluesky.hyperion.exceptions import SampleException +from mx_bluesky.common.utils.exceptions import SampleException def move_smargon_warn_on_out_of_range( diff --git a/src/mx_bluesky/hyperion/device_setup_plans/utils.py b/src/mx_bluesky/hyperion/device_setup_plans/utils.py index d469a9cdf..ea6d9ffdf 100644 --- a/src/mx_bluesky/hyperion/device_setup_plans/utils.py +++ b/src/mx_bluesky/hyperion/device_setup_plans/utils.py @@ -9,7 +9,9 @@ ) from dodal.devices.detector.detector_motion import DetectorMotion, ShutterState from dodal.devices.eiger import EigerDetector +from dodal.devices.i03.beamstop import Beamstop +from mx_bluesky.hyperion.device_setup_plans.check_beamstop import check_beamstop from mx_bluesky.hyperion.device_setup_plans.position_detector import ( set_detector_z_position, set_shutter, @@ -24,6 +26,7 @@ def fill_in_energy_if_not_supplied(dcm: DCM, detector_params: DetectorParams): def start_preparing_data_collection_then_do_plan( + beamstop: Beamstop, eiger: EigerDetector, detector_motion: DetectorMotion, detector_distance_mm: float | None, @@ -49,6 +52,7 @@ def wrapped_plan(): yield from set_shutter(detector_motion, ShutterState.OPEN, group) yield from plan_to_run + yield from check_beamstop(beamstop) yield from bpp.contingency_wrapper( wrapped_plan(), except_plan=lambda e: (yield from bps.stop(eiger)), # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809 diff --git a/src/mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py b/src/mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py index e00c6637c..7ccdafcb4 100644 --- a/src/mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +++ b/src/mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py @@ -1,15 +1,15 @@ from bluesky import plan_stubs as bps from bluesky.preprocessors import finalize_wrapper from bluesky.utils import make_decorator -from dodal.devices.attenuator import Attenuator +from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator from dodal.devices.xbpm_feedback import Pause, XBPMFeedback -from mx_bluesky.hyperion.log import LOGGER +from mx_bluesky.common.utils.log import LOGGER def _check_and_pause_feedback( xbpm_feedback: XBPMFeedback, - attenuator: Attenuator, + attenuator: BinaryFilterAttenuator, desired_transmission_fraction: float, ): """Checks that the xbpm is in position before then turning it off and setting a new @@ -18,7 +18,7 @@ def _check_and_pause_feedback( Args: xbpm_feedback (XBPMFeedback): The XBPM device that is responsible for keeping the beam in position - attenuator (Attenuator): The attenuator used to set transmission + attenuator (BinaryFilterAttenuator): The attenuator used to set transmission desired_transmission_fraction (float): The desired transmission to set after turning XBPM feedback off. @@ -34,7 +34,7 @@ def _check_and_pause_feedback( def _unpause_xbpm_feedback_and_set_transmission_to_1( - xbpm_feedback: XBPMFeedback, attenuator: Attenuator + xbpm_feedback: XBPMFeedback, attenuator: BinaryFilterAttenuator ): """Turns the XBPM feedback back on and sets transmission to 1 so that it keeps the beam aligned whilst not collecting. @@ -42,7 +42,7 @@ def _unpause_xbpm_feedback_and_set_transmission_to_1( Args: xbpm_feedback (XBPMFeedback): The XBPM device that is responsible for keeping the beam in position - attenuator (Attenuator): The attenuator used to set transmission + attenuator (BinaryFilterAttenuator): The attenuator used to set transmission """ yield from bps.mv(xbpm_feedback.pause_feedback, Pause.RUN, attenuator, 1.0) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809 @@ -50,7 +50,7 @@ def _unpause_xbpm_feedback_and_set_transmission_to_1( def transmission_and_xbpm_feedback_for_collection_wrapper( plan, xbpm_feedback: XBPMFeedback, - attenuator: Attenuator, + attenuator: BinaryFilterAttenuator, desired_transmission_fraction: float, ): """Sets the transmission for the data collection, ensuring the xbpm feedback is valid @@ -70,7 +70,7 @@ def transmission_and_xbpm_feedback_for_collection_wrapper( plan: The plan performing the data collection xbpm_feedback (XBPMFeedback): The XBPM device that is responsible for keeping the beam in position - attenuator (Attenuator): The attenuator used to set transmission + attenuator (BinaryFilterAttenuator): The attenuator used to set transmission desired_transmission_fraction (float): The desired transmission for the collection """ diff --git a/src/mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py b/src/mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py index 20df2803b..930f4428b 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py @@ -4,10 +4,10 @@ from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue from dodal.devices.smargon import Smargon, StubPosition +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.common.utils.tracing import TRACER from mx_bluesky.hyperion.device_setup_plans.manipulate_sample import move_x_y_z from mx_bluesky.hyperion.experiment_plans.common.xrc_result import XRayCentreResult -from mx_bluesky.hyperion.log import LOGGER from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan @@ -26,8 +26,9 @@ def change_aperture_then_move_to_xtal( best_hit.bounding_box_mm[1] - best_hit.bounding_box_mm[0] ) with TRACER.start_span("change_aperture"): - yield from _set_aperture_for_bbox_mm( - aperture_scatterguard, bounding_box_size + yield from set_aperture_for_bbox_mm( + aperture_scatterguard, + bounding_box_size, ) else: LOGGER.warning("No bounding box size received") @@ -49,25 +50,35 @@ def change_aperture_then_move_to_xtal( ) -def _set_aperture_for_bbox_mm( - aperture_device: ApertureScatterguard, bbox_size_mm: list[float] | numpy.ndarray +def set_aperture_for_bbox_mm( + aperture_device: ApertureScatterguard, + bbox_size_mm: list[float] | numpy.ndarray, ): - # TODO confirm correction factor see https://github.com/DiamondLightSource/mx-bluesky/issues/618 - ASSUMED_BOX_SIZE_MM = 0.020 - bbox_size_boxes = [round(mm / ASSUMED_BOX_SIZE_MM) for mm in bbox_size_mm] - yield from set_aperture_for_bbox_size(aperture_device, bbox_size_boxes) + """Sets aperture size based on bbox_size. + This function determines the aperture size needed to accomodate the bounding box + of a crystal. The x-axis length of the bounding box is used, setting the aperture + to Medium if this is less than 50um, and Large otherwise. + + Args: + aperture_device: The aperture scatter gaurd device we are controlling. + bbox_size_mm: The [x,y,z] lengths, in mm, of a bounding box + containing a crystal. This describes (in no particular order): + * The maximum width a crystal occupies + * The maximum height a crystal occupies + * The maximum depth a crystal occupies + constructing a three dimensional cuboid, completely encapsulating the crystal. + + Yields: + Iterator[MsgGenerator] + """ -def set_aperture_for_bbox_size( - aperture_device: ApertureScatterguard, - bbox_size: list[int] | numpy.ndarray, -): # bbox_size is [x,y,z], for i03 we only care about x new_selected_aperture = ( - ApertureValue.MEDIUM if bbox_size[0] < 2 else ApertureValue.LARGE + ApertureValue.MEDIUM if bbox_size_mm[0] < 0.05 else ApertureValue.LARGE ) LOGGER.info( - f"Setting aperture to {new_selected_aperture} based on bounding box size {bbox_size}." + f"Setting aperture to {new_selected_aperture} based on bounding box size {bbox_size_mm}." ) @bpp.set_run_key_decorator("change_aperture") diff --git a/src/mx_bluesky/hyperion/experiment_plans/common/xrc_result.py b/src/mx_bluesky/hyperion/experiment_plans/common/xrc_result.py index 0cce363c2..6c6f64cfd 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/common/xrc_result.py +++ b/src/mx_bluesky/hyperion/experiment_plans/common/xrc_result.py @@ -14,7 +14,16 @@ @dataclasses.dataclass class XRayCentreResult: - """Represents information about a hit from an X-ray centring.""" + """ + Represents information about a hit from an X-ray centring. + + Attributes: + centre_of_mass_mm: coordinates in mm of the centre of mass + bounding_box_mm: coordinates in mm of opposite corners of the bounding box + containing the crystal + max_count: The maximum spot count encountered in any one grid box in the crystal + total_count: The total count across all boxes in the crystal. + """ centre_of_mass_mm: np.ndarray bounding_box_mm: tuple[np.ndarray, np.ndarray] diff --git a/src/mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py b/src/mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py index e38e35b30..cdd845906 100755 --- a/src/mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py @@ -16,7 +16,7 @@ from dodal.devices.aperturescatterguard import ( ApertureScatterguard, ) -from dodal.devices.attenuator import Attenuator +from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator from dodal.devices.backlight import Backlight from dodal.devices.dcm import DCM from dodal.devices.eiger import EigerDetector @@ -47,7 +47,15 @@ from event_model import RunStart from ophyd_async.fastcs.panda import HDFPanda +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( + ispyb_activation_wrapper, +) from mx_bluesky.common.plans.do_fgs import kickoff_and_complete_gridscan +from mx_bluesky.common.utils.exceptions import ( + CrystalNotFoundException, + SampleException, +) +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.common.utils.tracing import TRACER from mx_bluesky.hyperion.device_setup_plans.read_hardware_for_setup import ( read_hardware_during_collection, @@ -66,15 +74,10 @@ from mx_bluesky.hyperion.device_setup_plans.xbpm_feedback import ( transmission_and_xbpm_feedback_for_collection_decorator, ) -from mx_bluesky.hyperion.exceptions import CrystalNotFoundException, SampleException from mx_bluesky.hyperion.experiment_plans.change_aperture_then_move_plan import ( change_aperture_then_move_to_xtal, ) from mx_bluesky.hyperion.experiment_plans.common.xrc_result import XRayCentreResult -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( - ispyb_activation_wrapper, -) -from mx_bluesky.hyperion.log import LOGGER from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan from mx_bluesky.hyperion.utils.context import device_composite_from_context @@ -91,7 +94,7 @@ class FlyScanXRayCentreComposite: """All devices which are directly or indirectly required by this plan""" aperture_scatterguard: ApertureScatterguard - attenuator: Attenuator + attenuator: BinaryFilterAttenuator backlight: Backlight dcm: DCM eiger: EigerDetector @@ -109,11 +112,6 @@ class FlyScanXRayCentreComposite: robot: BartRobot sample_shutter: ZebraShutter - @property - def sample_motors(self) -> Smargon: - """Convenience alias with a more user-friendly name""" - return self.smargon - class XRayCentreEventHandler(CallbackBase): def __init__(self): @@ -149,7 +147,7 @@ def flyscan_xray_centre_no_move( @bpp.run_decorator( # attach experiment metadata to the start document md={ "subplan_name": CONST.PLAN.GRIDSCAN_OUTER, - "hyperion_parameters": parameters.model_dump_json(), + "mx_bluesky_parameters": parameters.model_dump_json(), "activate_callbacks": [ "GridscanNexusFileCallback", ], @@ -201,9 +199,9 @@ def flyscan_and_fetch_results() -> MsgGenerator: yield from flyscan_and_fetch_results() xray_centre_results = xrc_event_handler.xray_centre_results - assert ( - xray_centre_results - ), "Flyscan result event not received or no crystal found and exception not raised" + assert xray_centre_results, ( + "Flyscan result event not received or no crystal found and exception not raised" + ) yield from change_aperture_then_move_to_xtal( xray_centre_results[0], composite.smargon, @@ -222,16 +220,7 @@ def run_gridscan_and_fetch_results( """A multi-run plan which runs a gridscan, gets the results from zocalo and fires an event with the centres of mass determined by zocalo""" - # We get the initial motor positions so we can return to them on zocalo failure - initial_xyz = np.array( - [ - (yield from bps.rd(fgs_composite.sample_motors.x)), - (yield from bps.rd(fgs_composite.sample_motors.y)), - (yield from bps.rd(fgs_composite.sample_motors.z)), - ] - ) - - yield from feature_controlled.setup_trigger(fgs_composite, parameters, initial_xyz) + yield from feature_controlled.setup_trigger(fgs_composite, parameters) LOGGER.info("Starting grid scan") yield from bps.stage( @@ -286,14 +275,20 @@ def _xrc_result_in_boxes_to_result_in_mm( xray_centre = fgs_params.grid_position_to_motor_position( 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 + # to the xray-centre as it is already in continuous space and the conversion has + # been performed already + # In other words, xrc_result["bounding_box"] contains the position of the box centre, + # so we subtract half a box to get the corner of the box 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]) + np.array(xrc_result["bounding_box"][0]) - 0.5 ), fgs_params.grid_position_to_motor_position( - np.array(xrc_result["bounding_box"][1]) + np.array(xrc_result["bounding_box"][1]) - 0.5 ), ), max_count=xrc_result["max_count"], @@ -322,11 +317,9 @@ def run_gridscan( "plan_name": CONST.PLAN.GRIDSCAN_MAIN, }, ): - sample_motors = fgs_composite.sample_motors - # Currently gridscan only works for omega 0, see # with TRACER.start_span("moving_omega_to_0"): - yield from bps.abs_set(sample_motors.omega, 0) + yield from bps.abs_set(fgs_composite.smargon.omega, 0) # We only subscribe to the communicator callback for run_gridscan, so this is where # we should generate an event reading the values which need to be included in the @@ -399,7 +392,6 @@ def __call__( self, fgs_composite: FlyScanXRayCentreComposite, parameters: HyperionThreeDGridScan, - initial_xyz: np.ndarray, ) -> MsgGenerator: ... setup_trigger: _ExtraSetup @@ -460,7 +452,6 @@ def _panda_tidy(fgs_composite: FlyScanXRayCentreComposite): def _zebra_triggering_setup( fgs_composite: FlyScanXRayCentreComposite, parameters: HyperionThreeDGridScan, - initial_xyz: np.ndarray, ): yield from setup_zebra_for_gridscan( fgs_composite.zebra, fgs_composite.sample_shutter, wait=True @@ -470,7 +461,6 @@ def _zebra_triggering_setup( def _panda_triggering_setup( fgs_composite: FlyScanXRayCentreComposite, parameters: HyperionThreeDGridScan, - initial_xyz: np.ndarray, ): LOGGER.info("Setting up Panda for flyscan") @@ -515,7 +505,7 @@ def _panda_triggering_setup( yield from setup_panda_for_flyscan( fgs_composite.panda, parameters.panda_FGS_params, - initial_xyz[0], + fgs_composite.smargon, parameters.exposure_time_s, time_between_x_steps_ms, sample_velocity_mm_per_s, diff --git a/src/mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py b/src/mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py index dbdcd6b19..0077af952 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py @@ -9,13 +9,14 @@ from bluesky.preprocessors import subs_decorator from bluesky.utils import MsgGenerator from dodal.devices.aperturescatterguard import ApertureScatterguard -from dodal.devices.attenuator import Attenuator +from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator from dodal.devices.backlight import Backlight, BacklightPosition from dodal.devices.dcm import DCM from dodal.devices.detector.detector_motion import DetectorMotion from dodal.devices.eiger import EigerDetector from dodal.devices.fast_grid_scan import PandAFastGridScan, ZebraFastGridScan from dodal.devices.flux import Flux +from dodal.devices.i03.beamstop import Beamstop from dodal.devices.oav.oav_detector import OAV from dodal.devices.oav.oav_parameters import OAVParameters from dodal.devices.oav.pin_image_recognition import PinTipDetection @@ -30,8 +31,16 @@ from dodal.devices.zocalo import ZocaloResults from ophyd_async.fastcs.panda import HDFPanda +from mx_bluesky.common.external_interaction.callbacks.common.grid_detection_callback import ( + GridDetectionCallback, + GridParamUpdate, +) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( + ispyb_activation_wrapper, +) from mx_bluesky.common.parameters.constants import OavConstants from mx_bluesky.common.parameters.gridscan import GridScanWithEdgeDetect +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.device_setup_plans.manipulate_sample import ( move_aperture_if_required, ) @@ -52,14 +61,6 @@ OavGridDetectionComposite, grid_detection_plan, ) -from mx_bluesky.hyperion.external_interaction.callbacks.grid_detection_callback import ( - GridDetectionCallback, - GridParamUpdate, -) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( - ispyb_activation_wrapper, -) -from mx_bluesky.hyperion.log import LOGGER from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.gridscan import ( HyperionThreeDGridScan, @@ -72,8 +73,9 @@ class GridDetectThenXRayCentreComposite: """All devices which are directly or indirectly required by this plan""" aperture_scatterguard: ApertureScatterguard - attenuator: Attenuator + attenuator: BinaryFilterAttenuator backlight: Backlight + beamstop: Beamstop dcm: DCM detector_motion: DetectorMotion eiger: EigerDetector @@ -93,10 +95,6 @@ class GridDetectThenXRayCentreComposite: robot: BartRobot sample_shutter: ZebraShutter - @property - def sample_motors(self): - return self.smargon - def create_devices(context: BlueskyContext) -> GridDetectThenXRayCentreComposite: return device_composite_from_context(context, GridDetectThenXRayCentreComposite) @@ -217,6 +215,7 @@ def plan_to_perform(): ) yield from start_preparing_data_collection_then_do_plan( + composite.beamstop, eiger, composite.detector_motion, parameters.detector_params.detector_distance, @@ -224,9 +223,9 @@ def plan_to_perform(): group=CONST.WAIT.GRID_READY_FOR_DC, ) - assert ( - flyscan_event_handler.xray_centre_results - ), "Flyscan result event not received or no crystal found and exception not raised" + assert flyscan_event_handler.xray_centre_results, ( + "Flyscan result event not received or no crystal found and exception not raised" + ) yield from change_aperture_then_move_to_xtal( flyscan_event_handler.xray_centre_results[0], diff --git a/src/mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py b/src/mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py index 4633d6b7b..8154bdafc 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py @@ -7,9 +7,9 @@ from bluesky.preprocessors import run_decorator, set_run_key_decorator, subs_wrapper from bluesky.utils import MsgGenerator from dodal.devices.oav.oav_parameters import OAVParameters -from dodal.devices.smargon import Smargon import mx_bluesky.hyperion.experiment_plans.common.xrc_result as flyscan_result +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import ( XRayCentreEventHandler, ) @@ -22,10 +22,6 @@ RotationScanComposite, multi_rotation_scan, ) -from mx_bluesky.hyperion.external_interaction.callbacks.sample_handling.sample_handling_callback import ( - sample_handling_callback_decorator, -) -from mx_bluesky.hyperion.log import LOGGER from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect from mx_bluesky.hyperion.utils.context import device_composite_from_context @@ -35,10 +31,6 @@ class LoadCentreCollectComposite(RobotLoadThenCentreComposite, RotationScanComposite): """Composite that provides access to the required devices.""" - @property - def sample_motors(self) -> Smargon: - return self.smargon - def create_devices(context: BlueskyContext) -> LoadCentreCollectComposite: """Create the necessary devices for the plan.""" @@ -67,7 +59,6 @@ def load_centre_collect_full( "activate_callbacks": ["SampleHandlingCallback"], } ) - @sample_handling_callback_decorator() def plan_with_callback_subs(): flyscan_event_handler = XRayCentreEventHandler() yield from subs_wrapper( @@ -75,7 +66,9 @@ def plan_with_callback_subs(): flyscan_event_handler, ) - assert flyscan_event_handler.xray_centre_results, "Flyscan result event not received or no crystal found and exception not raised" + assert flyscan_event_handler.xray_centre_results, ( + "Flyscan result event not received or no crystal found and exception not raised" + ) selection_func = flyscan_result.resolve_selection_fn( parameters.selection_params diff --git a/src/mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py b/src/mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py index 1214fa9d7..e7f4b8e5a 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py @@ -14,11 +14,11 @@ from dodal.devices.oav.utils import PinNotFoundException, wait_for_tip_to_be_found from dodal.devices.smargon import Smargon +from mx_bluesky.common.utils.exceptions import catch_exception_and_warn +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.device_setup_plans.setup_oav import ( pre_centring_setup_oav, ) -from mx_bluesky.hyperion.exceptions import catch_exception_and_warn -from mx_bluesky.hyperion.log import LOGGER from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.utils.context import device_composite_from_context @@ -139,7 +139,7 @@ def grid_detection_plan( # See https://github.com/DiamondLightSource/hyperion/wiki/PandA-constant%E2%80%90motion-scanning#motion-program-summary if y_steps % 2 and angle == 0: LOGGER.debug( - f"Forcing number of rows in first grid to be even: Adding an extra row onto bottom of first grid and shifting grid upwards by {box_size_y_pixels/2}" + f"Forcing number of rows in first grid to be even: Adding an extra row onto bottom of first grid and shifting grid upwards by {box_size_y_pixels / 2}" ) y_steps += 1 min_y -= box_size_y_pixels / 2 diff --git a/src/mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py b/src/mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py index 2f99d7a0b..00db5a89b 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py @@ -5,11 +5,11 @@ import numpy as np import pydantic from blueapi.core import BlueskyContext -from dodal.devices.attenuator import Attenuator +from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator from dodal.devices.xspress3.xspress3 import Xspress3 from dodal.devices.zebra_controlled_shutter import ZebraShutter, ZebraShutterState -from mx_bluesky.hyperion.log import LOGGER +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.utils.context import device_composite_from_context @@ -26,7 +26,7 @@ class Direction(Enum): class OptimizeAttenuationComposite: """All devices which are directly or indirectly required by this plan""" - attenuator: Attenuator + attenuator: BinaryFilterAttenuator sample_shutter: ZebraShutter xspress3mini: Xspress3 @@ -197,11 +197,7 @@ def deadtime_optimisation( cycles to complete. Args: - attenuator: (Attenuator) Ophyd device - - xspress3mini: (Xspress3Mini) Ophyd device - - sample_shutter: (ZebraShutter) Ophyd_async device for the fast shutter + composite: (OptimizeAttenuationComposite) Devices required to optimise attenuation transmission: (float) The initial transmission value to use for the optimising @@ -302,11 +298,7 @@ def total_counts_optimisation( defined by the lower and upper limit. To protect the sample, the transmission has a maximum value of 10%. Args: - attenuator: (Attenuator) Ophyd device - - xspress3mini: (Xspress3Mini) Ophyd device - - sample_shutter: (ZebraShutter) Ophyd_async device for the fast shutter + composite: (OptimizeAttenuationComposite) Devices required to optimise attenuation transmission: (float) The initial transmission value to use for the optimising diff --git a/src/mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py b/src/mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py index c5dc7cd6c..6306f7886 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py @@ -8,11 +8,15 @@ from dodal.devices.eiger import EigerDetector from dodal.devices.oav.oav_parameters import OAVParameters +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( + ispyb_activation_wrapper, +) from mx_bluesky.common.parameters.constants import OavConstants from mx_bluesky.common.parameters.gridscan import ( GridScanWithEdgeDetect, PinTipCentreThenXrayCentre, ) +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.device_setup_plans.manipulate_sample import move_phi_chi_omega from mx_bluesky.hyperion.device_setup_plans.utils import ( start_preparing_data_collection_then_do_plan, @@ -34,10 +38,6 @@ PinTipCentringComposite, pin_tip_centre_plan, ) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( - ispyb_activation_wrapper, -) -from mx_bluesky.hyperion.log import LOGGER from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.utils.context import device_composite_from_context @@ -121,6 +121,7 @@ def pin_tip_centre_then_xray_centre( @bpp.subs_decorator(flyscan_event_handler) def pin_centre_flyscan_then_fetch_results() -> MsgGenerator: yield from start_preparing_data_collection_then_do_plan( + composite.beamstop, eiger, composite.detector_motion, parameters.detector_params.detector_distance, @@ -130,9 +131,9 @@ def pin_centre_flyscan_then_fetch_results() -> MsgGenerator: yield from pin_centre_flyscan_then_fetch_results() flyscan_results = flyscan_event_handler.xray_centre_results - assert ( - flyscan_results - ), "Flyscan result event not received or no crystal found and exception not raised" + assert flyscan_results, ( + "Flyscan result event not received or no crystal found and exception not raised" + ) yield from change_aperture_then_move_to_xtal( flyscan_results[0], composite.smargon, composite.aperture_scatterguard ) diff --git a/src/mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py b/src/mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py index 29a6dfe0f..f67566eae 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py @@ -15,12 +15,12 @@ ) from dodal.devices.smargon import Smargon +from mx_bluesky.common.utils.exceptions import SampleException +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.device_setup_plans.setup_oav import pre_centring_setup_oav from mx_bluesky.hyperion.device_setup_plans.smargon import ( move_smargon_warn_on_out_of_range, ) -from mx_bluesky.hyperion.exceptions import SampleException -from mx_bluesky.hyperion.log import LOGGER from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.utils.context import device_composite_from_context diff --git a/src/mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py b/src/mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py index 08dbab6d2..bcc206ea6 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +++ b/src/mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py @@ -11,7 +11,7 @@ from blueapi.core import BlueskyContext from bluesky.utils import Msg from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue -from dodal.devices.attenuator import Attenuator +from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator from dodal.devices.dcm import DCM from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, MirrorVoltages from dodal.devices.motors import XYZPositioner @@ -25,11 +25,11 @@ from dodal.plan_stubs.motor_utils import MoveTooLarge, home_and_reset_wrapper from mx_bluesky.common.parameters.robot_load import RobotLoadAndEnergyChange +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.experiment_plans.set_energy_plan import ( SetEnergyComposite, set_energy_plan, ) -from mx_bluesky.hyperion.log import LOGGER from mx_bluesky.hyperion.parameters.constants import CONST @@ -41,7 +41,7 @@ class RobotLoadAndEnergyChangeComposite: dcm: DCM undulator_dcm: UndulatorDCM xbpm_feedback: XBPMFeedback - attenuator: Attenuator + attenuator: BinaryFilterAttenuator # RobotLoad fields robot: BartRobot @@ -122,6 +122,11 @@ def do_robot_load( demand_energy_ev: float | None, thawing_time: float, ): + error_code = yield from bps.rd(composite.robot.error_code) + # Reset robot if light curtains were tripped + if error_code == 40: + yield from bps.trigger(composite.robot.reset, wait=True) + yield from bps.abs_set( composite.robot, sample_location, diff --git a/src/mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py b/src/mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py index 1534d1db7..8e37c5193 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py @@ -9,7 +9,7 @@ from bluesky import plan_stubs as bps from bluesky.utils import MsgGenerator from dodal.devices.aperturescatterguard import ApertureScatterguard -from dodal.devices.attenuator import Attenuator +from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator from dodal.devices.backlight import Backlight from dodal.devices.dcm import DCM from dodal.devices.detector.detector_motion import DetectorMotion @@ -17,6 +17,7 @@ from dodal.devices.fast_grid_scan import PandAFastGridScan, ZebraFastGridScan from dodal.devices.flux import Flux from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, MirrorVoltages +from dodal.devices.i03.beamstop import Beamstop from dodal.devices.motors import XYZPositioner from dodal.devices.oav.oav_detector import OAV from dodal.devices.oav.pin_image_recognition import PinTipDetection @@ -69,7 +70,7 @@ class RobotLoadThenCentreComposite: # common fields xbpm_feedback: XBPMFeedback - attenuator: Attenuator + attenuator: BinaryFilterAttenuator # GridDetectThenXRayCentreComposite fields aperture_scatterguard: ApertureScatterguard @@ -101,10 +102,7 @@ class RobotLoadThenCentreComposite: robot: BartRobot webcam: Webcam lower_gonio: XYZPositioner - - @property - def sample_motors(self): - return self.smargon + beamstop: Beamstop def create_devices(context: BlueskyContext) -> RobotLoadThenCentreComposite: @@ -211,6 +209,7 @@ def robot_load_then_xray_centre( eiger.set_detector_parameters(detector_params) yield from start_preparing_data_collection_then_do_plan( + composite.beamstop, eiger, composite.detector_motion, parameters.detector_distance_mm, diff --git a/src/mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py b/src/mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py index 9887847c4..bb1102ead 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py @@ -8,12 +8,13 @@ from blueapi.core import BlueskyContext from bluesky.utils import MsgGenerator from dodal.devices.aperturescatterguard import ApertureScatterguard -from dodal.devices.attenuator import Attenuator +from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator from dodal.devices.backlight import Backlight from dodal.devices.dcm import DCM from dodal.devices.detector.detector_motion import DetectorMotion from dodal.devices.eiger import EigerDetector from dodal.devices.flux import Flux +from dodal.devices.i03.beamstop import Beamstop from dodal.devices.oav.oav_detector import OAV from dodal.devices.oav.oav_parameters import OAVParameters from dodal.devices.robot import BartRobot @@ -29,6 +30,7 @@ from mx_bluesky.common.device_setup_plans.read_hardware_for_setup import ( read_hardware_for_zocalo, ) +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.device_setup_plans.manipulate_sample import ( cleanup_sample_environment, move_phi_chi_omega, @@ -55,7 +57,6 @@ oav_snapshot_plan, setup_beamline_for_OAV, ) -from mx_bluesky.hyperion.log import LOGGER from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.rotation import ( MultiRotationScan, @@ -69,8 +70,9 @@ class RotationScanComposite(OavSnapshotComposite): """All devices which are directly or indirectly required by this plan""" aperture_scatterguard: ApertureScatterguard - attenuator: Attenuator + attenuator: BinaryFilterAttenuator backlight: Backlight + beamstop: Beamstop dcm: DCM detector_motion: DetectorMotion eiger: EigerDetector @@ -126,13 +128,26 @@ def calculate_motion_profile( See https://github.com/DiamondLightSource/hyperion/wiki/rotation-scan-geometry for a simple pictorial explanation.""" - direction = params.rotation_direction.multiplier + assert params.rotation_increment_deg > 0 + + direction = params.rotation_direction + start_scan_deg = params.omega_start_deg + + if params.features.omega_flip: + # If omega_flip is True then the motor omega axis is inverted with respect to the + # hyperion coordinate system. + start_scan_deg = -start_scan_deg + direction = ( + direction.POSITIVE + if direction == direction.NEGATIVE + else direction.NEGATIVE + ) + num_images = params.num_images shutter_time_s = params.shutter_opening_time_s image_width_deg = params.rotation_increment_deg exposure_time_s = params.exposure_time_s motor_time_to_speed_s *= ACCELERATION_MARGIN - start_scan_deg = params.omega_start_deg LOGGER.info("Calculating rotation scan motion profile:") LOGGER.info( @@ -153,9 +168,9 @@ def calculate_motion_profile( f"{acceleration_offset_deg=} = {motor_time_to_speed_s=} * {speed_for_rotation_deg_s=}" ) - start_motion_deg = start_scan_deg - (acceleration_offset_deg * direction) + start_motion_deg = start_scan_deg - (acceleration_offset_deg * direction.multiplier) LOGGER.info( - f"{start_motion_deg=} = {start_scan_deg=} - ({acceleration_offset_deg=} * {direction=})" + f"{start_motion_deg=} = {start_scan_deg=} - ({acceleration_offset_deg=} * {direction.multiplier=})" ) shutter_opening_deg = speed_for_rotation_deg_s * shutter_time_s @@ -173,7 +188,7 @@ def calculate_motion_profile( distance_to_move_deg = ( scan_width_deg + shutter_opening_deg + acceleration_offset_deg * 2 - ) * direction + ) * direction.multiplier LOGGER.info( f"{distance_to_move_deg=} = ({scan_width_deg=} + {shutter_opening_deg=} + {acceleration_offset_deg=} * 2) * {direction=})" ) @@ -183,7 +198,7 @@ def calculate_motion_profile( start_motion_deg=start_motion_deg, scan_width_deg=scan_width_deg, shutter_time_s=shutter_time_s, - direction=params.rotation_direction, + direction=direction, speed_for_rotation_deg_s=speed_for_rotation_deg_s, acceleration_offset_deg=acceleration_offset_deg, shutter_opening_deg=shutter_opening_deg, @@ -344,6 +359,8 @@ def rotation_scan( parameters: RotationScan, oav_params: OAVParameters | None = None, ) -> MsgGenerator: + parameters.features.update_self_from_server() + if not oav_params: oav_params = OAVParameters(context="xrayCentring") @@ -353,7 +370,7 @@ def rotation_scan( "subplan_name": CONST.PLAN.ROTATION_OUTER, CONST.TRIGGER.ZOCALO: CONST.PLAN.ROTATION_MAIN, "zocalo_environment": CONST.ZOCALO_ENV, - "hyperion_parameters": parameters.model_dump_json(), + "mx_bluesky_parameters": parameters.model_dump_json(), "activate_callbacks": [ "RotationISPyBCallback", "RotationNexusFileCallback", @@ -377,6 +394,7 @@ def rotation_with_cleanup_and_stage(params: RotationScan): LOGGER.info("setting up and staging eiger...") yield from start_preparing_data_collection_then_do_plan( + composite.beamstop, eiger, composite.detector_motion, params.detector_distance_mm, @@ -393,6 +411,7 @@ def multi_rotation_scan( parameters: MultiRotationScan, oav_params: OAVParameters | None = None, ) -> MsgGenerator: + parameters.features.update_self_from_server() if not oav_params: oav_params = OAVParameters(context="xrayCentring") eiger: EigerDetector = composite.eiger @@ -425,8 +444,7 @@ def _multi_rotation_scan(): md={ "subplan_name": CONST.PLAN.ROTATION_OUTER, CONST.TRIGGER.ZOCALO: CONST.PLAN.ROTATION_MAIN, - "zocalo_environment": CONST.ZOCALO_ENV, - "hyperion_parameters": single_scan.model_dump_json(), + "mx_bluesky_parameters": single_scan.model_dump_json(), } ) def rotation_scan_core( @@ -438,6 +456,7 @@ def rotation_scan_core( LOGGER.info("setting up and staging eiger...") yield from start_preparing_data_collection_then_do_plan( + composite.beamstop, eiger, composite.detector_motion, parameters.detector_distance_mm, diff --git a/src/mx_bluesky/hyperion/experiment_plans/set_energy_plan.py b/src/mx_bluesky/hyperion/experiment_plans/set_energy_plan.py index 5d603a085..4f37ce59c 100644 --- a/src/mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +++ b/src/mx_bluesky/hyperion/experiment_plans/set_energy_plan.py @@ -7,7 +7,7 @@ import pydantic from bluesky import plan_stubs as bps -from dodal.devices.attenuator import Attenuator +from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator from dodal.devices.dcm import DCM from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, MirrorVoltages from dodal.devices.undulator_dcm import UndulatorDCM @@ -30,7 +30,7 @@ class SetEnergyComposite: dcm: DCM undulator_dcm: UndulatorDCM xbpm_feedback: XBPMFeedback - attenuator: Attenuator + attenuator: BinaryFilterAttenuator def _set_energy_plan( diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py index b29fec9e7..02d2ab1af 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/__main__.py @@ -7,10 +7,24 @@ from dodal.log import LOGGER as dodal_logger from dodal.log import set_up_all_logging_handlers -from mx_bluesky.common.utils.log import _get_logging_dir, tag_filter -from mx_bluesky.hyperion.external_interaction.callbacks.log_uid_tag_callback import ( +from mx_bluesky.common.external_interaction.callbacks.common.log_uid_tag_callback import ( LogUidTaggingCallback, ) +from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import ( + ZocaloCallback, +) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( + GridscanISPyBCallback, +) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( + GridscanNexusFileCallback, +) +from mx_bluesky.common.utils.log import ( + ISPYB_ZOCALO_CALLBACK_LOGGER, + NEXUS_LOGGER, + _get_logging_dir, + tag_filter, +) from mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback import ( RobotLoadISPyBCallback, ) @@ -23,19 +37,6 @@ from mx_bluesky.hyperion.external_interaction.callbacks.sample_handling.sample_handling_callback import ( SampleHandlingCallback, ) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, -) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.nexus_callback import ( - GridscanNexusFileCallback, -) -from mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback import ( - ZocaloCallback, -) -from mx_bluesky.hyperion.log import ( - ISPYB_LOGGER, - NEXUS_LOGGER, -) from mx_bluesky.hyperion.parameters.cli import parse_callback_dev_mode_arg from mx_bluesky.hyperion.parameters.constants import CONST @@ -58,7 +59,7 @@ def setup_callbacks(): def setup_logging(dev_mode: bool): for logger, filename in [ - (ISPYB_LOGGER, "hyperion_ispyb_callback.log"), + (ISPYB_ZOCALO_CALLBACK_LOGGER, "hyperion_ispyb_callback.log"), (NEXUS_LOGGER, "hyperion_nexus_callback.log"), ]: if logger.handlers == []: @@ -74,7 +75,7 @@ def setup_logging(dev_mode: bool): log_info(f"Loggers initialised with dev_mode={dev_mode}") nexgen_logger = logging.getLogger("nexgen") nexgen_logger.parent = NEXUS_LOGGER - dodal_logger.parent = ISPYB_LOGGER + dodal_logger.parent = ISPYB_ZOCALO_CALLBACK_LOGGER log_debug("nexgen logger added to nexus logger") @@ -94,12 +95,12 @@ def start_dispatcher(callbacks: list[Callable]): def log_info(msg, *args, **kwargs): - ISPYB_LOGGER.info(msg, *args, **kwargs) + ISPYB_ZOCALO_CALLBACK_LOGGER.info(msg, *args, **kwargs) NEXUS_LOGGER.info(msg, *args, **kwargs) def log_debug(msg, *args, **kwargs): - ISPYB_LOGGER.debug(msg, *args, **kwargs) + ISPYB_ZOCALO_CALLBACK_LOGGER.debug(msg, *args, **kwargs) NEXUS_LOGGER.debug(msg, *args, **kwargs) diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py index c7c7fe7c6..63595b49e 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py @@ -2,6 +2,15 @@ from bluesky.callbacks import CallbackBase +from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import ( + ZocaloCallback, +) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( + GridscanISPyBCallback, +) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( + GridscanNexusFileCallback, +) from mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback import ( RobotLoadISPyBCallback, ) @@ -14,22 +23,13 @@ from mx_bluesky.hyperion.external_interaction.callbacks.sample_handling.sample_handling_callback import ( SampleHandlingCallback, ) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, -) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.nexus_callback import ( - GridscanNexusFileCallback, -) -from mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback import ( - ZocaloCallback, -) CallbacksFactory = Callable[[], tuple[CallbackBase, ...]] -def create_robot_load_and_centre_callbacks() -> ( - tuple[GridscanNexusFileCallback, GridscanISPyBCallback, RobotLoadISPyBCallback] -): +def create_robot_load_and_centre_callbacks() -> tuple[ + GridscanNexusFileCallback, GridscanISPyBCallback, RobotLoadISPyBCallback +]: return ( GridscanNexusFileCallback(), GridscanISPyBCallback(emit=ZocaloCallback()), @@ -37,28 +37,29 @@ def create_robot_load_and_centre_callbacks() -> ( ) -def create_gridscan_callbacks() -> ( - tuple[GridscanNexusFileCallback, GridscanISPyBCallback] -): - return (GridscanNexusFileCallback(), GridscanISPyBCallback(emit=ZocaloCallback())) +def create_gridscan_callbacks() -> tuple[ + GridscanNexusFileCallback, GridscanISPyBCallback +]: + return ( + GridscanNexusFileCallback(), + GridscanISPyBCallback(emit=ZocaloCallback()), + ) -def create_rotation_callbacks() -> ( - tuple[RotationNexusFileCallback, RotationISPyBCallback] -): +def create_rotation_callbacks() -> tuple[ + RotationNexusFileCallback, RotationISPyBCallback +]: return (RotationNexusFileCallback(), RotationISPyBCallback(emit=ZocaloCallback())) -def create_load_centre_collect_callbacks() -> ( - tuple[ - GridscanNexusFileCallback, - GridscanISPyBCallback, - RobotLoadISPyBCallback, - RotationNexusFileCallback, - RotationISPyBCallback, - SampleHandlingCallback, - ] -): +def create_load_centre_collect_callbacks() -> tuple[ + GridscanNexusFileCallback, + GridscanISPyBCallback, + RobotLoadISPyBCallback, + RotationNexusFileCallback, + RotationISPyBCallback, + SampleHandlingCallback, +]: return ( GridscanNexusFileCallback(), GridscanISPyBCallback(emit=ZocaloCallback()), diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py index 29d91f5ff..fd4b27ee4 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py @@ -2,18 +2,18 @@ from typing import TYPE_CHECKING -from mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping import ( +from mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping import ( get_proposal_and_session_from_visit_string, ) -from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import ( +from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import ( PlanReactiveCallback, ) -from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import ( +from mx_bluesky.common.external_interaction.ispyb.exp_eye_store import ( BLSampleStatus, ExpeyeInteraction, RobotActionID, ) -from mx_bluesky.hyperion.log import ISPYB_LOGGER +from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER from mx_bluesky.hyperion.parameters.constants import CONST if TYPE_CHECKING: @@ -22,18 +22,23 @@ class RobotLoadISPyBCallback(PlanReactiveCallback): def __init__(self) -> None: - ISPYB_LOGGER.debug("Initialising ISPyB Robot Load Callback") - super().__init__(log=ISPYB_LOGGER) + ISPYB_ZOCALO_CALLBACK_LOGGER.debug("Initialising ISPyB Robot Load Callback") + super().__init__(log=ISPYB_ZOCALO_CALLBACK_LOGGER) self._metadata: dict | None = None + self.run_uid: str | None = None self.descriptors: dict[str, EventDescriptor] = {} self.action_id: RobotActionID | None = None self.expeye = ExpeyeInteraction() def activity_gated_start(self, doc: RunStart): - ISPYB_LOGGER.debug("ISPyB robot load callback received start document.") + ISPYB_ZOCALO_CALLBACK_LOGGER.debug( + "ISPyB robot load callback received start document." + ) if doc.get("subplan_name") == CONST.PLAN.ROBOT_LOAD: - ISPYB_LOGGER.debug(f"ISPyB robot load callback received: {doc}") + ISPYB_ZOCALO_CALLBACK_LOGGER.debug( + f"ISPyB robot load callback received: {doc}" + ) self.run_uid = doc.get("uid") self._metadata = doc.get("metadata") assert isinstance(self._metadata, dict) @@ -59,9 +64,9 @@ def activity_gated_event(self, doc: Event) -> Event | None: event_descriptor and event_descriptor.get("name") == CONST.DESCRIPTORS.ROBOT_LOAD ): - assert ( - self.action_id is not None - ), "ISPyB Robot load callback event called unexpectedly" + assert self.action_id is not None, ( + "ISPyB Robot load callback event called unexpectedly" + ) barcode = doc["data"]["robot-barcode"] oav_snapshot = doc["data"]["oav-snapshot-last_saved_path"] webcam_snapshot = doc["data"]["webcam-last_saved_path"] @@ -73,11 +78,13 @@ def activity_gated_event(self, doc: Event) -> Event | None: return super().activity_gated_event(doc) def activity_gated_stop(self, doc: RunStop) -> RunStop | None: - ISPYB_LOGGER.debug("ISPyB robot load callback received stop document.") + ISPYB_ZOCALO_CALLBACK_LOGGER.debug( + "ISPyB robot load callback received stop document." + ) if doc.get("run_start") == self.run_uid: - assert ( - self.action_id is not None - ), "ISPyB Robot load callback stop called unexpectedly" + assert self.action_id is not None, ( + "ISPyB Robot load callback stop called unexpectedly" + ) exit_status = doc.get("exit_status") assert exit_status, "Exit status not available in stop document!" assert self._metadata, "Metadata not received before stop document." diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py index a3ca2c5d8..d2dc37bab 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py @@ -3,28 +3,27 @@ from collections.abc import Callable, Sequence from typing import TYPE_CHECKING, Any, cast -from mx_bluesky.common.parameters.components import IspybExperimentType -from mx_bluesky.common.utils.log import set_dcgid_tag -from mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping import ( - populate_data_collection_group, - populate_remaining_data_collection_info, -) -from mx_bluesky.hyperion.external_interaction.callbacks.ispyb_callback_base import ( +from mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base import ( BaseISPyBCallback, ) -from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_mapping import ( - populate_data_collection_info_for_rotation, +from mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping import ( + populate_data_collection_group, + populate_remaining_data_collection_info, ) -from mx_bluesky.hyperion.external_interaction.ispyb.data_model import ( +from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionInfo, DataCollectionPositionInfo, ScanDataInfo, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import ( +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( IspybIds, StoreInIspyb, ) -from mx_bluesky.hyperion.log import ISPYB_LOGGER +from mx_bluesky.common.parameters.components import IspybExperimentType +from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, set_dcgid_tag +from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_mapping import ( + populate_data_collection_info_for_rotation, +) from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.rotation import RotationScan @@ -58,10 +57,10 @@ def __init__( def activity_gated_start(self, doc: RunStart): if doc.get("subplan_name") == CONST.PLAN.ROTATION_OUTER: - ISPYB_LOGGER.info( + ISPYB_ZOCALO_CALLBACK_LOGGER.info( "ISPyB callback received start document with experiment parameters." ) - hyperion_params = doc.get("hyperion_parameters") + hyperion_params = doc.get("mx_bluesky_parameters") assert isinstance(hyperion_params, str) self.params = RotationScan.model_validate_json(hyperion_params) dcgid = ( @@ -73,16 +72,18 @@ def activity_gated_start(self, doc: RunStart): self.params.ispyb_experiment_type == IspybExperimentType.CHARACTERIZATION ): - ISPYB_LOGGER.info("Screening collection - using new DCG") + ISPYB_ZOCALO_CALLBACK_LOGGER.info( + "Screening collection - using new DCG" + ) dcgid = None self.last_sample_id = None else: - ISPYB_LOGGER.info( + ISPYB_ZOCALO_CALLBACK_LOGGER.info( f"Collection is {self.params.ispyb_experiment_type} - storing sampleID to bundle images" ) self.last_sample_id = self.params.sample_id self.ispyb = StoreInIspyb(self.ispyb_config) - ISPYB_LOGGER.info("Beginning ispyb deposition") + ISPYB_ZOCALO_CALLBACK_LOGGER.info("Beginning ispyb deposition") data_collection_group_info = populate_data_collection_group(self.params) data_collection_info = populate_data_collection_info_for_rotation( cast(RotationScan, self.params) @@ -100,7 +101,7 @@ def activity_gated_start(self, doc: RunStart): self.ispyb_ids = self.ispyb.begin_deposition( data_collection_group_info, [scan_data_info] ) - ISPYB_LOGGER.info("ISPYB handler received start document.") + ISPYB_ZOCALO_CALLBACK_LOGGER.info("ISPYB handler received start document.") if doc.get("subplan_name") == CONST.PLAN.ROTATION_MAIN: self.uid_to_finalize_on = doc.get("uid") return super().activity_gated_start(doc) @@ -111,9 +112,9 @@ def populate_info_for_update( event_sourced_position_info: DataCollectionPositionInfo | None, params, ) -> Sequence[ScanDataInfo]: - assert ( - self.ispyb_ids.data_collection_ids - ), "Expect an existing DataCollection to update" + assert self.ispyb_ids.data_collection_ids, ( + "Expect an existing DataCollection to update" + ) return [ ScanDataInfo( @@ -131,9 +132,9 @@ def _handle_ispyb_hardware_read(self, doc: Event): doc["data"]["smargon-y"], doc["data"]["smargon-z"], ] - assert ( - self.params - ), "handle_ispyb_hardware_read triggered before activity_gated_start" + assert self.params, ( + "handle_ispyb_hardware_read triggered before activity_gated_start" + ) motor_positions_um = [position * 1000 for position in motor_positions_mm] comment = f"Sample position (µm): ({motor_positions_um[0]:.0f}, {motor_positions_um[1]:.0f}, {motor_positions_um[2]:.0f}) {self.params.comment} " scan_data_infos[0].data_collection_info.comments = comment diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py index ca6ac3124..a4e4f0ca6 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py @@ -1,6 +1,6 @@ from __future__ import annotations -from mx_bluesky.hyperion.external_interaction.ispyb.data_model import DataCollectionInfo +from mx_bluesky.common.external_interaction.ispyb.data_model import DataCollectionInfo from mx_bluesky.hyperion.parameters.rotation import RotationScan diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py index 01e3d640c..00251962d 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py @@ -2,20 +2,22 @@ from typing import TYPE_CHECKING -from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import ( +from mx_bluesky.common.external_interaction.callbacks.common.logging_callback import ( + format_doc_for_log, +) +from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import ( PlanReactiveCallback, ) -from mx_bluesky.hyperion.external_interaction.nexus.nexus_utils import ( +from mx_bluesky.common.external_interaction.nexus.nexus_utils import ( + AxisDirection, create_beam_and_attenuator_parameters, vds_type_based_on_bit_depth, ) -from mx_bluesky.hyperion.external_interaction.nexus.write_nexus import NexusWriter -from mx_bluesky.hyperion.log import NEXUS_LOGGER +from mx_bluesky.common.external_interaction.nexus.write_nexus import NexusWriter +from mx_bluesky.common.utils.log import NEXUS_LOGGER from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.rotation import RotationScan -from ..logging_callback import format_doc_for_log - if TYPE_CHECKING: from event_model.documents import Event, EventDescriptor, RunStart @@ -78,7 +80,7 @@ def activity_gated_start(self, doc: RunStart): self.meta_data_run_number = doc.get("meta_data_run_number") if doc.get("subplan_name") == CONST.PLAN.ROTATION_OUTER: self.run_uid = doc.get("uid") - hyperion_params = doc.get("hyperion_parameters") + hyperion_params = doc.get("mx_bluesky_parameters") assert isinstance(hyperion_params, str) NEXUS_LOGGER.info( f"Nexus writer received start document with experiment parameters {hyperion_params}" @@ -100,5 +102,7 @@ def activity_gated_start(self, doc: RunStart): vds_start_index=parameters.nexus_vds_start_img, full_num_of_images=self.full_num_of_images, meta_data_run_number=self.meta_data_run_number, - rotation_direction=parameters.rotation_direction, + axis_direction=AxisDirection.NEGATIVE + if parameters.features.omega_flip + else AxisDirection.POSITIVE, ) diff --git a/src/mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/sample_handling_callback.py b/src/mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/sample_handling_callback.py index daf1b4ab5..a874ef128 100644 --- a/src/mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/sample_handling_callback.py +++ b/src/mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/sample_handling_callback.py @@ -1,45 +1,14 @@ -import dataclasses -from collections.abc import Generator -from functools import partial -from typing import Any +from event_model import RunStart, RunStop -import bluesky.plan_stubs as bps -from bluesky.preprocessors import contingency_wrapper -from bluesky.utils import Msg, make_decorator -from event_model import Event, EventDescriptor, RunStart - -from mx_bluesky.hyperion.exceptions import CrystalNotFoundException, SampleException -from mx_bluesky.hyperion.external_interaction.callbacks.common.abstract_event import ( - AbstractEvent, -) -from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import ( +from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import ( PlanReactiveCallback, ) -from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import ( +from mx_bluesky.common.external_interaction.ispyb.exp_eye_store import ( BLSampleStatus, ExpeyeInteraction, ) -from mx_bluesky.hyperion.log import ISPYB_LOGGER -from mx_bluesky.hyperion.parameters.constants import CONST - -# TODO remove this event-raising shenanigans once -# https://github.com/bluesky/bluesky/issues/1829 is addressed - - -@dataclasses.dataclass(frozen=True) -class _ExceptionEvent(AbstractEvent): - exception_type: str - - -def _exception_interceptor(exception: Exception) -> Generator[Msg, Any, Any]: - yield from bps.create(CONST.DESCRIPTORS.SAMPLE_HANDLING_EXCEPTION) - yield from bps.read(_ExceptionEvent(type(exception).__name__)) - yield from bps.save() - - -sample_handling_callback_decorator = make_decorator( - partial(contingency_wrapper, except_plan=_exception_interceptor) -) +from mx_bluesky.common.utils.exceptions import CrystalNotFoundException, SampleException +from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER class SampleHandlingCallback(PlanReactiveCallback): @@ -47,7 +16,7 @@ class SampleHandlingCallback(PlanReactiveCallback): field according to the type of exception raised.""" def __init__(self): - super().__init__(log=ISPYB_LOGGER) + super().__init__(log=ISPYB_ZOCALO_CALLBACK_LOGGER) self._sample_id: int | None = None self._descriptor: str | None = None @@ -57,16 +26,13 @@ def activity_gated_start(self, doc: RunStart): self.log.info(f"Recording sample ID at run start {sample_id}") self._sample_id = sample_id - def activity_gated_descriptor(self, doc: EventDescriptor) -> EventDescriptor | None: - if doc.get("name") == CONST.DESCRIPTORS.SAMPLE_HANDLING_EXCEPTION: - self._descriptor = doc["uid"] - return super().activity_gated_descriptor(doc) - - def activity_gated_event(self, doc: Event) -> Event | None: - if doc["descriptor"] == self._descriptor: - exception_type = doc["data"]["exception_type"] + def activity_gated_stop(self, doc: RunStop) -> RunStop: + if doc["exit_status"] != "success": + exception_type, message = SampleException.type_and_message_from_reason( + doc.get("reason", "") + ) self.log.info( - f"Sample handling callback intercepted exception of type {exception_type}" + f"Sample handling callback intercepted exception of type {exception_type}: {message}" ) self._record_exception(exception_type) return doc diff --git a/src/mx_bluesky/hyperion/external_interaction/config_server.py b/src/mx_bluesky/hyperion/external_interaction/config_server.py index 3875083a0..bfa1dfef2 100644 --- a/src/mx_bluesky/hyperion/external_interaction/config_server.py +++ b/src/mx_bluesky/hyperion/external_interaction/config_server.py @@ -3,11 +3,24 @@ from daq_config_server.client import ConfigServer from mx_bluesky.common.external_interaction.config_server import FeatureFlags -from mx_bluesky.hyperion.log import LOGGER +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.parameters.constants import CONST class HyperionFeatureFlags(FeatureFlags): + """ + Feature flags specific to Hyperion. + + Attributes: + use_panda_for_gridscan: If True then the PandA is used for gridscans, otherwise the zebra is used + compare_cpu_and_gpu_zocalo: If True then GPU result processing is enabled alongside CPU, if False then + CPU only is used. + set_stub_offsets: If True then set the stub offsets after moving to the crystal (ignored for + multi-centre) + omega_flip: If True then invert the smargon omega motor rotation commands with respect to + the hyperion request. + """ + @staticmethod @cache def get_config_server() -> ConfigServer: @@ -16,3 +29,4 @@ def get_config_server() -> ConfigServer: use_panda_for_gridscan: bool = CONST.I03.USE_PANDA_FOR_GRIDSCAN compare_cpu_and_gpu_zocalo: bool = CONST.I03.COMPARE_CPU_AND_GPU_ZOCALO set_stub_offsets: bool = CONST.I03.SET_STUB_OFFSETS + omega_flip: bool = CONST.I03.OMEGA_FLIP diff --git a/src/mx_bluesky/hyperion/external_interaction/exceptions.py b/src/mx_bluesky/hyperion/external_interaction/exceptions.py deleted file mode 100644 index eadc5806e..000000000 --- a/src/mx_bluesky/hyperion/external_interaction/exceptions.py +++ /dev/null @@ -1,4 +0,0 @@ -class ISPyBDepositionNotMade(Exception): - """Raised when the ISPyB or Zocalo callbacks can't access ISPyB deposition numbers.""" - - pass diff --git a/src/mx_bluesky/hyperion/log.py b/src/mx_bluesky/hyperion/log.py deleted file mode 100755 index d7f2e5da2..000000000 --- a/src/mx_bluesky/hyperion/log.py +++ /dev/null @@ -1,15 +0,0 @@ -import logging - -from dodal.log import LOGGER as dodal_logger - -LOGGER = logging.getLogger("Hyperion") -LOGGER.setLevel("DEBUG") -LOGGER.parent = dodal_logger - -ISPYB_LOGGER = logging.getLogger("Hyperion ISPyB and Zocalo callbacks") -ISPYB_LOGGER.setLevel(logging.DEBUG) - -NEXUS_LOGGER = logging.getLogger("Hyperion NeXus callbacks") -NEXUS_LOGGER.setLevel(logging.DEBUG) - -ALL_LOGGERS = [LOGGER, ISPYB_LOGGER, NEXUS_LOGGER] diff --git a/src/mx_bluesky/hyperion/parameters/constants.py b/src/mx_bluesky/hyperion/parameters/constants.py index 051fa461b..3bcc99504 100644 --- a/src/mx_bluesky/hyperion/parameters/constants.py +++ b/src/mx_bluesky/hyperion/parameters/constants.py @@ -28,6 +28,7 @@ class I03Constants: SHUTTER_TIME_S = 0.06 USE_PANDA_FOR_GRIDSCAN = False SET_STUB_OFFSETS = False + OMEGA_FLIP = True # Turns on GPU processing for zocalo and logs a comparison between GPU and CPU- # processed results. GPU results never used in analysis for now diff --git a/src/mx_bluesky/hyperion/parameters/gridscan.py b/src/mx_bluesky/hyperion/parameters/gridscan.py index 5e8d4ad1a..77e6b12ef 100644 --- a/src/mx_bluesky/hyperion/parameters/gridscan.py +++ b/src/mx_bluesky/hyperion/parameters/gridscan.py @@ -7,27 +7,26 @@ PandAGridScanParams, ZebraGridScanParams, ) -from pydantic import Field, PrivateAttr -from scanspec.core import Path as ScanPath -from scanspec.specs import Line, Static from mx_bluesky.common.parameters.components import ( - SplitScan, - WithOptionalEnergyChange, WithPandaGridScan, ) from mx_bluesky.common.parameters.gridscan import ( - GridCommon, - SpecifiedGrid, + ThreeDGridScan, ) from mx_bluesky.hyperion.parameters.components import WithHyperionFeatures from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants -class HyperionGridCommon(GridCommon, WithHyperionFeatures): - # This class only exists so that we can properly select enable_dev_shm. Remove in - # https://github.com/DiamondLightSource/hyperion/issues/1395""" +class HyperionThreeDGridScan( + ThreeDGridScan, + WithPandaGridScan, + WithHyperionFeatures, +): + """Hyperion's 3D grid scan varies from the common class due to: optionally using a PandA, optionally using dev_shm for GPU analysis, and using a config server for features""" + # These detector params only exist so that we can properly select enable_dev_shm. Remove in + # https://github.com/DiamondLightSource/hyperion/issues/1395""" @property def detector_params(self): self.det_dist_to_beam_converter_path = ( @@ -37,9 +36,9 @@ def detector_params(self): optional_args = {} if self.run_number: optional_args["run_number"] = self.run_number - assert ( - self.detector_distance_mm is not None - ), "Detector distance must be filled before generating DetectorParams" + assert self.detector_distance_mm is not None, ( + "Detector distance must be filled before generating DetectorParams" + ) return DetectorParams( detector_size_constants=I03Constants.DETECTOR, expected_energy_ev=self.demand_energy_ev, @@ -58,29 +57,7 @@ def detector_params(self): **optional_args, ) - -class HyperionThreeDGridScan( - HyperionGridCommon, - SpecifiedGrid, - SplitScan, - WithOptionalEnergyChange, - WithPandaGridScan, -): - """Parameters representing a so-called 3D grid scan, which consists of doing a - gridscan in X and Y, followed by one in X and Z.""" - - grid1_omega_deg: float = Field(default=CONST.PARAM.GRIDSCAN.OMEGA_1) # type: ignore - grid2_omega_deg: float = Field(default=CONST.PARAM.GRIDSCAN.OMEGA_2) - x_step_size_um: float = Field(default=CONST.PARAM.GRIDSCAN.BOX_WIDTH_UM) - y_step_size_um: float = Field(default=CONST.PARAM.GRIDSCAN.BOX_WIDTH_UM) - z_step_size_um: float = Field(default=CONST.PARAM.GRIDSCAN.BOX_WIDTH_UM) - y2_start_um: float - z2_start_um: float - x_steps: int = Field(gt=0) - y_steps: int = Field(gt=0) - z_steps: int = Field(gt=0) - _set_stub_offsets: bool = PrivateAttr(default_factory=lambda: False) - + # Relative to common grid scan, stub offsets are defined by config server @property def FGS_params(self) -> ZebraGridScanParams: return ZebraGridScanParams( @@ -124,59 +101,5 @@ def panda_FGS_params(self) -> PandAGridScanParams: transmission_fraction=self.transmission_frac, ) - def do_set_stub_offsets(self, value: bool): - self._set_stub_offsets = value - - @property - def grid_1_spec(self): - x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1) - y1_end = self.y_start_um + self.y_step_size_um * (self.y_steps - 1) - grid_1_x = Line("sam_x", self.x_start_um, x_end, self.x_steps) - grid_1_y = Line("sam_y", self.y_start_um, y1_end, self.y_steps) - grid_1_z = Static("sam_z", self.z_start_um) - return grid_1_y.zip(grid_1_z) * ~grid_1_x - - @property - def grid_2_spec(self): - x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1) - z2_end = self.z2_start_um + self.z_step_size_um * (self.z_steps - 1) - grid_2_x = Line("sam_x", self.x_start_um, x_end, self.x_steps) - grid_2_z = Line("sam_z", self.z2_start_um, z2_end, self.z_steps) - grid_2_y = Static("sam_y", self.y2_start_um) - return grid_2_z.zip(grid_2_y) * ~grid_2_x - - @property - def scan_indices(self): - """The first index of each gridscan, useful for writing nexus files/VDS""" - return [ - 0, - len(ScanPath(self.grid_1_spec.calculate()).consume().midpoints["sam_x"]), - ] - - @property - def scan_spec(self): - """A fully specified ScanSpec object representing both grids, with x, y, z and - omega positions.""" - return self.grid_1_spec.concat(self.grid_2_spec) - - @property - def scan_points(self): - """A list of all the points in the scan_spec.""" - return ScanPath(self.scan_spec.calculate()).consume().midpoints - - @property - def scan_points_first_grid(self): - """A list of all the points in the first grid scan.""" - return ScanPath(self.grid_1_spec.calculate()).consume().midpoints - - @property - def scan_points_second_grid(self): - """A list of all the points in the second grid scan.""" - return ScanPath(self.grid_2_spec.calculate()).consume().midpoints - - @property - def num_images(self) -> int: - return len(self.scan_points["sam_x"]) - class OddYStepsException(Exception): ... diff --git a/src/mx_bluesky/hyperion/parameters/load_centre_collect.py b/src/mx_bluesky/hyperion/parameters/load_centre_collect.py index c07ea81f7..73d6b2774 100644 --- a/src/mx_bluesky/hyperion/parameters/load_centre_collect.py +++ b/src/mx_bluesky/hyperion/parameters/load_centre_collect.py @@ -1,4 +1,4 @@ -from typing import TypeVar +from typing import Self, TypeVar from pydantic import BaseModel, model_validator @@ -39,10 +39,31 @@ def validate_model(cls, values): | RobotLoadThenCentre.model_fields.keys() | MultiRotationScan.model_fields.keys() ) + disallowed_keys = values.keys() - allowed_keys - assert ( - disallowed_keys == set() - ), f"Unexpected fields found in LoadCentreCollect {disallowed_keys}" + assert disallowed_keys == set(), ( + f"Unexpected fields found in LoadCentreCollect {disallowed_keys}" + ) + + keys_from_outer_load_centre_collect = ( + MxBlueskyParameters.model_fields.keys() + | WithSample.model_fields.keys() + | WithVisit.model_fields.keys() + ) + duplicated_robot_load_then_centre_keys = ( + keys_from_outer_load_centre_collect + & values["robot_load_then_centre"].keys() + ) + assert not (duplicated_robot_load_then_centre_keys), ( + f"Unexpected keys in robot_load_then_centre: {', '.join(duplicated_robot_load_then_centre_keys)}" + ) + + duplicated_multi_rotation_scan_keys = ( + keys_from_outer_load_centre_collect & values["multi_rotation_scan"].keys() + ) + assert not (duplicated_multi_rotation_scan_keys), ( + f"Unexpected keys in multi_rotation_scan: {', '.join(duplicated_multi_rotation_scan_keys)}" + ) new_robot_load_then_centre_params = construct_from_values( values, values["robot_load_then_centre"], RobotLoadThenCentre @@ -53,3 +74,22 @@ def validate_model(cls, values): values["multi_rotation_scan"] = new_multi_rotation_scan_params values["robot_load_then_centre"] = new_robot_load_then_centre_params return values + + @model_validator(mode="after") + def _check_rotation_start_xyz_is_not_specified(self) -> Self: + for scan in self.multi_rotation_scan.single_rotation_scans: + assert ( + not scan.x_start_um and not scan.y_start_um and not scan.z_start_um + ), ( + "Specifying start xyz for sweeps is not supported in combination with centring." + ) + return self + + @model_validator(mode="after") + def _check_different_gridscan_and_rotation_energy_not_specified(self) -> Self: + assert ( + self.multi_rotation_scan.demand_energy_ev is None + or self.multi_rotation_scan.demand_energy_ev + == self.robot_load_then_centre.demand_energy_ev + ), "Setting a different energy for gridscan and rotation is not supported." + return self diff --git a/src/mx_bluesky/hyperion/parameters/rotation.py b/src/mx_bluesky/hyperion/parameters/rotation.py index f3e3b7de4..97bf8808a 100644 --- a/src/mx_bluesky/hyperion/parameters/rotation.py +++ b/src/mx_bluesky/hyperion/parameters/rotation.py @@ -3,7 +3,7 @@ import os from collections.abc import Iterator from itertools import accumulate -from typing import Annotated, Any +from typing import Annotated, Any, Self from annotated_types import Len from dodal.devices.aperturescatterguard import ApertureValue @@ -26,6 +26,7 @@ SplitScan, WithScan, ) +from mx_bluesky.hyperion.parameters.components import WithHyperionFeatures from mx_bluesky.hyperion.parameters.constants import ( CONST, I03Constants, @@ -33,6 +34,18 @@ class RotationScanPerSweep(OptionalGonioAngleStarts, OptionalXyzStarts): + """ + Describes a rotation scan about the specified axis. + + Attributes: + rotation_axis: The rotation axis, by default this is the omega axis + omega_start_deg: The initial angle of the rotation in degrees (default 0) + scan_width_deg: The sweep of the rotation in degrees, this must be positive (default 360) + rotation_direction: Indicates the direction of rotation, if RotationDirection.POSITIVE + the final angle is obtained by adding scan_width_deg, otherwise by subtraction (default NEGATIVE) + nexus_vds_start_img: The frame number of the first frame captured during the rotation + """ + omega_start_deg: float = Field(default=0) # type: ignore rotation_axis: RotationAxis = Field(default=RotationAxis.OMEGA) scan_width_deg: float = Field(default=360, gt=0) @@ -40,7 +53,7 @@ class RotationScanPerSweep(OptionalGonioAngleStarts, OptionalXyzStarts): nexus_vds_start_img: int = Field(default=0, ge=0) -class RotationExperiment(DiffractionExperimentWithSample): +class RotationExperiment(DiffractionExperimentWithSample, WithHyperionFeatures): shutter_opening_time_s: float = Field(default=CONST.I03.SHUTTER_TIME_S) rotation_increment_deg: float = Field(default=0.1, gt=0) ispyb_experiment_type: IspybExperimentType = Field( @@ -98,6 +111,7 @@ def detector_params(self): @property def scan_points(self) -> AxesPoints: + """The scan points are defined in application space""" scan_spec = Line( axis="omega", start=self.omega_start_deg, @@ -141,6 +155,17 @@ def correct_start_vds(cls, values: Any) -> Any: start_img += scan.scan_width_deg / values.rotation_increment_deg return values + @model_validator(mode="after") + def _check_valid_for_single_arm_multiple_sweep(self) -> Self: + if len(self.rotation_scans) > 0: + scan_width = self.rotation_scans[0].scan_width_deg + for scan in self.rotation_scans[1:]: + assert scan.scan_width_deg == scan_width, ( + "Sweeps with different numbers of frames are not supported." + ) + + return self + @property def single_rotation_scans(self) -> Iterator[RotationScan]: for scan in self.rotation_scans: diff --git a/src/mx_bluesky/hyperion/utils/context.py b/src/mx_bluesky/hyperion/utils/context.py index f67c4a1b6..66c330f1d 100644 --- a/src/mx_bluesky/hyperion/utils/context.py +++ b/src/mx_bluesky/hyperion/utils/context.py @@ -6,7 +6,7 @@ from dodal.utils import get_beamline_based_on_environment_variable import mx_bluesky.hyperion.experiment_plans as hyperion_plans -from mx_bluesky.hyperion.log import LOGGER +from mx_bluesky.common.utils.log import LOGGER T = TypeVar("T", bound=Device) diff --git a/src/mx_bluesky/hyperion/utils/validation.py b/src/mx_bluesky/hyperion/utils/validation.py index 00c9cf52e..a88afe414 100644 --- a/src/mx_bluesky/hyperion/utils/validation.py +++ b/src/mx_bluesky/hyperion/utils/validation.py @@ -62,7 +62,7 @@ def fake_rotation_scan( @bpp.run_decorator( # attach experiment metadata to the start document md={ "subplan_name": CONST.PLAN.ROTATION_OUTER, - "hyperion_parameters": parameters.model_dump_json(), + "mx_bluesky_parameters": parameters.model_dump_json(), "activate_callbacks": "RotationNexusFileCallback", } ) @@ -79,6 +79,7 @@ def plan(): def fake_create_rotation_devices(): + beamstop = i03.beamstop(fake_with_ophyd_sim=True) eiger = i03.eiger(fake_with_ophyd_sim=True) smargon = i03.smargon(fake_with_ophyd_sim=True) zebra = i03.zebra(fake_with_ophyd_sim=True) @@ -106,6 +107,7 @@ def fake_create_rotation_devices(): return RotationScanComposite( attenuator=attenuator, backlight=backlight, + beamstop=beamstop, dcm=dcm, detector_motion=detector_motion, eiger=eiger, @@ -135,7 +137,7 @@ def sim_rotation_scan_to_create_nexus( fake_create_rotation_devices.eiger.bit_depth.sim_put(32) # type: ignore with patch( - "mx_bluesky.hyperion.external_interaction.nexus.write_nexus.get_start_and_predicted_end_time", + "mx_bluesky.common.external_interaction.nexus.write_nexus.get_start_and_predicted_end_time", return_value=("test_time", "test_time"), ): RE( diff --git a/tests/conftest.py b/tests/conftest.py index 713e788d9..151e93624 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,10 +7,11 @@ import threading from collections.abc import Callable, Generator, Sequence from contextlib import ExitStack +from copy import deepcopy from functools import partial from inspect import get_annotations from typing import Any -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, mock_open, patch import bluesky.plan_stubs as bps import numpy @@ -29,15 +30,17 @@ ApertureScatterguard, ApertureValue, ) -from dodal.devices.attenuator import Attenuator +from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator from dodal.devices.backlight import Backlight from dodal.devices.dcm import DCM from dodal.devices.detector.detector_motion import DetectorMotion from dodal.devices.eiger import EigerDetector from dodal.devices.fast_grid_scan import FastGridScanCommon from dodal.devices.flux import Flux +from dodal.devices.i03.beamstop import Beamstop, BeamstopPositions from dodal.devices.oav.oav_detector import OAV, OAVConfig from dodal.devices.oav.oav_parameters import OAVParameters +from dodal.devices.oav.pin_image_recognition import PinTipDetection from dodal.devices.robot import BartRobot from dodal.devices.s4_slit_gaps import S4SlitGaps from dodal.devices.smargon import Smargon @@ -50,8 +53,13 @@ from dodal.devices.zebra import ArmDemand, Zebra from dodal.devices.zebra_controlled_shutter import ZebraShutter from dodal.devices.zocalo import XrcResult, ZocaloResults +from dodal.devices.zocalo.zocalo_results import ( + ZOCALO_READING_PLAN_NAME, +) from dodal.log import LOGGER as dodal_logger from dodal.log import set_up_all_logging_handlers +from event_model.documents import Event, EventDescriptor, RunStart, RunStop +from ispyb.sp.mxacquisition import MXAcquisition from ophyd.sim import NullStatus from ophyd_async.core import ( AsyncStatus, @@ -65,24 +73,32 @@ from scanspec.core import Path as ScanPath from scanspec.specs import Line +from mx_bluesky.common.external_interaction.callbacks.common.logging_callback import ( + VerbosePlanExecutionLoggingCallback, +) +from mx_bluesky.common.external_interaction.config_server import FeatureFlags +from mx_bluesky.common.parameters.constants import ( + DocDescriptorNames, + EnvironmentConstants, + PlanNameConstants, + TriggerConstants, +) from mx_bluesky.common.parameters.gridscan import GridScanWithEdgeDetect -from mx_bluesky.common.utils.log import _get_logging_dir, do_default_logging_setup +from mx_bluesky.common.utils.log import ( + ALL_LOGGERS, + ISPYB_ZOCALO_CALLBACK_LOGGER, + LOGGER, + NEXUS_LOGGER, + _get_logging_dir, + do_default_logging_setup, +) from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import ( FlyScanXRayCentreComposite, ) from mx_bluesky.hyperion.experiment_plans.rotation_scan_plan import ( RotationScanComposite, ) -from mx_bluesky.hyperion.external_interaction.callbacks.logging_callback import ( - VerbosePlanExecutionLoggingCallback, -) from mx_bluesky.hyperion.external_interaction.config_server import HyperionFeatureFlags -from mx_bluesky.hyperion.log import ( - ALL_LOGGERS, - ISPYB_LOGGER, - LOGGER, - NEXUS_LOGGER, -) from mx_bluesky.hyperion.parameters.gridscan import ( HyperionThreeDGridScan, ) @@ -98,10 +114,10 @@ def raw_params_from_file(filename): return json.loads(f.read()) -def default_raw_params(): - return raw_params_from_file( - "tests/test_data/parameter_json_files/test_gridscan_param_defaults.json" - ) +def default_raw_params( + json_file="tests/test_data/parameter_json_files/good_test_parameters.json", +): + return raw_params_from_file(json_file) def create_dummy_scan_spec(x_steps, y_steps, z_steps): @@ -118,7 +134,7 @@ def _reset_loggers(loggers): """Clear all handlers and tear down the logging hierarchy, leave logger references intact.""" clear_log_handlers(loggers) for logger in loggers: - if logger.name != "Hyperion": + if logger.name != "Hyperion" and logger.name != "MX-Bluesky": # Hyperion parent is configured on module import, do not remove logger.parent = logging.getLogger() @@ -137,10 +153,10 @@ def pytest_runtest_setup(item): if dodal_logger.handlers == []: print("Initialising Hyperion logger for tests") do_default_logging_setup("dev_log.py", TEST_GRAYLOG_PORT, dev_mode=True) - if ISPYB_LOGGER.handlers == []: + if ISPYB_ZOCALO_CALLBACK_LOGGER.handlers == []: print("Initialising ISPyB logger for tests") set_up_all_logging_handlers( - ISPYB_LOGGER, + ISPYB_ZOCALO_CALLBACK_LOGGER, _get_logging_dir(), "hyperion_ispyb_callback.log", True, @@ -211,6 +227,17 @@ def patch_async_motor( return callback_on_mock_put(motor.user_setpoint, pass_on_mock(motor, call_log)) +@pytest.fixture(params=[False, True]) +def feature_flags_update_with_omega_flip(request): + def update_with_overrides(self): + self.overriden_features["omega_flip"] = request.param + self.omega_flip = request.param + + with patch.object(FeatureFlags, "update_self_from_server", autospec=True) as update: + update.side_effect = update_with_overrides + yield update + + @pytest.fixture def beamline_parameters(): return GDABeamlineParameters.from_file( @@ -417,6 +444,27 @@ async def fake_attenuator_set(val): yield attenuator +@pytest.fixture +def beamstop_i03( + beamline_parameters: GDABeamlineParameters, sim_run_engine: RunEngineSimulator +) -> Generator[Beamstop, Any, Any]: + with patch( + "dodal.beamlines.i03.get_beamline_parameters", return_value=beamline_parameters + ): + beamstop = i03.beamstop(fake_with_ophyd_sim=True) + patch_motor(beamstop.x_mm) + patch_motor(beamstop.y_mm) + patch_motor(beamstop.z_mm) + set_mock_value(beamstop.x_mm.user_readback, 1.52) + set_mock_value(beamstop.y_mm.user_readback, 44.78) + set_mock_value(beamstop.z_mm.user_readback, 30.0) + sim_run_engine.add_read_handler_for( + beamstop.selected_pos, BeamstopPositions.DATA_COLLECTION + ) + yield beamstop + beamline_utils.clear_devices() + + @pytest.fixture def xbpm_feedback(done_status): xbpm = i03.xbpm_feedback(fake_with_ophyd_sim=True) @@ -589,6 +637,7 @@ def test_full_grid_scan_params(): @pytest.fixture() def fake_create_devices( + beamstop_i03: Beamstop, eiger: EigerDetector, smargon: Smargon, zebra: Zebra, @@ -602,6 +651,7 @@ def fake_create_devices( smargon.omega.set = mock_omega_sets devices = { + "beamstop": beamstop_i03, "eiger": eiger, "smargon": smargon, "zebra": zebra, @@ -614,12 +664,13 @@ def fake_create_devices( @pytest.fixture() def fake_create_rotation_devices( + beamstop_i03: Beamstop, eiger: EigerDetector, smargon: Smargon, zebra: Zebra, detector_motion: DetectorMotion, backlight: Backlight, - attenuator: Attenuator, + attenuator: BinaryFilterAttenuator, flux: Flux, undulator: Undulator, aperture_scatterguard: ApertureScatterguard, @@ -636,6 +687,7 @@ def fake_create_rotation_devices( return RotationScanComposite( attenuator=attenuator, backlight=backlight, + beamstop=beamstop_i03, dcm=dcm, detector_motion=detector_motion, eiger=eiger, @@ -925,9 +977,9 @@ def assert_events_and_data_in_order( for event_data_keys in match_data_keys_list: docs = DocumentCapturer.get_docs_from(docs, "event") doc = docs.pop(0)[1]["data"] - assert all( - k in doc.keys() for k in event_data_keys - ), f"One of {event_data_keys=} not in {doc}" + assert all(k in doc.keys() for k in event_data_keys), ( + f"One of {event_data_keys=} not in {doc}" + ) @pytest.fixture @@ -959,6 +1011,27 @@ def pin_tip_edge_data(): return tip_x_px, tip_y_px, top_edge_array, bottom_edge_array +def find_a_pin(pin_tip_detection): + def set_good_position(): + x, y, top_edge_array, bottom_edge_array = pin_tip_edge_data() + set_mock_value(pin_tip_detection.triggered_tip, numpy.array([x, y])) + set_mock_value(pin_tip_detection.triggered_top_edge, top_edge_array) + set_mock_value(pin_tip_detection.triggered_bottom_edge, bottom_edge_array) + return NullStatus() + + return set_good_position + + +@pytest.fixture +def pin_tip_detection_with_found_pin(ophyd_pin_tip_detection: PinTipDetection): + with patch.object( + ophyd_pin_tip_detection, + "trigger", + side_effect=find_a_pin(ophyd_pin_tip_detection), + ): + yield ophyd_pin_tip_detection + + # Prevent pytest from catching exceptions when debugging in vscode so that break on # exception works correctly (see: https://github.com/pytest-dev/pytest/issues/7409) if os.getenv("PYTEST_RAISE", "0") == "1": @@ -992,3 +1065,429 @@ def generate_xrc_result_event(device_name: str, test_results: Sequence[dict]) -> keys = get_annotations(XrcResult).keys() results_by_key = {k: [r[k] for r in test_results] for k in keys} return {f"{device_name}-{k}": numpy.array(v) for k, v in results_by_key.items()} + + +# The remaining code in this conftest is utility for external interaction tests. See https://github.com/DiamondLightSource/mx-bluesky/issues/699 for +# a better organisation of this + + +def default_raw_gridscan_params( + json_file="tests/test_data/parameter_json_files/test_gridscan_param_defaults.json", +): + return raw_params_from_file(json_file) + + +TEST_SESSION_ID = 90 +EXPECTED_START_TIME = "2024-02-08 14:03:59" +EXPECTED_END_TIME = "2024-02-08 14:04:01" +TEST_DATA_COLLECTION_IDS = (12, 13) +TEST_DATA_COLLECTION_GROUP_ID = 34 +TEST_POSITION_ID = 78 +TEST_GRID_INFO_IDS = (56, 57) +TEST_SAMPLE_ID = 364758 +TEST_BARCODE = "12345A" + + +def mx_acquisition_from_conn(mock_ispyb_conn) -> MagicMock: + return mock_ispyb_conn.return_value.__enter__.return_value.mx_acquisition + + +def assert_upsert_call_with(call, param_template, expected: dict): + actual = remap_upsert_columns(list(param_template), call.args[0]) + assert actual == dict(param_template | expected) + + +def remap_upsert_columns(keys: Sequence[str], values: list): + return dict(zip(keys, values, strict=False)) + + +class OavGridSnapshotTestEvents: + test_descriptor_document_oav_snapshot: EventDescriptor = { + "uid": "b5ba4aec-de49-4970-81a4-b4a847391d34", + "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "name": DocDescriptorNames.OAV_GRID_SNAPSHOT_TRIGGERED, + } # type: ignore + test_event_document_oav_snapshot_xy: Event = { + "descriptor": "b5ba4aec-de49-4970-81a4-b4a847391d34", + "time": 1666604299.828203, + "timestamps": {}, + "seq_num": 1, + "uid": "29033ecf-e052-43dd-98af-c7cdd62e8174", + "data": { + "oav-grid_snapshot-top_left_x": 50, + "oav-grid_snapshot-top_left_y": 100, + "oav-grid_snapshot-num_boxes_x": 40, + "oav-grid_snapshot-num_boxes_y": 20, + "oav-microns_per_pixel_x": 1.58, + "oav-microns_per_pixel_y": 1.58, + "oav-beam_centre_i": 517, + "oav-beam_centre_j": 350, + "oav-grid_snapshot-box_width": 0.1 * 1000 / 1.25, # size in pixels + "oav-grid_snapshot-last_path_full_overlay": "test_1_y", + "oav-grid_snapshot-last_path_outer": "test_2_y", + "oav-grid_snapshot-last_saved_path": "test_3_y", + "smargon-omega": 0, + "smargon-x": 0, + "smargon-y": 0, + "smargon-z": 0, + }, + } + test_event_document_oav_snapshot_xz: Event = { + "descriptor": "b5ba4aec-de49-4970-81a4-b4a847391d34", + "time": 1666604299.828203, + "timestamps": {}, + "seq_num": 1, + "uid": "29033ecf-e052-43dd-98af-c7cdd62e8174", + "data": { + "oav-grid_snapshot-top_left_x": 50, + "oav-grid_snapshot-top_left_y": 0, + "oav-grid_snapshot-num_boxes_x": 40, + "oav-grid_snapshot-num_boxes_y": 10, + "oav-grid_snapshot-box_width": 0.1 * 1000 / 1.25, # size in pixels + "oav-grid_snapshot-last_path_full_overlay": "test_1_z", + "oav-grid_snapshot-last_path_outer": "test_2_z", + "oav-grid_snapshot-last_saved_path": "test_3_z", + "oav-microns_per_pixel_x": 1.58, + "oav-microns_per_pixel_y": 1.58, + "oav-beam_centre_i": 517, + "oav-beam_centre_j": 350, + "smargon-omega": -90, + "smargon-x": 0, + "smargon-y": 0, + "smargon-z": 0, + }, + } + + +def dummy_params(): + dummy_params = HyperionThreeDGridScan(**default_raw_gridscan_params()) + return dummy_params + + +def dummy_params_2d(): + raw_params = raw_params_from_file( + "tests/test_data/parameter_json_files/test_gridscan_param_defaults.json" + ) + raw_params["z_steps"] = 1 + return HyperionThreeDGridScan(**raw_params) + + +class TestData(OavGridSnapshotTestEvents): + DUMMY_TIME_STRING: str = "1970-01-01 00:00:00" + GOOD_ISPYB_RUN_STATUS: str = "DataCollection Successful" + BAD_ISPYB_RUN_STATUS: str = "DataCollection Unsuccessful" + test_start_document: RunStart = { # type: ignore + "uid": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "time": 1666604299.6149616, + "versions": {"ophyd": "1.6.4.post76+g0895f9f", "bluesky": "1.8.3"}, + "scan_id": 1, + "plan_type": "generator", + "plan_name": PlanNameConstants.GRIDSCAN_OUTER, + "subplan_name": PlanNameConstants.GRIDSCAN_OUTER, + TriggerConstants.ZOCALO: PlanNameConstants.DO_FGS, + "mx_bluesky_parameters": dummy_params().model_dump_json(), + } + test_gridscan3d_start_document: RunStart = { # type: ignore + "uid": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "time": 1666604299.6149616, + "versions": {"ophyd": "1.6.4.post76+g0895f9f", "bluesky": "1.8.3"}, + "scan_id": 1, + "plan_type": "generator", + "plan_name": "test", + "subplan_name": PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN, + "mx_bluesky_parameters": dummy_params().model_dump_json(), + } + test_gridscan2d_start_document = { + "uid": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "time": 1666604299.6149616, + "versions": {"ophyd": "1.6.4.post76+g0895f9f", "bluesky": "1.8.3"}, + "scan_id": 1, + "plan_type": "generator", + "plan_name": "test", + "subplan_name": PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN, + "mx_bluesky_parameters": dummy_params_2d().model_dump_json(), + } + test_rotation_start_main_document = { + "uid": "2093c941-ded1-42c4-ab74-ea99980fbbfd", + "subplan_name": PlanNameConstants.ROTATION_MAIN, + "zocalo_environment": EnvironmentConstants.ZOCALO_ENV, + } + test_gridscan_outer_start_document = { + "uid": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "time": 1666604299.6149616, + "versions": {"ophyd": "1.6.4.post76+g0895f9f", "bluesky": "1.8.3"}, + "scan_id": 1, + "plan_type": "generator", + "plan_name": PlanNameConstants.GRIDSCAN_OUTER, + "subplan_name": PlanNameConstants.GRIDSCAN_OUTER, + "zocalo_environment": EnvironmentConstants.ZOCALO_ENV, + TriggerConstants.ZOCALO: PlanNameConstants.DO_FGS, + "mx_bluesky_parameters": dummy_params().model_dump_json(), + } + test_rotation_event_document_during_data_collection: Event = { + "descriptor": "bd45c2e5-2b85-4280-95d7-a9a15800a78b", + "time": 2666604299.928203, + "data": { + "aperture_scatterguard-aperture-x": 15, + "aperture_scatterguard-aperture-y": 16, + "aperture_scatterguard-aperture-z": 2, + "aperture_scatterguard-scatterguard-x": 18, + "aperture_scatterguard-scatterguard-y": 19, + "aperture_scatterguard-selected_aperture": ApertureValue.MEDIUM, + "aperture_scatterguard-radius": 50, + "attenuator-actual_transmission": 0.98, + "flux_flux_reading": 9.81, + "dcm-energy_in_kev": 11.105, + }, + "timestamps": {"det1": 1666604299.8220396, "det2": 1666604299.8235943}, + "seq_num": 1, + "uid": "2093c941-ded1-42c4-ab74-ea99980fbbfd", + "filled": {}, + } + test_rotation_stop_main_document: RunStop = { + "run_start": "2093c941-ded1-42c4-ab74-ea99980fbbfd", + "time": 1666604300.0310638, + "uid": "65b2bde5-5740-42d7-9047-e860e06fbe15", + "exit_status": "success", + "reason": "Test succeeded", + "num_events": {"fake_ispyb_params": 1, "primary": 1}, + } + test_run_gridscan_start_document: RunStart = { # type: ignore + "uid": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "time": 1666604299.6149616, + "versions": {"ophyd": "1.6.4.post76+g0895f9f", "bluesky": "1.8.3"}, + "scan_id": 1, + "plan_type": "generator", + "plan_name": PlanNameConstants.GRIDSCAN_AND_MOVE, + "subplan_name": PlanNameConstants.GRIDSCAN_MAIN, + } + test_do_fgs_start_document: RunStart = { # type: ignore + "uid": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "time": 1666604299.6149616, + "versions": {"ophyd": "1.6.4.post76+g0895f9f", "bluesky": "1.8.3"}, + "scan_id": 1, + "plan_type": "generator", + "plan_name": PlanNameConstants.GRIDSCAN_AND_MOVE, + "subplan_name": PlanNameConstants.DO_FGS, + "scan_points": create_dummy_scan_spec(10, 20, 30), + } + test_descriptor_document_oav_rotation_snapshot: EventDescriptor = { + "uid": "c7d698ce-6d49-4c56-967e-7d081f964573", + "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "name": DocDescriptorNames.OAV_ROTATION_SNAPSHOT_TRIGGERED, + } # type: ignore + test_descriptor_document_pre_data_collection: EventDescriptor = { + "uid": "bd45c2e5-2b85-4280-95d7-a9a15800a78b", + "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "name": DocDescriptorNames.HARDWARE_READ_PRE, + } # type: ignore + test_descriptor_document_during_data_collection: EventDescriptor = { + "uid": "bd45c2e5-2b85-4280-95d7-a9a15800a78b", + "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "name": DocDescriptorNames.HARDWARE_READ_DURING, + } # type: ignore + test_descriptor_document_zocalo_hardware: EventDescriptor = { + "uid": "f082901b-7453-4150-8ae5-c5f98bb34406", + "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "name": DocDescriptorNames.ZOCALO_HW_READ, + } # type: ignore + test_event_document_oav_rotation_snapshot: Event = { + "descriptor": "c7d698ce-6d49-4c56-967e-7d081f964573", + "time": 1666604299.828203, + "timestamps": {}, + "seq_num": 1, + "uid": "32d7c25c-c310-4292-ac78-36ce6509be3d", + "data": {"oav-snapshot-last_saved_path": "snapshot_0"}, + } + test_event_document_pre_data_collection: Event = { + "descriptor": "bd45c2e5-2b85-4280-95d7-a9a15800a78b", + "time": 1666604299.828203, + "data": { + "s4_slit_gaps_xgap": 0.1234, + "s4_slit_gaps_ygap": 0.2345, + "synchrotron-synchrotron_mode": SynchrotronMode.USER, + "undulator-current_gap": 1.234, + "smargon-x": 0.158435435, + "smargon-y": 0.023547354, + "smargon-z": 0.00345684712, + "dcm-energy_in_kev": 11.105, + }, + "timestamps": {"det1": 1666604299.8220396, "det2": 1666604299.8235943}, + "seq_num": 1, + "uid": "29033ecf-e052-43dd-98af-c7cdd62e8173", + "filled": {}, + } + test_event_document_during_data_collection: Event = { + "descriptor": "bd45c2e5-2b85-4280-95d7-a9a15800a78b", + "time": 2666604299.928203, + "data": { + "aperture_scatterguard-aperture-x": 15, + "aperture_scatterguard-aperture-y": 16, + "aperture_scatterguard-aperture-z": 2, + "aperture_scatterguard-scatterguard-x": 18, + "aperture_scatterguard-scatterguard-y": 19, + "aperture_scatterguard-selected_aperture": ApertureValue.MEDIUM, + "aperture_scatterguard-radius": 50, + "attenuator-actual_transmission": 1, + "flux_flux_reading": 10, + "dcm-energy_in_kev": 11.105, + "eiger_bit_depth": "16", + }, + "timestamps": { + "det1": 1666604299.8220396, + "det2": 1666604299.8235943, + "eiger_bit_depth": 1666604299.8220396, + }, + "seq_num": 1, + "uid": "29033ecf-e052-43dd-98af-c7cdd62e8174", + "filled": {}, + } + test_event_document_zocalo_hardware: Event = { + "uid": "29033ecf-e052-43dd-98af-c7cdd62e8175", + "time": 1709654583.9770422, + "data": {"eiger_odin_file_writer_id": "test_path"}, + "timestamps": {"eiger_odin_file_writer_id": 1666604299.8220396}, + "seq_num": 1, + "filled": {}, + "descriptor": "f082901b-7453-4150-8ae5-c5f98bb34406", + } + test_stop_document: RunStop = { + "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "time": 1666604300.0310638, + "uid": "65b2bde5-5740-42d7-9047-e860e06fbe15", + "exit_status": "success", + "reason": "", + "num_events": {"fake_ispyb_params": 1, "primary": 1}, + } + test_run_gridscan_stop_document: RunStop = { + "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "time": 1666604300.0310638, + "uid": "65b2bde5-5740-42d7-9047-e860e06fbe15", + "exit_status": "success", + "reason": "", + "num_events": {"fake_ispyb_params": 1, "primary": 1}, + } + test_do_fgs_gridscan_stop_document: RunStop = { + "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "time": 1666604300.0310638, + "uid": "65b2bde5-5740-42d7-9047-e860e06fbe15", + "exit_status": "success", + "reason": "", + "num_events": {"fake_ispyb_params": 1, "primary": 1}, + } + test_failed_stop_document: RunStop = { + "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "time": 1666604300.0310638, + "uid": "65b2bde5-5740-42d7-9047-e860e06fbe15", + "exit_status": "fail", + "reason": "could not connect to devices", + "num_events": {"fake_ispyb_params": 1, "primary": 1}, + } + test_run_gridscan_failed_stop_document: RunStop = { + "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "time": 1666604300.0310638, + "uid": "65b2bde5-5740-42d7-9047-e860e06fbe15", + "exit_status": "fail", + "reason": "could not connect to devices", + "num_events": {"fake_ispyb_params": 1, "primary": 1}, + } + test_descriptor_document_zocalo_reading: EventDescriptor = { + "uid": "unique_id_zocalo_reading", + "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", + "name": ZOCALO_READING_PLAN_NAME, + } # type:ignore + test_zocalo_reading_event: Event = { + "descriptor": "unique_id_zocalo_reading", + "data": generate_xrc_result_event("zocalo", []), + } # type:ignore + + +def _mock_ispyb_conn(base_ispyb_conn, position_id, dcgid, dcids, giids): + def upsert_data_collection(values): + kvpairs = remap_upsert_columns( + list(MXAcquisition.get_data_collection_params()), values + ) + if kvpairs["id"]: + return kvpairs["id"] + else: + return next(upsert_data_collection.i) # pyright: ignore + + mx_acq = base_ispyb_conn.return_value.mx_acquisition + mx_acq.upsert_data_collection.side_effect = upsert_data_collection + mx_acq.update_dc_position.return_value = position_id + mx_acq.upsert_data_collection_group.return_value = dcgid + + def upsert_dc_grid(values): + kvpairs = remap_upsert_columns(list(MXAcquisition.get_dc_grid_params()), values) + if kvpairs["id"]: + return kvpairs["id"] + else: + return next(upsert_dc_grid.i) # pyright: ignore + + upsert_data_collection.i = iter(dcids) # pyright: ignore + upsert_dc_grid.i = iter(giids) # pyright: ignore + + mx_acq.upsert_dc_grid.side_effect = upsert_dc_grid + return base_ispyb_conn + + +@pytest.fixture +def mock_ispyb_conn(base_ispyb_conn): + return _mock_ispyb_conn( + base_ispyb_conn, + TEST_POSITION_ID, + TEST_DATA_COLLECTION_GROUP_ID, + TEST_DATA_COLLECTION_IDS, + TEST_GRID_INFO_IDS, + ) + + +@pytest.fixture +def dummy_rotation_params(): + dummy_params = RotationScan( + **default_raw_params( + "tests/test_data/parameter_json_files/good_test_rotation_scan_parameters.json" + ) + ) + dummy_params.sample_id = TEST_SAMPLE_ID + return dummy_params + + +@pytest.fixture +def base_ispyb_conn(): + with patch("ispyb.open", mock_open()) as ispyb_connection: + mock_mx_acquisition = MagicMock() + mock_mx_acquisition.get_data_collection_group_params.side_effect = ( + lambda: deepcopy(MXAcquisition.get_data_collection_group_params()) + ) + + mock_mx_acquisition.get_data_collection_params.side_effect = lambda: deepcopy( + MXAcquisition.get_data_collection_params() + ) + mock_mx_acquisition.get_dc_position_params.side_effect = lambda: deepcopy( + MXAcquisition.get_dc_position_params() + ) + mock_mx_acquisition.get_dc_grid_params.side_effect = lambda: deepcopy( + MXAcquisition.get_dc_grid_params() + ) + ispyb_connection.return_value.mx_acquisition = mock_mx_acquisition + mock_core = MagicMock() + + def mock_retrieve_visit(visit_str): + assert visit_str, "No visit id supplied" + return TEST_SESSION_ID + + mock_core.retrieve_visit_id.side_effect = mock_retrieve_visit + ispyb_connection.return_value.core = mock_core + yield ispyb_connection + + +@pytest.fixture +def mock_ispyb_conn_multiscan(base_ispyb_conn): + return _mock_ispyb_conn( + base_ispyb_conn, + TEST_POSITION_ID, + TEST_DATA_COLLECTION_GROUP_ID, + list(range(12, 24)), + list(range(56, 68)), + ) diff --git a/tests/system_tests/conftest.py b/tests/system_tests/conftest.py index 4d30ef9d5..89172389d 100644 --- a/tests/system_tests/conftest.py +++ b/tests/system_tests/conftest.py @@ -154,6 +154,10 @@ def oav_for_system_test(test_config_files): ): mock_get.return_value.__aenter__.return_value = empty_response set_mock_value(oav.zoom_controller.level, "1.0") + zoom_levels_list = ["1.0x", "3.0x", "5.0x", "7.5x", "10.0x", "15.0x"] + oav.zoom_controller._get_allowed_zoom_levels = AsyncMock( + return_value=zoom_levels_list + ) yield oav diff --git a/tests/system_tests/hyperion/experiment_plans/test_fgs_plan.py b/tests/system_tests/hyperion/experiment_plans/test_fgs_plan.py index df0b2b977..79ed76711 100644 --- a/tests/system_tests/hyperion/experiment_plans/test_fgs_plan.py +++ b/tests/system_tests/hyperion/experiment_plans/test_fgs_plan.py @@ -13,6 +13,14 @@ from ophyd.sim import NullStatus from ophyd_async.testing import set_mock_value +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( + GridscanISPyBCallback, +) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( + GridscanNexusFileCallback, +) +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import IspybIds +from mx_bluesky.common.utils.exceptions import WarningException from mx_bluesky.hyperion.device_setup_plans.read_hardware_for_setup import ( read_hardware_during_collection, read_hardware_pre_collection, @@ -20,7 +28,6 @@ from mx_bluesky.hyperion.device_setup_plans.xbpm_feedback import ( transmission_and_xbpm_feedback_for_collection_decorator, ) -from mx_bluesky.hyperion.exceptions import WarningException from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import ( FlyScanXRayCentreComposite, flyscan_xray_centre, @@ -28,17 +35,10 @@ from mx_bluesky.hyperion.external_interaction.callbacks.common.callback_util import ( create_gridscan_callbacks, ) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, -) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.nexus_callback import ( - GridscanNexusFileCallback, -) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import IspybIds from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan +from tests.conftest import default_raw_gridscan_params -from ....conftest import default_raw_params from ..external_interaction.conftest import ( # noqa fetch_comment, zocalo_env, @@ -47,7 +47,7 @@ @pytest.fixture def params(): - params = HyperionThreeDGridScan(**default_raw_params()) + params = HyperionThreeDGridScan(**default_raw_gridscan_params()) params.beamline = CONST.SIM.BEAMLINE yield params @@ -55,7 +55,7 @@ def params(): @pytest.fixture() def callbacks(params): with patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter" + "mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter" ): _, ispyb_cb = create_gridscan_callbacks() ispyb_cb.ispyb_config = CONST.SIM.DEV_ISPYB_DATABASE_CFG @@ -298,18 +298,9 @@ def zocalo_trigger(): RE(flyscan_xray_centre(fxc_composite, params)) # The following numbers are derived from the centre returned in fake_zocalo - assert ( - await fxc_composite.sample_motors.x.user_readback.get_value() - == pytest.approx(-1) - ) - assert ( - await fxc_composite.sample_motors.y.user_readback.get_value() - == pytest.approx(-1) - ) - assert ( - await fxc_composite.sample_motors.z.user_readback.get_value() - == pytest.approx(-1) - ) + assert await fxc_composite.smargon.x.user_readback.get_value() == pytest.approx(-1) + assert await fxc_composite.smargon.y.user_readback.get_value() == pytest.approx(-1) + assert await fxc_composite.smargon.z.user_readback.get_value() == pytest.approx(-1) @pytest.mark.s03 @@ -336,15 +327,12 @@ async def test_complete_xray_centre_plan_with_callbacks_moves_to_centre( RE(flyscan_xray_centre(fxc_composite, params)) # The following numbers are derived from the centre returned in fake_zocalo - assert ( - await fxc_composite.sample_motors.x.user_readback.get_value() - == pytest.approx(0.05) + assert await fxc_composite.smargon.x.user_readback.get_value() == pytest.approx( + 0.05 ) - assert ( - await fxc_composite.sample_motors.y.user_readback.get_value() - == pytest.approx(0.15) + assert await fxc_composite.smargon.y.user_readback.get_value() == pytest.approx( + 0.15 ) - assert ( - await fxc_composite.sample_motors.z.user_readback.get_value() - == pytest.approx(0.25) + assert await fxc_composite.smargon.z.user_readback.get_value() == pytest.approx( + 0.25 ) 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 ba9b0995e..20b930e9e 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 @@ -21,6 +21,8 @@ ) from zmq.utils.monitor import recv_monitor_message +from mx_bluesky.common.utils.log import LOGGER +from mx_bluesky.common.utils.utils import convert_angstrom_to_eV from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import ( FlyScanXRayCentreComposite, flyscan_xray_centre, @@ -29,11 +31,9 @@ RotationScanComposite, rotation_scan, ) -from mx_bluesky.hyperion.log import LOGGER from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan from mx_bluesky.hyperion.parameters.rotation import RotationScan -from mx_bluesky.hyperion.utils.utils import convert_angstrom_to_eV from .....conftest import fake_read from ..conftest import ( # noqa @@ -213,6 +213,7 @@ def test_remote_callbacks_write_to_dev_ispyb_for_rotation( aperture_scatterguard=aperture_scatterguard, attenuator=attenuator, backlight=fake_create_devices["backlight"], + beamstop=fake_create_devices["beamstop"], dcm=fake_create_devices["dcm"], detector_motion=fake_create_devices["detector_motion"], eiger=fake_create_devices["eiger"], diff --git a/tests/system_tests/hyperion/external_interaction/conftest.py b/tests/system_tests/hyperion/external_interaction/conftest.py index 0c45557fc..d630f86d1 100644 --- a/tests/system_tests/hyperion/external_interaction/conftest.py +++ b/tests/system_tests/hyperion/external_interaction/conftest.py @@ -9,12 +9,13 @@ import pytest import pytest_asyncio from dodal.devices.aperturescatterguard import ApertureScatterguard -from dodal.devices.attenuator import Attenuator +from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator from dodal.devices.backlight import Backlight from dodal.devices.dcm import DCM from dodal.devices.detector.detector_motion import DetectorMotion from dodal.devices.eiger import EigerDetector from dodal.devices.flux import Flux +from dodal.devices.i03.beamstop import Beamstop from dodal.devices.oav.oav_detector import OAV from dodal.devices.oav.pin_image_recognition import PinTipDetection from dodal.devices.robot import BartRobot @@ -39,6 +40,8 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import StoreInIspyb +from mx_bluesky.common.utils.utils import convert_angstrom_to_eV from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import ( FlyScanXRayCentreComposite, ) @@ -48,10 +51,8 @@ from mx_bluesky.hyperion.experiment_plans.rotation_scan_plan import ( RotationScanComposite, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import StoreInIspyb from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan -from mx_bluesky.hyperion.utils.utils import convert_angstrom_to_eV from ....conftest import fake_read, pin_tip_edge_data, raw_params_from_file @@ -267,6 +268,7 @@ async def mock_zocalo_complete(): def grid_detect_then_xray_centre_composite( fast_grid_scan, backlight, + beamstop_i03, smargon, undulator_for_system_test, synchrotron, @@ -291,6 +293,7 @@ def grid_detect_then_xray_centre_composite( zebra_fast_grid_scan=fast_grid_scan, pin_tip_detection=ophyd_pin_tip_detection, backlight=backlight, + beamstop=beamstop_i03, panda_fast_grid_scan=panda_fast_grid_scan, smargon=smargon, undulator=undulator_for_system_test, @@ -390,12 +393,13 @@ async def no_pin_tip_found(): @pytest.fixture def composite_for_rotation_scan( + beamstop_i03: Beamstop, eiger: EigerDetector, smargon: Smargon, zebra: Zebra, detector_motion: DetectorMotion, backlight: Backlight, - attenuator: Attenuator, + attenuator: BinaryFilterAttenuator, flux: Flux, undulator_for_system_test: Undulator, aperture_scatterguard: ApertureScatterguard, @@ -415,6 +419,7 @@ def composite_for_rotation_scan( fake_create_rotation_devices = RotationScanComposite( attenuator=attenuator, backlight=backlight, + beamstop=beamstop_i03, dcm=dcm, detector_motion=detector_motion, eiger=eiger, diff --git a/tests/system_tests/hyperion/external_interaction/test_exp_eye_dev.py b/tests/system_tests/hyperion/external_interaction/test_exp_eye_dev.py index b8578acda..e426df7e3 100644 --- a/tests/system_tests/hyperion/external_interaction/test_exp_eye_dev.py +++ b/tests/system_tests/hyperion/external_interaction/test_exp_eye_dev.py @@ -5,7 +5,7 @@ import pytest from requests import get -from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import ( +from mx_bluesky.common.external_interaction.ispyb.exp_eye_store import ( BLSampleStatus, ExpeyeInteraction, ) diff --git a/tests/system_tests/hyperion/external_interaction/test_ispyb_dev_connection.py b/tests/system_tests/hyperion/external_interaction/test_ispyb_dev_connection.py index 5d324502d..fc2f1479f 100644 --- a/tests/system_tests/hyperion/external_interaction/test_ispyb_dev_connection.py +++ b/tests/system_tests/hyperion/external_interaction/test_ispyb_dev_connection.py @@ -10,40 +10,40 @@ from dodal.devices.oav.oav_parameters import OAVParameters from dodal.devices.synchrotron import SynchrotronMode -from mx_bluesky.common.parameters.components import IspybExperimentType -from mx_bluesky.common.parameters.gridscan import GridScanWithEdgeDetect -from mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan import ( - GridDetectThenXRayCentreComposite, - grid_detect_then_xray_centre, -) -from mx_bluesky.hyperion.experiment_plans.rotation_scan_plan import ( - RotationScanComposite, - rotation_scan, -) -from mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping import ( +from mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping import ( populate_data_collection_group, populate_remaining_data_collection_info, ) -from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_callback import ( - RotationISPyBCallback, -) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( GridscanISPyBCallback, ) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_mapping import ( +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping import ( construct_comment_for_gridscan, populate_xy_data_collection_info, populate_xz_data_collection_info, ) -from mx_bluesky.hyperion.external_interaction.ispyb.data_model import ( +from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionGridInfo, Orientation, ScanDataInfo, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import ( +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( IspybIds, StoreInIspyb, ) +from mx_bluesky.common.parameters.components import IspybExperimentType +from mx_bluesky.common.parameters.gridscan import GridScanWithEdgeDetect +from mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan import ( + GridDetectThenXRayCentreComposite, + grid_detect_then_xray_centre, +) +from mx_bluesky.hyperion.experiment_plans.rotation_scan_plan import ( + RotationScanComposite, + rotation_scan, +) +from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_callback import ( + RotationISPyBCallback, +) from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.gridscan import ( HyperionThreeDGridScan, @@ -249,11 +249,11 @@ def test_ispyb_deposition_comment_correct_for_3D_on_failure( dummy_ispyb_3d.end_deposition(ispyb_ids, "fail", "could not connect to devices") assert ( fetch_comment(dcid1) - == "Hyperion: Xray centring - Diffraction grid scan of 40 by 20 images in 100.0 um by 100.0 um steps. Top left (px): [100,100], bottom right (px): [3300,1700]. DataCollection Unsuccessful reason: could not connect to devices" + == "MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 20 images in 100.0 um by 100.0 um steps. Top left (px): [100,100], bottom right (px): [3300,1700]. DataCollection Unsuccessful reason: could not connect to devices" ) assert ( fetch_comment(dcid2) - == "Hyperion: Xray centring - Diffraction grid scan of 40 by 10 images in 100.0 um by 100.0 um steps. Top left (px): [100,50], bottom right (px): [3300,850]. DataCollection Unsuccessful reason: could not connect to devices" + == "MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 10 images in 100.0 um by 100.0 um steps. Top left (px): [100,50], bottom right (px): [3300,850]. DataCollection Unsuccessful reason: could not connect to devices" ) @@ -291,11 +291,11 @@ def test_can_store_2D_ispyb_data_correctly_when_in_error( expected_comments = [ ( - "Hyperion: Xray centring - Diffraction grid scan of 40 by 20 " + "MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 20 " "images in 100.0 um by 100.0 um steps. Top left (px): [100,100], bottom right (px): [3300,1700]." ), ( - "Hyperion: Xray centring - Diffraction grid scan of 40 by 10 " + "MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 10 " "images in 100.0 um by 100.0 um steps. Top left (px): [100,50], bottom right (px): [3300,850]." ), ] @@ -378,9 +378,9 @@ def test_ispyb_deposition_in_gridscan( compare_comment( fetch_datacollection_attribute, ispyb_ids.data_collection_ids[0], - "Hyperion: Xray centring - Diffraction grid scan of 20 by 12 " + "MX-Bluesky: Xray centring - Diffraction grid scan of 20 by 12 " "images in 20.0 um by 20.0 um steps. Top left (px): [100,161], " - "bottom right (px): [239,244]. ApertureValue.SMALL. ", + "bottom right (px): [239,244]. Small. ", ) compare_actual_and_expected( ispyb_ids.data_collection_ids[0], @@ -432,9 +432,9 @@ def test_ispyb_deposition_in_gridscan( compare_comment( fetch_datacollection_attribute, ispyb_ids.data_collection_ids[1], - "Hyperion: Xray centring - Diffraction grid scan of 20 by 11 " + "MX-Bluesky: Xray centring - Diffraction grid scan of 20 by 11 " "images in 20.0 um by 20.0 um steps. Top left (px): [100,165], " - "bottom right (px): [239,241]. ApertureValue.SMALL. ", + "bottom right (px): [239,241]. Small. ", ) position_id = fetch_datacollection_attribute( ispyb_ids.data_collection_ids[1], DATA_COLLECTION_COLUMN_MAP["positionid"] @@ -465,6 +465,7 @@ def test_ispyb_deposition_in_rotation_plan( fetch_comment: Callable[..., Any], fetch_datacollection_attribute: Callable[..., Any], fetch_datacollection_position_attribute: Callable[..., Any], + feature_flags_update_with_omega_flip, ): os.environ["ISPYB_CONFIG_PATH"] = CONST.SIM.DEV_ISPYB_DATABASE_CFG ispyb_cb = RotationISPyBCallback() @@ -481,8 +482,7 @@ def test_ispyb_deposition_in_rotation_plan( dcid = ispyb_cb.ispyb_ids.data_collection_ids[0] assert dcid is not None assert ( - fetch_comment(dcid) - == "Sample position (µm): (1, 2, 3) test Aperture: ApertureValue.SMALL. " + fetch_comment(dcid) == "Sample position (µm): (1, 2, 3) test Aperture: Small. " ) expected_values = EXPECTED_DATACOLLECTION_FOR_ROTATION | { 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 b7f093120..be5a0697a 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 @@ -15,7 +15,11 @@ from ophyd_async.core import AsyncStatus from ophyd_async.testing import set_mock_value -from mx_bluesky.hyperion.exceptions import WarningException +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( + GridscanISPyBCallback, +) +from mx_bluesky.common.utils.exceptions import WarningException +from mx_bluesky.hyperion.device_setup_plans.check_beamstop import BeamstopException from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import ( CrystalNotFoundException, ) @@ -32,9 +36,6 @@ from mx_bluesky.hyperion.external_interaction.callbacks.sample_handling.sample_handling_callback import ( SampleHandlingCallback, ) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, -) from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect @@ -57,6 +58,7 @@ def load_centre_collect_params(): @pytest.fixture def load_centre_collect_composite( grid_detect_then_xray_centre_composite, + beamstop_i03, composite_for_rotation_scan, thawer, vfm, @@ -69,6 +71,7 @@ def load_centre_collect_composite( aperture_scatterguard=composite_for_rotation_scan.aperture_scatterguard, attenuator=composite_for_rotation_scan.attenuator, backlight=composite_for_rotation_scan.backlight, + beamstop=beamstop_i03, dcm=composite_for_rotation_scan.dcm, detector_motion=composite_for_rotation_scan.detector_motion, eiger=grid_detect_then_xray_centre_composite.eiger, @@ -271,16 +274,16 @@ def test_execute_load_centre_collect_full( compare_comment( fetch_datacollection_attribute, ispyb_gridscan_cb.ispyb_ids.data_collection_ids[0], - "Hyperion: Xray centring - Diffraction grid scan of 30 by 6 " + "MX-Bluesky: Xray centring - Diffraction grid scan of 30 by 6 " "images in 20.0 um by 20.0 um steps. Top left (px): [130,130], " - "bottom right (px): [874,278]. Aperture: ApertureValue.SMALL. ", + "bottom right (px): [874,278]. Aperture: Small. ", ) compare_comment( fetch_datacollection_attribute, ispyb_gridscan_cb.ispyb_ids.data_collection_ids[1], - "Hyperion: Xray centring - Diffraction grid scan of 30 by 6 " + "MX-Bluesky: Xray centring - Diffraction grid scan of 30 by 6 " "images in 20.0 um by 20.0 um steps. Top left (px): [130,130], " - "bottom right (px): [874,278]. Aperture: ApertureValue.SMALL. ", + "bottom right (px): [874,278]. Aperture: Small. ", ) rotation_dcg_id = ispyb_rotation_cb.ispyb_ids.data_collection_group_id @@ -304,7 +307,7 @@ def test_execute_load_centre_collect_full( compare_comment( fetch_datacollection_attribute, ispyb_rotation_cb.ispyb_ids.data_collection_ids[0], - "Sample position (µm): (-2309, -591, 341) Hyperion Rotation Scan - Aperture: ApertureValue.SMALL. ", + "Sample position (µm): (-2309, -591, 341) Hyperion Rotation Scan - Aperture: Small. ", ) assert fetch_blsample(SAMPLE_ID).blSampleStatus == "LOADED" # type: ignore @@ -370,6 +373,30 @@ def test_load_centre_collect_updates_bl_sample_status_pin_tip_detection_fail( assert fetch_blsample(SAMPLE_ID).blSampleStatus == "ERROR - sample" +@pytest.mark.s03 +def test_load_centre_collect_updates_bl_sample_status_no_beamstop( + load_centre_collect_composite: LoadCentreCollectComposite, + load_centre_collect_params: LoadCentreCollect, + oav_parameters_for_rotation: OAVParameters, + RE: RunEngine, + fetch_blsample: Callable[..., Any], +): + sample_handling_cb = SampleHandlingCallback() + RE.subscribe(sample_handling_cb) + set_mock_value(load_centre_collect_composite.beamstop.x_mm.user_readback, 1) + + with pytest.raises(BeamstopException, match="Beamstop is not DATA_COLLECTION"): + RE( + load_centre_collect_full( + load_centre_collect_composite, + load_centre_collect_params, + oav_parameters_for_rotation, + ) + ) + + assert fetch_blsample(SAMPLE_ID).blSampleStatus == "ERROR - beamline" + + @pytest.mark.s03 def test_load_centre_collect_updates_bl_sample_status_grid_detection_fail_tip_not_found( load_centre_collect_composite: LoadCentreCollectComposite, diff --git a/tests/system_tests/hyperion/external_interaction/test_nexgen.py b/tests/system_tests/hyperion/external_interaction/test_nexgen.py index b8f07ab4a..a15677921 100644 --- a/tests/system_tests/hyperion/external_interaction/test_nexgen.py +++ b/tests/system_tests/hyperion/external_interaction/test_nexgen.py @@ -78,7 +78,7 @@ def test_rotation_nexgen( RE = RunEngine({}) with patch( - "mx_bluesky.hyperion.external_interaction.nexus.write_nexus.get_start_and_predicted_end_time", + "mx_bluesky.common.external_interaction.nexus.write_nexus.get_start_and_predicted_end_time", return_value=("test_time", "test_time"), ): RE( @@ -115,9 +115,9 @@ def _check_nexgen_output_passes_imginfo(test_file, reference_file): continue if HEADER_PATTERN.match(actual_line): break - assert ( - actual_line == expected_line - ), f"Header line {i} didn't match contents of {reference_file}: {actual_line} <-> {expected_line}" + assert actual_line == expected_line, ( + f"Header line {i} didn't match contents of {reference_file}: {actual_line} <-> {expected_line}" + ) while True: i += 1 @@ -125,9 +125,9 @@ def _check_nexgen_output_passes_imginfo(test_file, reference_file): actual_line = next(it_actual_lines) if DATE_PATTERN.match(actual_line): continue - assert ( - actual_line == expected_line - ), f"Header line {i} didn't match contents of {reference_file}: {actual_line} <-> {expected_line}" + assert actual_line == expected_line, ( + f"Header line {i} didn't match contents of {reference_file}: {actual_line} <-> {expected_line}" + ) except StopIteration: pass @@ -140,9 +140,9 @@ def _run_imginfo(filename): ["utility_scripts/run_imginfo.sh", filename], text=True, capture_output=True ) assert process.returncode != 2, "imginfo is not available" - assert ( - process.returncode == 0 - ), f"imginfo failed with returncode {process.returncode}" + assert process.returncode == 0, ( + f"imginfo failed with returncode {process.returncode}" + ) return process.stdout, process.stderr @@ -157,7 +157,7 @@ def _fake_rotation_scan( @bpp.run_decorator( # attach experiment metadata to the start document md={ "subplan_name": CONST.PLAN.ROTATION_OUTER, - "hyperion_parameters": parameters.model_dump_json(), + "mx_bluesky_parameters": parameters.model_dump_json(), "activate_callbacks": "RotationNexusFileCallback", } ) diff --git a/tests/system_tests/hyperion/external_interaction/test_zocalo_system.py b/tests/system_tests/hyperion/external_interaction/test_zocalo_system.py index e722eab16..9d3b73af5 100644 --- a/tests/system_tests/hyperion/external_interaction/test_zocalo_system.py +++ b/tests/system_tests/hyperion/external_interaction/test_zocalo_system.py @@ -8,6 +8,9 @@ from bluesky.run_engine import RunEngine from dodal.devices.zocalo import ZOCALO_READING_PLAN_NAME, ZocaloResults +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( + ispyb_activation_wrapper, +) from mx_bluesky.common.parameters.constants import ( EnvironmentConstants, PlanNameConstants, @@ -15,9 +18,6 @@ from mx_bluesky.hyperion.external_interaction.callbacks.common.callback_util import ( create_gridscan_callbacks, ) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( - ispyb_activation_wrapper, -) from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan from tests.conftest import create_dummy_scan_spec @@ -80,7 +80,7 @@ def trigger_zocalo_after_fast_grid_scan(): "subplan_name": CONST.PLAN.GRIDSCAN_OUTER, CONST.TRIGGER.ZOCALO: PlanNameConstants.DO_FGS, "zocalo_environment": EnvironmentConstants.ZOCALO_ENV, - "hyperion_parameters": dummy_params.model_dump_json(), + "mx_bluesky_parameters": dummy_params.model_dump_json(), } ) def inner_plan(): diff --git a/tests/system_tests/hyperion/test_aperturescatterguard_system.py b/tests/system_tests/hyperion/test_aperturescatterguard_system.py index b92499b03..c05a82ce8 100644 --- a/tests/system_tests/hyperion/test_aperturescatterguard_system.py +++ b/tests/system_tests/hyperion/test_aperturescatterguard_system.py @@ -11,7 +11,7 @@ from ophyd_async.core import DeviceCollector from mx_bluesky.hyperion.experiment_plans.change_aperture_then_move_plan import ( - set_aperture_for_bbox_size, + set_aperture_for_bbox_mm, ) @@ -29,7 +29,17 @@ def ap_sg(): @pytest.mark.s03() -def test_aperture_change_callback(ap_sg: ApertureScatterguard): +@pytest.mark.parametrize( + "bbox, expected_aperture", + [ + ([0.05, 0.05, 0.05], "LARGE_APERTURE"), + ([0.02, 0.02, 0.02], "MEDIUM_APERTURE"), + ], + ids=["large_aperture", "medium_aperture"], +) +def test_aperture_change_callback( + ap_sg: ApertureScatterguard, bbox: list[float], expected_aperture: str +): from bluesky.run_engine import RunEngine from mx_bluesky.hyperion.external_interaction.callbacks.aperture_change_callback import ( @@ -39,5 +49,5 @@ def test_aperture_change_callback(ap_sg: ApertureScatterguard): cb = ApertureChangeCallback() RE = RunEngine({}) RE.subscribe(cb) - RE(set_aperture_for_bbox_size(ap_sg, [2, 2, 2])) - assert cb.last_selected_aperture == "LARGE_APERTURE" + RE(set_aperture_for_bbox_mm(ap_sg, bbox)) + assert cb.last_selected_aperture == expected_aperture diff --git a/tests/test_data/parameter_json_files/example_load_centre_collect_params.json b/tests/test_data/parameter_json_files/example_load_centre_collect_params.json index 5b827f041..07db1cdf1 100644 --- a/tests/test_data/parameter_json_files/example_load_centre_collect_params.json +++ b/tests/test_data/parameter_json_files/example_load_centre_collect_params.json @@ -19,7 +19,7 @@ "comment": "Hyperion Rotation Scan - ", "file_name": "protk", "storage_directory": "/tmp/dls/i03/data/2024/cm31105-4/auto/123457/", - "demand_energy_ev": 11200, + "demand_energy_ev": 11100, "exposure_time_s": 0.004, "rotation_increment_deg": 0.1, "snapshot_omegas_deg": [ diff --git a/tests/test_data/parameter_json_files/good_test_load_centre_collect_params.json b/tests/test_data/parameter_json_files/good_test_load_centre_collect_params.json index 0fff145e4..4b064ebef 100644 --- a/tests/test_data/parameter_json_files/good_test_load_centre_collect_params.json +++ b/tests/test_data/parameter_json_files/good_test_load_centre_collect_params.json @@ -35,10 +35,7 @@ "scan_width_deg": 180.0, "omega_start_deg": 0, "phi_start_deg": 0.47, - "chi_start_deg": 23.85, - "x_start_um": 1.0, - "y_start_um": 2.0, - "z_start_um": 3.0 + "chi_start_deg": 23.85 }] } } diff --git a/tests/test_data/parameter_json_files/good_test_load_centre_collect_params_multi_rotation.json b/tests/test_data/parameter_json_files/good_test_load_centre_collect_params_multi_rotation.json new file mode 100644 index 000000000..72d44f500 --- /dev/null +++ b/tests/test_data/parameter_json_files/good_test_load_centre_collect_params_multi_rotation.json @@ -0,0 +1,51 @@ +{ + "parameter_model_version": "5.0.0", + "beamline": "BL03S", + "det_dist_to_beam_converter_path": "tests/test_data/test_lookup_table.txt", + "insertion_prefix": "SR03S", + "visit": "cm31105-4", + "detector_distance_mm": 255, + "sample_id": 12345, + "sample_puck": 40, + "sample_pin": 3, + "robot_load_then_centre": { + "storage_directory": "/tmp/dls/i03/data/2024/cm31105-4/auto/123458/xraycentring", + "file_name": "robot_load_centring_file", + "comment": "Robot load and centre", + "exposure_time_s": 0.004, + "use_roi_mode": false, + "demand_energy_ev": 11100, + "run_number": 0 + }, + "multi_rotation_scan": { + "comment": "Rotation", + "storage_directory": "/tmp/dls/i03/data/2024/cm31105-4/auto/123458/", + "file_name": "file_name", + "exposure_time_s": 0.004, + "selected_aperture": "SMALL_APERTURE", + "transmission_frac": 1.0, + "demand_energy_ev": 11100, + "rotation_increment_deg": 0.1, + "shutter_opening_time_s": 0.6, + "snapshot_omegas_deg": [0, 90, 180, 270], + "run_number": 1, + "rotation_scans": [ + { + "rotation_axis": "omega", + "rotation_direction": "Negative", + "scan_width_deg": 180.0, + "omega_start_deg": 0, + "phi_start_deg": 0.47, + "chi_start_deg": 23.85 + }, + { + "rotation_axis": "omega", + "rotation_direction": "Positive", + "scan_width_deg": 180.0, + "omega_start_deg": 0, + "phi_start_deg": 0.47, + "chi_start_deg": 0 + } + ] + } +} diff --git a/tests/test_data/parameter_json_files/good_test_multi_rotation_scan_parameters.json b/tests/test_data/parameter_json_files/good_test_multi_rotation_scan_parameters.json index a69313a94..3142570f6 100644 --- a/tests/test_data/parameter_json_files/good_test_multi_rotation_scan_parameters.json +++ b/tests/test_data/parameter_json_files/good_test_multi_rotation_scan_parameters.json @@ -28,7 +28,7 @@ },{ "rotation_axis": "omega", "rotation_direction": "Negative", - "scan_width_deg": 90.0, + "scan_width_deg": 180.0, "omega_start_deg": 180.0, "phi_start_deg": 0.47, "chi_start_deg": 4.7, @@ -38,7 +38,7 @@ },{ "rotation_axis": "omega", "rotation_direction": "Positive", - "scan_width_deg": 360.0, + "scan_width_deg": 180.0, "omega_start_deg": 270.0, "phi_start_deg": 0.47, "chi_start_deg": 45, diff --git a/tests/unit_tests/beamlines/i24/serial/conftest.py b/tests/unit_tests/beamlines/i24/serial/conftest.py index 4d0ac7818..08b3f6bfe 100644 --- a/tests/unit_tests/beamlines/i24/serial/conftest.py +++ b/tests/unit_tests/beamlines/i24/serial/conftest.py @@ -1,7 +1,6 @@ from __future__ import annotations from pathlib import Path -from unittest.mock import patch import pytest from dodal.beamlines import i24 @@ -56,11 +55,7 @@ def dummy_params_without_pp(): "checker_pattern": False, "chip_map": [1], } - with patch( - "mx_bluesky.beamlines.i24.serial.parameters.experiment_parameters.BEAM_CENTER_LUT_FILES", - new=TEST_LUT, - ): - yield FixedTargetParameters(**params) + return FixedTargetParameters(**params) @pytest.fixture @@ -76,11 +71,7 @@ def dummy_params_ex(): "num_images": 10, "pump_status": False, } - with patch( - "mx_bluesky.beamlines.i24.serial.parameters.experiment_parameters.BEAM_CENTER_LUT_FILES", - new=TEST_LUT, - ): - yield ExtruderParameters(**params) + return ExtruderParameters(**params) def patch_motor(motor: Motor, initial_position: float = 0): diff --git a/tests/unit_tests/beamlines/i24/serial/extruder/test_extruder_collect.py b/tests/unit_tests/beamlines/i24/serial/extruder/test_extruder_collect.py index bcd18007b..f3cc5e424 100644 --- a/tests/unit_tests/beamlines/i24/serial/extruder/test_extruder_collect.py +++ b/tests/unit_tests/beamlines/i24/serial/extruder/test_extruder_collect.py @@ -4,7 +4,6 @@ import pytest from dodal.devices.zebra import DISCONNECT, SOFT_IN3 from ophyd_async.testing import get_mock_put, set_mock_value -from tests.unit_tests.beamlines.i24.serial.conftest import TEST_LUT from mx_bluesky.beamlines.i24.serial.extruder.i24ssx_Extruder_Collect_py3v2 import ( TTL_EIGER, @@ -21,6 +20,8 @@ from mx_bluesky.beamlines.i24.serial.parameters import BeamSettings, ExtruderParameters from mx_bluesky.beamlines.i24.serial.setup_beamline import Eiger, Pilatus +from ..conftest import TEST_LUT + @pytest.fixture def dummy_params(): @@ -35,11 +36,7 @@ def dummy_params(): "num_images": 10, "pump_status": False, } - with patch( - "mx_bluesky.beamlines.i24.serial.parameters.experiment_parameters.BEAM_CENTER_LUT_FILES", - new=TEST_LUT, - ): - yield ExtruderParameters(**params) + return ExtruderParameters(**params) @pytest.fixture @@ -57,11 +54,7 @@ def dummy_params_pp(): "laser_dwell_s": 0.01, "laser_delay_s": 0.005, } - with patch( - "mx_bluesky.beamlines.i24.serial.parameters.experiment_parameters.BEAM_CENTER_LUT_FILES", - new=TEST_LUT, - ): - yield ExtruderParameters(**params_pp) + return ExtruderParameters(**params_pp) @pytest.fixture @@ -213,22 +206,26 @@ def test_run_extruder_quickshot_with_eiger( fake_generator(1702), fake_generator(0), # zebra disarm ] - RE( - main_extruder_plan( - zebra, - aperture, - backlight, - beamstop, - detector_stage, - shutter, - dcm, - mirrors, - eiger_beam_center, - dummy_params, - fake_dcid, - fake_start_time, + with patch( + "mx_bluesky.beamlines.i24.serial.extruder.i24ssx_Extruder_Collect_py3v2.BEAM_CENTER_LUT_FILES", + new=TEST_LUT, + ): + RE( + main_extruder_plan( + zebra, + aperture, + backlight, + beamstop, + detector_stage, + shutter, + dcm, + mirrors, + eiger_beam_center, + dummy_params, + fake_dcid, + fake_start_time, + ) ) - ) fake_nexgen.assert_called_once_with( None, dummy_params, 0.6, (1605, 1702), fake_start_time ) @@ -281,22 +278,26 @@ def test_run_extruder_pump_probe_with_pilatus( # Mock end of data collection (zebra disarmed) fake_read.side_effect = [fake_generator(0)] mock_pilatus_temp.side_effect = [fake_generator("test_00001_#####.cbf")] - RE( - main_extruder_plan( - zebra, - aperture, - backlight, - beamstop, - detector_stage, - shutter, - dcm, - mirrors, - pilatus_beam_center, - dummy_params_pp, - fake_dcid, - fake_start_time, + with patch( + "mx_bluesky.beamlines.i24.serial.extruder.i24ssx_Extruder_Collect_py3v2.BEAM_CENTER_LUT_FILES", + new=TEST_LUT, + ): + RE( + main_extruder_plan( + zebra, + aperture, + backlight, + beamstop, + detector_stage, + shutter, + dcm, + mirrors, + pilatus_beam_center, + dummy_params_pp, + fake_dcid, + fake_start_time, + ) ) - ) mock_pilatus_temp.assert_called_once() assert fake_dcid.generate_dcid.call_count == 1 assert fake_dcid.notify_start.call_count == 1 diff --git a/tests/unit_tests/beamlines/i24/serial/fixed_target/conftest.py b/tests/unit_tests/beamlines/i24/serial/fixed_target/conftest.py index 577c35933..6a73ba4e6 100644 --- a/tests/unit_tests/beamlines/i24/serial/fixed_target/conftest.py +++ b/tests/unit_tests/beamlines/i24/serial/fixed_target/conftest.py @@ -1,5 +1,4 @@ from pathlib import Path -from unittest.mock import patch import pytest @@ -37,8 +36,4 @@ def dummy_params_with_pp(): "laser_dwell_s": 0.02, "laser_delay_s": 0.05, } - with patch( - "mx_bluesky.beamlines.i24.serial.parameters.experiment_parameters.BEAM_CENTER_LUT_FILES", - new=TEST_LUT, - ): - yield FixedTargetParameters(**params) + return FixedTargetParameters(**params) diff --git a/tests/unit_tests/beamlines/i24/serial/fixed_target/test_ft_collect.py b/tests/unit_tests/beamlines/i24/serial/fixed_target/test_ft_collect.py index 9044690f1..0b96d1213 100644 --- a/tests/unit_tests/beamlines/i24/serial/fixed_target/test_ft_collect.py +++ b/tests/unit_tests/beamlines/i24/serial/fixed_target/test_ft_collect.py @@ -30,6 +30,8 @@ write_userlog, ) +from ..conftest import TEST_LUT + chipmap_str = """01status P3011 1 02status P3021 0 03status P3031 0 @@ -395,22 +397,26 @@ async def test_main_fixed_target_plan( mock_get_chip_prog.return_value = MagicMock() set_mock_value(dcm.wavelength_in_a, 0.6) fake_datasize.return_value = 400 - RE( - main_fixed_target_plan( - zebra, - pmac, - aperture, - backlight, - beamstop, - detector_stage, - shutter, - dcm, - mirrors, - eiger_beam_center, - dummy_params_without_pp, - fake_dcid, + with patch( + "mx_bluesky.beamlines.i24.serial.fixed_target.i24ssx_Chip_Collect_py3v1.BEAM_CENTER_LUT_FILES", + new=TEST_LUT, + ): + RE( + main_fixed_target_plan( + zebra, + pmac, + aperture, + backlight, + beamstop, + detector_stage, + shutter, + dcm, + mirrors, + eiger_beam_center, + dummy_params_without_pp, + fake_dcid, + ) ) - ) mock_beam_x = get_mock_put(eiger_beam_center.beam_x) mock_pmac_str = get_mock_put(pmac.pmac_string) diff --git a/tests/unit_tests/beamlines/i24/serial/setup_beamline/test_setup_beamline.py b/tests/unit_tests/beamlines/i24/serial/setup_beamline/test_setup_beamline.py index 7ab4da802..5def23a72 100644 --- a/tests/unit_tests/beamlines/i24/serial/setup_beamline/test_setup_beamline.py +++ b/tests/unit_tests/beamlines/i24/serial/setup_beamline/test_setup_beamline.py @@ -9,6 +9,8 @@ from mx_bluesky.beamlines.i24.serial.setup_beamline import setup_beamline +from ..conftest import TEST_LUT + @patch("mx_bluesky.beamlines.i24.serial.setup_beamline.setup_beamline.bps.sleep") async def test_setup_beamline_for_collection_plan( @@ -30,16 +32,35 @@ async def test_move_detector_stage_to_position_plan(detector_stage: DetectorMoti assert await detector_stage.z.user_readback.get_value() == det_dist -# TODO FIXME This won't work beacause of LUT file -# Probably also other, need to figure it out +def test_compute_beam_center_position_from_lut(dummy_params_ex): + lut_path = TEST_LUT[dummy_params_ex.detector_name] + + expected_beam_x = 1597.06 + expected_beam_y = 1693.33 + + beam_center_pos = setup_beamline.compute_beam_center_position_from_lut( + lut_path, + dummy_params_ex.detector_distance_mm, + dummy_params_ex.detector_size_constants, + ) + assert beam_center_pos[0] == pytest.approx(expected_beam_x, 1e-2) + assert beam_center_pos[1] == pytest.approx(expected_beam_y, 1e-2) + + async def test_set_detector_beam_center_plan( eiger_beam_center: DetectorBeamCenter, dummy_params_ex, RE ): - test_detector_distance = 100 - test_detector_params = dummy_params_ex.detector_params + beam_center_pos = setup_beamline.compute_beam_center_position_from_lut( + TEST_LUT[dummy_params_ex.detector_name], + dummy_params_ex.detector_distance_mm, # 100 + dummy_params_ex.detector_size_constants, + ) + # test_detector_distance = 100 + # test_detector_params = dummy_params_ex.detector_params RE( setup_beamline.set_detector_beam_center_plan( - eiger_beam_center, test_detector_params, test_detector_distance + eiger_beam_center, + beam_center_pos, # test_detector_params, test_detector_distance ) ) diff --git a/tests/unit_tests/common/external_interaction/__init__.py b/tests/unit_tests/common/external_interaction/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/common/external_interaction/callbacks/__init__.py b/tests/unit_tests/common/external_interaction/callbacks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/common/external_interaction/callbacks/common/__init__.py b/tests/unit_tests/common/external_interaction/callbacks/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/common/test_abstract_event.py b/tests/unit_tests/common/external_interaction/callbacks/common/test_abstract_event.py similarity index 91% rename from tests/unit_tests/hyperion/external_interaction/callbacks/common/test_abstract_event.py rename to tests/unit_tests/common/external_interaction/callbacks/common/test_abstract_event.py index 0a75e554d..e25d82438 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/common/test_abstract_event.py +++ b/tests/unit_tests/common/external_interaction/callbacks/common/test_abstract_event.py @@ -4,7 +4,7 @@ import bluesky.preprocessors as bpp from bluesky.run_engine import RunEngine -from mx_bluesky.hyperion.external_interaction.callbacks.common.abstract_event import ( +from mx_bluesky.common.external_interaction.callbacks.common.abstract_event import ( AbstractEvent, ) diff --git a/tests/unit_tests/common/external_interaction/callbacks/ispyb/__init__.py b/tests/unit_tests/common/external_interaction/callbacks/ispyb/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/hyperion/external_interaction/ispyb/conftest.py b/tests/unit_tests/common/external_interaction/callbacks/ispyb/conftest.py similarity index 86% rename from tests/unit_tests/hyperion/external_interaction/ispyb/conftest.py rename to tests/unit_tests/common/external_interaction/callbacks/ispyb/conftest.py index c57c4a83e..caae596ac 100644 --- a/tests/unit_tests/hyperion/external_interaction/ispyb/conftest.py +++ b/tests/unit_tests/common/external_interaction/callbacks/ispyb/conftest.py @@ -2,27 +2,27 @@ import pytest -from mx_bluesky.hyperion.external_interaction.ispyb.data_model import ( +from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionGridInfo, DataCollectionPositionInfo, Orientation, ScanDataInfo, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import StoreInIspyb +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import StoreInIspyb from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan -from ..conftest import ( +from ......conftest import ( TEST_DATA_COLLECTION_GROUP_ID, TEST_DATA_COLLECTION_IDS, TEST_SAMPLE_ID, - default_raw_params, + default_raw_gridscan_params, ) @pytest.fixture def dummy_params(): - dummy_params = HyperionThreeDGridScan(**default_raw_params()) + dummy_params = HyperionThreeDGridScan(**default_raw_gridscan_params()) dummy_params.sample_id = TEST_SAMPLE_ID dummy_params.run_number = 0 return dummy_params diff --git a/tests/unit_tests/hyperion/external_interaction/ispyb/test_expeye_interaction.py b/tests/unit_tests/common/external_interaction/callbacks/ispyb/test_expeye_interaction.py similarity index 81% rename from tests/unit_tests/hyperion/external_interaction/ispyb/test_expeye_interaction.py rename to tests/unit_tests/common/external_interaction/callbacks/ispyb/test_expeye_interaction.py index 2b19aba0f..3d9816a1d 100644 --- a/tests/unit_tests/hyperion/external_interaction/ispyb/test_expeye_interaction.py +++ b/tests/unit_tests/common/external_interaction/callbacks/ispyb/test_expeye_interaction.py @@ -2,13 +2,13 @@ import pytest -from mx_bluesky.hyperion.external_interaction.exceptions import ISPyBDepositionNotMade -from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import ( +from mx_bluesky.common.external_interaction.ispyb.exp_eye_store import ( BearerAuth, BLSampleStatus, ExpeyeInteraction, _get_base_url_and_token, ) +from mx_bluesky.common.utils.exceptions import ISPyBDepositionNotMade def test_get_url_and_token_returns_expected_data(): @@ -17,7 +17,7 @@ def test_get_url_and_token_returns_expected_data(): assert token == "notatoken" -@patch("mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store.post") +@patch("mx_bluesky.common.external_interaction.ispyb.exp_eye_store.post") def test_when_start_load_called_then_correct_expected_url_posted_to_with_expected_data( mock_post, ): @@ -39,7 +39,7 @@ def test_when_start_load_called_then_correct_expected_url_posted_to_with_expecte assert mock_post.call_args.kwargs["json"] == expected_data -@patch("mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store.post") +@patch("mx_bluesky.common.external_interaction.ispyb.exp_eye_store.post") def test_when_start_called_then_returns_id(mock_post): mock_post.return_value.json.return_value = {"robotActionId": 190} expeye_interactor = ExpeyeInteraction() @@ -47,7 +47,7 @@ def test_when_start_called_then_returns_id(mock_post): assert robot_id == 190 -@patch("mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store.post") +@patch("mx_bluesky.common.external_interaction.ispyb.exp_eye_store.post") def test_when_start_load_called_then_use_correct_token( mock_post, ): @@ -58,7 +58,7 @@ def test_when_start_load_called_then_use_correct_token( assert auth.token == "notatoken" -@patch("mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store.post") +@patch("mx_bluesky.common.external_interaction.ispyb.exp_eye_store.post") def test_given_server_does_not_respond_when_start_load_called_then_error(mock_post): mock_post.return_value.ok = False @@ -67,7 +67,7 @@ def test_given_server_does_not_respond_when_start_load_called_then_error(mock_po expeye_interactor.start_load("test", 3, 700, 10, 5) -@patch("mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store.patch") +@patch("mx_bluesky.common.external_interaction.ispyb.exp_eye_store.patch") def test_when_end_load_called_with_success_then_correct_expected_url_posted_to_with_expected_data( # mocks HTTP PATCH mock_patch, @@ -85,7 +85,7 @@ def test_when_end_load_called_with_success_then_correct_expected_url_posted_to_w assert mock_patch.call_args.kwargs["json"] == expected_data -@patch("mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store.patch") +@patch("mx_bluesky.common.external_interaction.ispyb.exp_eye_store.patch") def test_when_end_load_called_with_failure_then_correct_expected_url_posted_to_with_expected_data( mock_patch, ): @@ -102,7 +102,7 @@ def test_when_end_load_called_with_failure_then_correct_expected_url_posted_to_w assert mock_patch.call_args.kwargs["json"] == expected_data -@patch("mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store.patch") +@patch("mx_bluesky.common.external_interaction.ispyb.exp_eye_store.patch") def test_when_end_load_called_then_use_correct_token( mock_patch, ): @@ -113,7 +113,7 @@ def test_when_end_load_called_then_use_correct_token( assert auth.token == "notatoken" -@patch("mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store.patch") +@patch("mx_bluesky.common.external_interaction.ispyb.exp_eye_store.patch") def test_given_server_does_not_respond_when_end_load_called_then_error(mock_patch): mock_patch.return_value.ok = False @@ -122,7 +122,7 @@ def test_given_server_does_not_respond_when_end_load_called_then_error(mock_patc expeye_interactor.end_load(1, "", "") -@patch("mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store.patch") +@patch("mx_bluesky.common.external_interaction.ispyb.exp_eye_store.patch") def test_when_update_barcode_called_with_success_then_correct_expected_url_posted_to_with_expected_data( mock_patch, ): @@ -141,7 +141,7 @@ def test_when_update_barcode_called_with_success_then_correct_expected_url_poste assert mock_patch.call_args.kwargs["json"] == expected_data -@patch("mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store.patch") +@patch("mx_bluesky.common.external_interaction.ispyb.exp_eye_store.patch") def test_update_sample_status( mock_patch, ): diff --git a/tests/unit_tests/hyperion/external_interaction/ispyb/test_gridscan_ispyb_store_3d.py b/tests/unit_tests/common/external_interaction/callbacks/ispyb/test_gridscan_ispyb_store_3d.py similarity index 93% rename from tests/unit_tests/hyperion/external_interaction/ispyb/test_gridscan_ispyb_store_3d.py rename to tests/unit_tests/common/external_interaction/callbacks/ispyb/test_gridscan_ispyb_store_3d.py index f5fd08375..6b518268a 100644 --- a/tests/unit_tests/hyperion/external_interaction/ispyb/test_gridscan_ispyb_store_3d.py +++ b/tests/unit_tests/common/external_interaction/callbacks/ispyb/test_gridscan_ispyb_store_3d.py @@ -3,7 +3,7 @@ import pytest from ispyb.sp.mxacquisition import MXAcquisition -from mx_bluesky.hyperion.external_interaction.ispyb.data_model import ( +from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionGridInfo, DataCollectionGroupInfo, DataCollectionInfo, @@ -11,12 +11,14 @@ Orientation, ScanDataInfo, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import ( +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( IspybIds, StoreInIspyb, ) -from ..conftest import ( +from ......conftest import ( + EXPECTED_END_TIME, + EXPECTED_START_TIME, TEST_BARCODE, TEST_DATA_COLLECTION_GROUP_ID, TEST_DATA_COLLECTION_IDS, @@ -28,9 +30,6 @@ mx_acquisition_from_conn, ) -EXPECTED_START_TIME = "2024-02-08 14:03:59" -EXPECTED_END_TIME = "2024-02-08 14:04:01" - @pytest.fixture def dummy_collection_group_info(): @@ -66,7 +65,7 @@ def scan_data_info_for_begin(): beamsize_at_samplex=0.1, beamsize_at_sampley=0.1, transmission=100.0, - comments="Hyperion: Xray centring - Diffraction grid scan of 40 by 20 images in 100.0 um by 100.0 um steps. Top left (px): [50,100], bottom right (px): [3250,1700].", + comments="MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 20 images in 100.0 um by 100.0 um steps. Top left (px): [50,100], bottom right (px): [3250,1700].", detector_distance=100.0, exp_time=0.1, imgdir="/tmp/", @@ -114,7 +113,7 @@ def scan_data_infos_for_update(): beamsize_at_samplex=0.1, beamsize_at_sampley=0.1, transmission=100.0, - comments="Hyperion: Xray centring - Diffraction grid scan of 40 by 20 images in 100.0 um by 100.0 um steps. Top left (px): [50,100], bottom right (px): [3250,1700].", + comments="MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 20 images in 100.0 um by 100.0 um steps. Top left (px): [50,100], bottom right (px): [3250,1700].", detector_distance=100.0, exp_time=0.1, imgdir="/tmp/", @@ -172,7 +171,7 @@ def scan_data_infos_for_update(): beamsize_at_samplex=0.1, beamsize_at_sampley=0.1, transmission=100.0, - comments="Hyperion: Xray centring - Diffraction grid scan of 40 by 10 images in 100.0 um by 200.0 um steps. Top left (px): [50,120], bottom right (px): [3250,1720].", + comments="MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 10 images in 100.0 um by 200.0 um steps. Top left (px): [50,120], bottom right (px): [3250,1720].", detector_distance=100.0, exp_time=0.1, imgdir="/tmp/", @@ -247,11 +246,11 @@ def test_ispyb_deposition_comment_for_3D_correct( first_upserted_param_value_list = mock_upsert_dc.call_args_list[1][0][0] second_upserted_param_value_list = mock_upsert_dc.call_args_list[2][0][0] assert first_upserted_param_value_list[29] == ( - "Hyperion: Xray centring - Diffraction grid scan of 40 by 20 images " + "MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 20 images " "in 100.0 um by 100.0 um steps. Top left (px): [50,100], bottom right (px): [3250,1700]." ) assert second_upserted_param_value_list[29] == ( - "Hyperion: Xray centring - Diffraction grid scan of 40 by 10 images " + "MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 10 images " "in 100.0 um by 200.0 um steps. Top left (px): [50,120], bottom right (px): [3250,1720]." ) @@ -281,7 +280,7 @@ def test_store_3d_grid_scan( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def test_begin_deposition( @@ -326,7 +325,7 @@ def test_begin_deposition( "beamsize_at_samplex": 0.1, "beamsize_at_sampley": 0.1, "transmission": 100.0, - "comments": "Hyperion: Xray centring - Diffraction grid scan of 40 by 20 " + "comments": "MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 20 " "images in 100.0 um by 100.0 um steps. Top left (px): [50,100], " "bottom right (px): [3250,1700].", "data_collection_number": 1, @@ -357,7 +356,7 @@ def test_begin_deposition( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def test_update_deposition( @@ -408,7 +407,7 @@ def test_update_deposition( "beamsize_at_samplex": 0.1, "beamsize_at_sampley": 0.1, "transmission": 100.0, - "comments": "Hyperion: Xray centring - Diffraction grid scan of 40 by 20 " + "comments": "MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 20 " "images in 100.0 um by 100.0 um steps. Top left (px): [50,100], " "bottom right (px): [3250,1700].", "data_collection_number": 1, @@ -484,7 +483,7 @@ def test_update_deposition( "beamsize_at_samplex": 0.1, "beamsize_at_sampley": 0.1, "transmission": 100.0, - "comments": "Hyperion: Xray centring - Diffraction grid scan of 40 by 10 " + "comments": "MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 10 " "images in 100.0 um by 200.0 um steps. Top left (px): [50,120], " "bottom right (px): [3250,1720].", "data_collection_number": 1, @@ -543,11 +542,11 @@ def test_update_deposition( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) @patch( - "mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store.get_current_time_string", + "mx_bluesky.common.external_interaction.ispyb.ispyb_store.get_current_time_string", ) def test_end_deposition_happy_path( get_current_time, diff --git a/tests/unit_tests/hyperion/external_interaction/ispyb/test_rotation_ispyb_store.py b/tests/unit_tests/common/external_interaction/callbacks/ispyb/test_rotation_ispyb_store.py similarity index 94% rename from tests/unit_tests/hyperion/external_interaction/ispyb/test_rotation_ispyb_store.py rename to tests/unit_tests/common/external_interaction/callbacks/ispyb/test_rotation_ispyb_store.py index 130aedbe8..be7e8c21f 100644 --- a/tests/unit_tests/hyperion/external_interaction/ispyb/test_rotation_ispyb_store.py +++ b/tests/unit_tests/common/external_interaction/callbacks/ispyb/test_rotation_ispyb_store.py @@ -2,19 +2,19 @@ import pytest -from mx_bluesky.hyperion.external_interaction.ispyb.data_model import ( +from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionGroupInfo, DataCollectionInfo, DataCollectionPositionInfo, ScanDataInfo, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import ( +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( IspybIds, StoreInIspyb, ) from mx_bluesky.hyperion.parameters.constants import CONST -from ..conftest import ( +from ......conftest import ( EXPECTED_END_TIME, EXPECTED_START_TIME, TEST_BARCODE, @@ -75,7 +75,7 @@ def dummy_rotation_data_collection_group_info(): @pytest.fixture @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def scan_data_info_for_begin(): @@ -180,7 +180,7 @@ def dummy_rotation_ispyb_with_experiment_type(): @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def test_begin_deposition( @@ -221,7 +221,7 @@ def test_begin_deposition( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def test_begin_deposition_with_group_id_updates_but_doesnt_insert( @@ -264,7 +264,7 @@ def test_begin_deposition_with_group_id_updates_but_doesnt_insert( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def test_begin_deposition_with_alternate_experiment_type( @@ -294,7 +294,7 @@ def test_begin_deposition_with_alternate_experiment_type( @patch( - "mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store.get_current_time_string", + "mx_bluesky.common.external_interaction.ispyb.ispyb_store.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def test_update_deposition( @@ -351,7 +351,7 @@ def test_update_deposition( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def test_update_deposition_with_group_id_updates( @@ -410,11 +410,11 @@ def test_update_deposition_with_group_id_updates( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) @patch( - "mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store.get_current_time_string", + "mx_bluesky.common.external_interaction.ispyb.ispyb_store.get_current_time_string", ) def test_end_deposition_happy_path( get_current_time, diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/test_plan_reactive_callback.py b/tests/unit_tests/common/external_interaction/callbacks/test_plan_reactive_callback.py similarity index 97% rename from tests/unit_tests/hyperion/external_interaction/callbacks/test_plan_reactive_callback.py rename to tests/unit_tests/common/external_interaction/callbacks/test_plan_reactive_callback.py index f26715656..5ce718c14 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/test_plan_reactive_callback.py +++ b/tests/unit_tests/common/external_interaction/callbacks/test_plan_reactive_callback.py @@ -6,11 +6,14 @@ from bluesky.run_engine import RunEngine from event_model.documents import Event, EventDescriptor, RunStart, RunStop -from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import ( +from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import ( PlanReactiveCallback, ) -from ..conftest import MockReactiveCallback, get_test_plan +from ..conftest import ( + MockReactiveCallback, + get_test_plan, +) def test_activity_gated_functions_not_called_when_inactive( diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/test_zocalo_handler.py b/tests/unit_tests/common/external_interaction/callbacks/test_zocalo_handler.py similarity index 88% rename from tests/unit_tests/hyperion/external_interaction/callbacks/test_zocalo_handler.py rename to tests/unit_tests/common/external_interaction/callbacks/test_zocalo_handler.py index a2a592404..bcea1619d 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/test_zocalo_handler.py +++ b/tests/unit_tests/common/external_interaction/callbacks/test_zocalo_handler.py @@ -3,20 +3,20 @@ import pytest from dodal.devices.zocalo import ZocaloStartInfo -from mx_bluesky.hyperion.external_interaction.callbacks.common.callback_util import ( - create_gridscan_callbacks, -) -from mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback import ( +from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import ( ZocaloCallback, ) -from mx_bluesky.hyperion.external_interaction.exceptions import ISPyBDepositionNotMade -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import ( +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( IspybIds, StoreInIspyb, ) +from mx_bluesky.common.utils.exceptions import ISPyBDepositionNotMade +from mx_bluesky.hyperion.external_interaction.callbacks.common.callback_util import ( + create_gridscan_callbacks, +) from mx_bluesky.hyperion.parameters.constants import CONST -from .conftest import TestData +from .....conftest import TestData EXPECTED_DCID = 100 EXPECTED_RUN_START_MESSAGE = {"event": "start", "ispyb_dcid": EXPECTED_DCID} @@ -66,7 +66,7 @@ def test_handler_raises_on_right_plan_with_no_ispyb_ids(self): ) @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback.ZocaloTrigger", + "mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback.ZocaloTrigger", autospec=True, ) def test_handler_inits_zocalo_trigger_on_right_plan(self, zocalo_trigger): @@ -82,14 +82,14 @@ def test_handler_inits_zocalo_trigger_on_right_plan(self, zocalo_trigger): assert zocalo_handler.zocalo_interactor is not None @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback.ZocaloTrigger", + "mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback.ZocaloTrigger", autospec=True, ) @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter", + "mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter", ) @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb", + "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb", ) def test_execution_of_do_fgs_triggers_zocalo_calls( self, ispyb_store: MagicMock, nexus_writer: MagicMock, zocalo_trigger diff --git a/tests/unit_tests/common/external_interaction/conftest.py b/tests/unit_tests/common/external_interaction/conftest.py new file mode 100644 index 000000000..d5e018798 --- /dev/null +++ b/tests/unit_tests/common/external_interaction/conftest.py @@ -0,0 +1,52 @@ +from collections.abc import Callable +from typing import Any +from unittest.mock import MagicMock + +import bluesky.plan_stubs as bps +import bluesky.preprocessors as bpp +import pytest +from bluesky.run_engine import RunEngine +from ophyd.sim import SynAxis + +from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import ( + PlanReactiveCallback, +) + + +class MockReactiveCallback(PlanReactiveCallback): + activity_gated_start: MagicMock + activity_gated_descriptor: MagicMock + activity_gated_event: MagicMock + activity_gated_stop: MagicMock + + def __init__(self, *, emit: Callable[..., Any] | None = None) -> None: + super().__init__(MagicMock(), emit=emit) + self.activity_gated_start = MagicMock(name="activity_gated_start") # type: ignore + self.activity_gated_descriptor = MagicMock(name="activity_gated_descriptor") # type: ignore + self.activity_gated_event = MagicMock(name="activity_gated_event") # type: ignore + self.activity_gated_stop = MagicMock(name="activity_gated_stop") # type: ignore + + +@pytest.fixture +def mocked_test_callback(): + t = MockReactiveCallback() + return t + + +@pytest.fixture +def RE_with_mock_callback(mocked_test_callback): + RE = RunEngine() + RE.subscribe(mocked_test_callback) + yield RE, mocked_test_callback + + +def get_test_plan(callback_name): + s = SynAxis(name="fake_signal") + + @bpp.run_decorator(md={"activate_callbacks": [callback_name]}) + def test_plan(): + yield from bps.create() + yield from bps.read(s) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809 + yield from bps.save() + + return test_plan, s diff --git a/tests/unit_tests/common/external_interaction/nexus/test_nexus_utils.py b/tests/unit_tests/common/external_interaction/nexus/test_nexus_utils.py new file mode 100644 index 000000000..2aa45af0b --- /dev/null +++ b/tests/unit_tests/common/external_interaction/nexus/test_nexus_utils.py @@ -0,0 +1,45 @@ +import numpy as np +import pytest +from numpy.typing import DTypeLike +from scanspec.core import AxesPoints + +from mx_bluesky.common.external_interaction.nexus.nexus_utils import ( + AxisDirection, + create_goniometer_axes, + vds_type_based_on_bit_depth, +) + + +@pytest.mark.parametrize( + "bit_depth,expected_type", + [(8, np.uint8), (16, np.uint16), (32, np.uint32), (100, np.uint16)], +) +def test_vds_type_is_expected_based_on_bit_depth( + bit_depth: int, expected_type: DTypeLike +): + assert vds_type_based_on_bit_depth(bit_depth) == expected_type + + +@pytest.fixture +def scan_points(test_rotation_params) -> AxesPoints: + return test_rotation_params.scan_points + + +@pytest.mark.parametrize( + "omega_axis_direction, expected_axis_direction", + [[AxisDirection.NEGATIVE, -1], [AxisDirection.POSITIVE, 1]], +) +def test_omega_axis_direction_determined_from_features( + omega_axis_direction: AxisDirection, + expected_axis_direction: float, + scan_points: AxesPoints, +): + omega_start = 0 + gonio = create_goniometer_axes( + omega_start, scan_points, (0, 0, 0), 0, 0, omega_axis_direction + ) + assert gonio.axes_list[0].name == "omega" and gonio.axes_list[0].vector == ( + expected_axis_direction, + 0, + 0, + ) diff --git a/tests/unit_tests/hyperion/external_interaction/test_ispyb_utils.py b/tests/unit_tests/common/external_interaction/test_ispyb_utils.py similarity index 88% rename from tests/unit_tests/hyperion/external_interaction/test_ispyb_utils.py rename to tests/unit_tests/common/external_interaction/test_ispyb_utils.py index aadb32af3..1cbfc09b4 100644 --- a/tests/unit_tests/hyperion/external_interaction/test_ispyb_utils.py +++ b/tests/unit_tests/common/external_interaction/test_ispyb_utils.py @@ -2,10 +2,10 @@ import pytest -from mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping import ( +from mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping import ( get_proposal_and_session_from_visit_string, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_utils import ( +from mx_bluesky.common.external_interaction.ispyb.ispyb_utils import ( get_current_time_string, ) diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/__init__.py b/tests/unit_tests/common/external_interaction/xray_centre/__init__.py similarity index 100% rename from tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/__init__.py rename to tests/unit_tests/common/external_interaction/xray_centre/__init__.py diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/test_ispyb_callback.py b/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_callback.py similarity index 91% rename from tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/test_ispyb_callback.py rename to tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_callback.py index c1b193b0a..cec1799c2 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/test_ispyb_callback.py +++ b/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_callback.py @@ -1,19 +1,19 @@ from unittest.mock import MagicMock, patch -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( GridscanISPyBCallback, ) -from ...conftest import ( +from .....conftest import ( EXPECTED_START_TIME, TEST_DATA_COLLECTION_GROUP_ID, TEST_DATA_COLLECTION_IDS, TEST_SAMPLE_ID, TEST_SESSION_ID, + TestData, assert_upsert_call_with, mx_acquisition_from_conn, ) -from ..conftest import TestData EXPECTED_DATA_COLLECTION_3D_XY = { "visitid": TEST_SESSION_ID, @@ -44,8 +44,14 @@ } +TEST_GRID_INFO_IDS = (56, 57) +TEST_POSITION_ID = 78 + +EXPECTED_END_TIME = "2024-02-08 14:04:01" + + @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) class TestXrayCentreISPyBCallback: @@ -92,8 +98,8 @@ def test_hardware_read_event_3d(self, mock_ispyb_conn): "slitgapvertical": 0.2345, "synchrotronmode": "User", "undulatorgap1": 1.234, - "resolution": 1.1830593328548429, - "wavelength": 1.1164718451643736, + "resolution": 1.1830593331191241, + "wavelength": 1.11647184541378, } assert_upsert_call_with( mx_acq.upsert_data_collection.mock_calls[0], @@ -130,10 +136,10 @@ def test_flux_read_events_3d(self, mock_ispyb_conn): { "parentid": TEST_DATA_COLLECTION_GROUP_ID, "id": TEST_DATA_COLLECTION_IDS[0], - "wavelength": 1.1164718451643736, + "wavelength": 1.11647184541378, "transmission": 100, "flux": 10, - "resolution": 1.1830593328548429, + "resolution": 1.1830593331191241, "focal_spot_size_at_samplex": 0.05, "focal_spot_size_at_sampley": 0.02, "beamsize_at_samplex": 0.05, @@ -146,10 +152,10 @@ def test_flux_read_events_3d(self, mock_ispyb_conn): { "parentid": TEST_DATA_COLLECTION_GROUP_ID, "id": TEST_DATA_COLLECTION_IDS[1], - "wavelength": 1.1164718451643736, + "wavelength": 1.11647184541378, "transmission": 100, "flux": 10, - "resolution": 1.1830593328548429, + "resolution": 1.1830593331191241, "focal_spot_size_at_samplex": 0.05, "focal_spot_size_at_sampley": 0.02, "beamsize_at_samplex": 0.05, @@ -183,7 +189,7 @@ def test_activity_gated_event_oav_snapshot_triggered(self, mock_ispyb_conn): "xtal_snapshot1": "test_1_y", "xtal_snapshot2": "test_2_y", "xtal_snapshot3": "test_3_y", - "comments": "Hyperion: Xray centring - Diffraction grid scan of 40 by 20 " + "comments": "MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 20 " "images in 126.4 um by 126.4 um steps. Top left (px): [50,100], " "bottom right (px): [3250,1700].", "axisstart": 0, @@ -202,7 +208,7 @@ def test_activity_gated_event_oav_snapshot_triggered(self, mock_ispyb_conn): "xtal_snapshot1": "test_1_z", "xtal_snapshot2": "test_2_z", "xtal_snapshot3": "test_3_z", - "comments": "Hyperion: Xray centring - Diffraction grid scan of 40 by 10 " + "comments": "MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 10 " "images in 126.4 um by 126.4 um steps. Top left (px): [50,0], " "bottom right (px): [3250,800].", "axisstart": 90, diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/test_ispyb_handler.py b/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_handler.py similarity index 86% rename from tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/test_ispyb_handler.py rename to tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_handler.py index 9bbdbc93b..7578060f7 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/test_ispyb_handler.py +++ b/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_handler.py @@ -3,17 +3,17 @@ import pytest from graypy import GELFTCPHandler -from mx_bluesky.hyperion.external_interaction.callbacks.__main__ import setup_logging -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( GridscanISPyBCallback, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import ( +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( IspybIds, StoreInIspyb, ) -from mx_bluesky.hyperion.log import ISPYB_LOGGER +from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER +from mx_bluesky.hyperion.external_interaction.callbacks.__main__ import setup_logging -from ..conftest import TestData +from .....conftest import TestData DC_IDS = (1, 2) DCG_ID = 4 @@ -41,11 +41,11 @@ def mock_store_in_ispyb(config, *args, **kwargs) -> StoreInIspyb: @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", MagicMock(return_value=td.DUMMY_TIME_STRING), ) @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb", + "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb", mock_store_in_ispyb, ) class TestXrayCentreIspybHandler: @@ -109,7 +109,10 @@ def test_given_ispyb_callback_started_writing_to_ispyb_when_messages_logged_then ): setup_logging(True) gelf_handler: MagicMock = next( - filter(lambda h: isinstance(h, GELFTCPHandler), ISPYB_LOGGER.handlers) # type: ignore + filter( + lambda h: isinstance(h, GELFTCPHandler), + ISPYB_ZOCALO_CALLBACK_LOGGER.handlers, + ) # type: ignore ) gelf_handler.emit = MagicMock() @@ -126,7 +129,7 @@ def test_given_ispyb_callback_started_writing_to_ispyb_when_messages_logged_then td.test_event_document_during_data_collection ) - ISPYB_LOGGER.info("test") + ISPYB_ZOCALO_CALLBACK_LOGGER.info("test") latest_record = gelf_handler.emit.call_args.args[-1] assert latest_record.dc_group_id == DCG_ID @@ -136,7 +139,10 @@ def test_given_ispyb_callback_finished_writing_to_ispyb_when_messages_logged_the ): setup_logging(True) gelf_handler: MagicMock = next( - filter(lambda h: isinstance(h, GELFTCPHandler), ISPYB_LOGGER.handlers) # type: ignore + filter( + lambda h: isinstance(h, GELFTCPHandler), + ISPYB_ZOCALO_CALLBACK_LOGGER.handlers, + ) # type: ignore ) gelf_handler.emit = MagicMock() @@ -154,12 +160,12 @@ def test_given_ispyb_callback_finished_writing_to_ispyb_when_messages_logged_the ) ispyb_handler.activity_gated_stop(td.test_run_gridscan_failed_stop_document) - ISPYB_LOGGER.info("test") + ISPYB_ZOCALO_CALLBACK_LOGGER.info("test") latest_record = gelf_handler.emit.call_args.args[-1] assert not hasattr(latest_record, "dc_group_id") @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback.time", + "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.time", side_effect=[2, 100], ) def test_given_fgs_plan_finished_when_zocalo_results_event_then_expected_comment_deposited( diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/test_ispyb_mapping.py b/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_mapping.py similarity index 55% rename from tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/test_ispyb_mapping.py rename to tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_mapping.py index a6b36b4f0..03c2a6588 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/test_ispyb_mapping.py +++ b/tests/unit_tests/common/external_interaction/xray_centre/test_ispyb_mapping.py @@ -2,24 +2,24 @@ import pytest -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_mapping import ( +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping import ( construct_comment_for_gridscan, ) -from mx_bluesky.hyperion.external_interaction.ispyb.data_model import ( +from mx_bluesky.common.external_interaction.ispyb.data_model import ( DataCollectionGridInfo, Orientation, ) from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan -from ...conftest import ( +from .....conftest import ( TEST_SAMPLE_ID, - default_raw_params, + default_raw_gridscan_params, ) @pytest.fixture def dummy_params(): - dummy_params = HyperionThreeDGridScan(**default_raw_params()) + dummy_params = HyperionThreeDGridScan(**default_raw_gridscan_params()) dummy_params.sample_id = TEST_SAMPLE_ID dummy_params.run_number = 0 return dummy_params @@ -30,25 +30,22 @@ def test_ispyb_deposition_rounds_position_to_int( mock_ispyb_conn: MagicMock, dummy_params, ): - assert ( - construct_comment_for_gridscan( - DataCollectionGridInfo( - 0.1, - 0.1, - 40, - 20, - 1.25, - 1.25, - 0.01, # type: ignore - 100, - Orientation.HORIZONTAL, - True, # type: ignore - ), - ) - == ( - "Hyperion: Xray centring - Diffraction grid scan of 40 by 20 images " - "in 100.0 um by 100.0 um steps. Top left (px): [0,100], bottom right (px): [3200,1700]." - ) + assert construct_comment_for_gridscan( + DataCollectionGridInfo( + 0.1, + 0.1, + 40, + 20, + 1.25, + 1.25, + 0.01, # type: ignore + 100, + Orientation.HORIZONTAL, + True, # type: ignore + ), + ) == ( + "MX-Bluesky: Xray centring - Diffraction grid scan of 40 by 20 images " + "in 100.0 um by 100.0 um steps. Top left (px): [0,100], bottom right (px): [3200,1700]." ) @@ -65,7 +62,7 @@ def test_ispyb_deposition_rounds_position_to_int( ], ) @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_mapping.oav_utils.bottom_right_from_top_left", + "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping.oav_utils.bottom_right_from_top_left", autospec=True, ) def test_ispyb_deposition_rounds_box_size_int( @@ -80,6 +77,6 @@ def test_ispyb_deposition_rounds_box_size_int( bottom_right_from_top_left.return_value = [0, 0] assert construct_comment_for_gridscan(data_collection_grid_info) == ( - "Hyperion: Xray centring - Diffraction grid scan of 0 by 0 images in " + "MX-Bluesky: Xray centring - Diffraction grid scan of 0 by 0 images in " f"{rounded} um by {rounded} um steps. Top left (px): [0,0], bottom right (px): [0,0]." ) diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/test_nexus_handler.py b/tests/unit_tests/common/external_interaction/xray_centre/test_nexus_handler.py similarity index 88% rename from tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/test_nexus_handler.py rename to tests/unit_tests/common/external_interaction/xray_centre/test_nexus_handler.py index 49719af0a..acb940d99 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/test_nexus_handler.py +++ b/tests/unit_tests/common/external_interaction/xray_centre/test_nexus_handler.py @@ -5,17 +5,17 @@ import pytest from numpy.typing import DTypeLike -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.nexus_callback import ( +from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( GridscanNexusFileCallback, ) -from ..conftest import TestData +from .....conftest import TestData @pytest.fixture def nexus_writer(): with patch( - "mx_bluesky.hyperion.external_interaction.nexus.write_nexus.NexusWriter" + "mx_bluesky.common.external_interaction.nexus.write_nexus.NexusWriter" ) as nw: yield nw @@ -30,7 +30,7 @@ def test_writers_not_sDTypeLikeetup_on_plan_start_doc( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter" + "mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter" ) def test_writers_dont_create_on_init_but_do_on_during_collection_read_event( mock_nexus_writer: MagicMock, @@ -65,7 +65,7 @@ def test_writers_dont_create_on_init_but_do_on_during_collection_read_event( ], ) @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter" + "mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter" ) def test_given_different_bit_depths_then_writers_created_wth_correct_VDS_size( mock_nexus_writer: MagicMock, @@ -95,7 +95,7 @@ def test_given_different_bit_depths_then_writers_created_wth_correct_VDS_size( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter" + "mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter" ) def test_beam_and_attenuator_set_on_ispyb_transmission_event( mock_nexus_writer: MagicMock, diff --git a/tests/unit_tests/common/utils/test_log.py b/tests/unit_tests/common/utils/test_log.py index cf1067641..50dfa1c03 100644 --- a/tests/unit_tests/common/utils/test_log.py +++ b/tests/unit_tests/common/utils/test_log.py @@ -11,8 +11,7 @@ from dodal.log import set_up_all_logging_handlers import mx_bluesky.common.utils.log as log -import mx_bluesky.hyperion.log as hyperion_log -from mx_bluesky.hyperion.external_interaction.callbacks.log_uid_tag_callback import ( +from mx_bluesky.common.external_interaction.callbacks.common.log_uid_tag_callback import ( LogUidTaggingCallback, ) @@ -23,7 +22,7 @@ @pytest.fixture(scope="function") def clear_and_mock_loggers(): - clear_log_handlers([*hyperion_log.ALL_LOGGERS, dodal_logger]) + clear_log_handlers([*log.ALL_LOGGERS, dodal_logger]) mock_open_with_tell = MagicMock() mock_open_with_tell.tell.return_value = 0 with ( @@ -34,7 +33,7 @@ def clear_and_mock_loggers(): graylog_emit.reset_mock() filehandler_emit.reset_mock() yield filehandler_emit, graylog_emit - clear_log_handlers([*hyperion_log.ALL_LOGGERS, dodal_logger]) + clear_log_handlers([*log.ALL_LOGGERS, dodal_logger]) @pytest.mark.skip_log_setup @@ -76,7 +75,7 @@ def test_messages_logged_from_dodal_and_hyperion_contain_dcgid( log.set_dcgid_tag(100) - logger = hyperion_log.LOGGER + logger = log.LOGGER logger.info("test_hyperion") dodal_logger.info("test_dodal") @@ -93,7 +92,7 @@ def test_messages_are_tagged_with_run_uid(clear_and_mock_loggers, RE): RE.subscribe(LogUidTaggingCallback()) test_run_uid = None - logger = hyperion_log.LOGGER + logger = log.LOGGER @bpp.run_decorator() def test_plan(): @@ -129,8 +128,8 @@ def test_messages_logged_from_dodal_and_hyperion_get_sent_to_graylog_and_file( ): mock_filehandler_emit, mock_GELFTCPHandler_emit = clear_and_mock_loggers log.do_default_logging_setup("hyperion.log", TEST_GRAYLOG_PORT, dev_mode=True) - logger = hyperion_log.LOGGER - logger.info("test_hyperion") + logger = log.LOGGER + logger.info("test MX_Bluesky") dodal_logger.info("test_dodal") filehandler_calls = mock_filehandler_emit.mock_calls @@ -142,9 +141,9 @@ def test_messages_logged_from_dodal_and_hyperion_get_sent_to_graylog_and_file( for handler in [filehandler_calls, graylog_calls]: handler_names = [c.args[0].name for c in handler] handler_messages = [c.args[0].message for c in handler] - assert "Hyperion" in handler_names + assert "MX-Bluesky" in handler_names assert "Dodal" in handler_names - assert "test_hyperion" in handler_messages + assert "test MX_Bluesky" in handler_messages assert "test_dodal" in handler_messages @@ -155,16 +154,16 @@ def test_callback_loggers_log_to_own_files( mock_filehandler_emit, mock_GELFTCPHandler_emit = clear_and_mock_loggers log.do_default_logging_setup("hyperion.log", TEST_GRAYLOG_PORT, dev_mode=True) - hyperion_logger = hyperion_log.LOGGER - ispyb_logger = hyperion_log.ISPYB_LOGGER - nexus_logger = hyperion_log.NEXUS_LOGGER - for logger in [ispyb_logger, nexus_logger]: + hyperion_logger = log.LOGGER + ISPYB_ZOCALO_CALLBACK_LOGGER = log.ISPYB_ZOCALO_CALLBACK_LOGGER + nexus_logger = log.NEXUS_LOGGER + for logger in [ISPYB_ZOCALO_CALLBACK_LOGGER, nexus_logger]: set_up_all_logging_handlers( logger, log._get_logging_dir(), logger.name, True, 10000 ) hyperion_logger.info("test_hyperion") - ispyb_logger.info("test_ispyb") + ISPYB_ZOCALO_CALLBACK_LOGGER.info("test_ispyb") nexus_logger.info("test_nexus") total_filehandler_calls = mock_filehandler_emit.mock_calls @@ -176,7 +175,10 @@ def test_callback_loggers_log_to_own_files( filter(lambda h: isinstance(h, TimedRotatingFileHandler), dodal_logger.handlers) # type: ignore ) ispyb_filehandler = next( - filter(lambda h: isinstance(h, TimedRotatingFileHandler), ispyb_logger.handlers) # type: ignore + filter( + lambda h: isinstance(h, TimedRotatingFileHandler), + ISPYB_ZOCALO_CALLBACK_LOGGER.handlers, + ) # type: ignore ) nexus_filehandler = next( filter(lambda h: isinstance(h, TimedRotatingFileHandler), nexus_logger.handlers) # type: ignore @@ -190,10 +192,10 @@ def test_callback_loggers_log_to_own_files( def test_log_writes_debug_file_on_error(clear_and_mock_loggers): mock_filehandler_emit, _ = clear_and_mock_loggers log.do_default_logging_setup("hyperion.log", TEST_GRAYLOG_PORT, dev_mode=True) - hyperion_log.LOGGER.debug("debug_message_1") - hyperion_log.LOGGER.debug("debug_message_2") + log.LOGGER.debug("debug_message_1") + log.LOGGER.debug("debug_message_2") mock_filehandler_emit.assert_not_called() - hyperion_log.LOGGER.error("error happens") + log.LOGGER.error("error happens") assert len(mock_filehandler_emit.mock_calls) == 4 messages = [call.args[0].message for call in mock_filehandler_emit.mock_calls] assert "debug_message_1" in messages diff --git a/tests/unit_tests/hyperion/conftest.py b/tests/unit_tests/hyperion/conftest.py index 264ab2a89..9e012c754 100644 --- a/tests/unit_tests/hyperion/conftest.py +++ b/tests/unit_tests/hyperion/conftest.py @@ -2,9 +2,6 @@ from unittest.mock import patch import pytest -from event_model import Event, EventDescriptor - -from mx_bluesky.hyperion.parameters.constants import CONST BANNED_PATHS = [Path("/dls"), Path("/dls_sw")] @@ -17,68 +14,10 @@ def patched_open(*args, **kwargs): requested_path = Path(args[0]) if requested_path.is_absolute(): for p in BANNED_PATHS: - assert not requested_path.is_relative_to( - p - ), f"Attempt to open {requested_path} from inside a unit test" + assert not requested_path.is_relative_to(p), ( + f"Attempt to open {requested_path} from inside a unit test" + ) return unpatched_open(*args, **kwargs) with patch("builtins.open", side_effect=patched_open): yield [] - - -class OavGridSnapshotTestEvents: - test_descriptor_document_oav_snapshot: EventDescriptor = { - "uid": "b5ba4aec-de49-4970-81a4-b4a847391d34", - "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "name": CONST.DESCRIPTORS.OAV_GRID_SNAPSHOT_TRIGGERED, - } # type: ignore - test_event_document_oav_snapshot_xy: Event = { - "descriptor": "b5ba4aec-de49-4970-81a4-b4a847391d34", - "time": 1666604299.828203, - "timestamps": {}, - "seq_num": 1, - "uid": "29033ecf-e052-43dd-98af-c7cdd62e8174", - "data": { - "oav-grid_snapshot-top_left_x": 50, - "oav-grid_snapshot-top_left_y": 100, - "oav-grid_snapshot-num_boxes_x": 40, - "oav-grid_snapshot-num_boxes_y": 20, - "oav-microns_per_pixel_x": 1.58, - "oav-microns_per_pixel_y": 1.58, - "oav-beam_centre_i": 517, - "oav-beam_centre_j": 350, - "oav-grid_snapshot-box_width": 0.1 * 1000 / 1.25, # size in pixels - "oav-grid_snapshot-last_path_full_overlay": "test_1_y", - "oav-grid_snapshot-last_path_outer": "test_2_y", - "oav-grid_snapshot-last_saved_path": "test_3_y", - "smargon-omega": 0, - "smargon-x": 0, - "smargon-y": 0, - "smargon-z": 0, - }, - } - test_event_document_oav_snapshot_xz: Event = { - "descriptor": "b5ba4aec-de49-4970-81a4-b4a847391d34", - "time": 1666604299.828203, - "timestamps": {}, - "seq_num": 1, - "uid": "29033ecf-e052-43dd-98af-c7cdd62e8174", - "data": { - "oav-grid_snapshot-top_left_x": 50, - "oav-grid_snapshot-top_left_y": 0, - "oav-grid_snapshot-num_boxes_x": 40, - "oav-grid_snapshot-num_boxes_y": 10, - "oav-grid_snapshot-box_width": 0.1 * 1000 / 1.25, # size in pixels - "oav-grid_snapshot-last_path_full_overlay": "test_1_z", - "oav-grid_snapshot-last_path_outer": "test_2_z", - "oav-grid_snapshot-last_saved_path": "test_3_z", - "oav-microns_per_pixel_x": 1.58, - "oav-microns_per_pixel_y": 1.58, - "oav-beam_centre_i": 517, - "oav-beam_centre_j": 350, - "smargon-omega": -90, - "smargon-x": 0, - "smargon-y": 0, - "smargon-z": 0, - }, - } diff --git a/tests/unit_tests/hyperion/device_setup_plans/test_check_beamstop.py b/tests/unit_tests/hyperion/device_setup_plans/test_check_beamstop.py new file mode 100644 index 000000000..3512851f1 --- /dev/null +++ b/tests/unit_tests/hyperion/device_setup_plans/test_check_beamstop.py @@ -0,0 +1,24 @@ +import pytest +from bluesky.run_engine import RunEngine +from dodal.devices.i03.beamstop import Beamstop +from ophyd_async.testing import set_mock_value + +from mx_bluesky.hyperion.device_setup_plans.check_beamstop import ( + BeamstopException, + check_beamstop, +) + + +def test_beamstop_check_passes_when_in_beam(beamstop_i03: Beamstop, RE: RunEngine): + set_mock_value(beamstop_i03.x_mm.user_readback, 1.52) + set_mock_value(beamstop_i03.y_mm.user_readback, 44.78) + set_mock_value(beamstop_i03.z_mm.user_readback, 30.0) + RE(check_beamstop(beamstop_i03)) + + +def test_beamstop_check_fails_when_not_in_beam(beamstop_i03: Beamstop, RE: RunEngine): + set_mock_value(beamstop_i03.x_mm.user_readback, 0) + set_mock_value(beamstop_i03.y_mm.user_readback, 0) + set_mock_value(beamstop_i03.z_mm.user_readback, 0) + with pytest.raises(BeamstopException, match="Beamstop is not DATA_COLLECTION"): + RE(check_beamstop(beamstop_i03)) diff --git a/tests/unit_tests/hyperion/device_setup_plans/test_manipulate_sample.py b/tests/unit_tests/hyperion/device_setup_plans/test_manipulate_sample.py index 67b611469..04cea4328 100644 --- a/tests/unit_tests/hyperion/device_setup_plans/test_manipulate_sample.py +++ b/tests/unit_tests/hyperion/device_setup_plans/test_manipulate_sample.py @@ -67,14 +67,14 @@ def test_move_x_y_z( motor_position: list[float], expected_moves: list[float | None], ): - RE(move_x_y_z(fake_fgs_composite.sample_motors, *motor_position)) # type: ignore + RE(move_x_y_z(fake_fgs_composite.smargon, *motor_position)) # type: ignore expected_calls = [ call(axis, pos, group="move_x_y_z") for axis, pos in zip( [ - fake_fgs_composite.sample_motors.x, - fake_fgs_composite.sample_motors.y, - fake_fgs_composite.sample_motors.z, + fake_fgs_composite.smargon.x, + fake_fgs_composite.smargon.y, + fake_fgs_composite.smargon.z, ], expected_moves, strict=False, @@ -108,14 +108,14 @@ def test_move_phi_chi_omega( motor_position: list[float], expected_moves: list[float | None], ): - RE(move_phi_chi_omega(fake_fgs_composite.sample_motors, *motor_position)) # type: ignore + RE(move_phi_chi_omega(fake_fgs_composite.smargon, *motor_position)) # type: ignore expected_calls = [ call(axis, pos, group="move_phi_chi_omega") for axis, pos in zip( [ - fake_fgs_composite.sample_motors.phi, - fake_fgs_composite.sample_motors.chi, - fake_fgs_composite.sample_motors.omega, + fake_fgs_composite.smargon.phi, + fake_fgs_composite.smargon.chi, + fake_fgs_composite.smargon.omega, ], expected_moves, strict=False, diff --git a/tests/unit_tests/hyperion/device_setup_plans/test_setup_panda.py b/tests/unit_tests/hyperion/device_setup_plans/test_setup_panda.py index 21526d494..34a4886ed 100644 --- a/tests/unit_tests/hyperion/device_setup_plans/test_setup_panda.py +++ b/tests/unit_tests/hyperion/device_setup_plans/test_setup_panda.py @@ -8,6 +8,7 @@ from bluesky.simulators import RunEngineSimulator, assert_message_and_return_remaining from dodal.common.types import UpdatingPathProvider from dodal.devices.fast_grid_scan import PandAGridScanParams +from dodal.devices.smargon import Smargon from ophyd_async.fastcs.panda import HDFPanda, SeqTable, SeqTrigger from mx_bluesky.hyperion.device_setup_plans.setup_panda import ( @@ -26,6 +27,7 @@ def get_smargon_speed(x_step_size_mm: float, time_between_x_steps_ms: float) -> def run_simulating_setup_panda_functions( plan: str, panda: HDFPanda, + smargon: Smargon, sim_run_engine: RunEngineSimulator, mock_load_device=MagicMock, ): @@ -49,7 +51,7 @@ def count_commands(msg): setup_panda_for_flyscan( panda, PandAGridScanParams(transmission_fraction=0.01), - 1, + smargon, 0.1, 100.1, smargon_speed, @@ -62,13 +64,15 @@ def count_commands(msg): @patch("mx_bluesky.hyperion.device_setup_plans.setup_panda.load_device") -def test_setup_panda_performs_correct_plans(mock_load_device, sim_run_engine, panda): +def test_setup_panda_performs_correct_plans( + mock_load_device, sim_run_engine, panda, smargon +): num_of_sets, num_of_waits = run_simulating_setup_panda_functions( - "setup", panda, sim_run_engine, mock_load_device + "setup", panda, smargon, sim_run_engine, mock_load_device ) mock_load_device.assert_called_once() - assert num_of_sets == 8 - assert num_of_waits == 3 + assert num_of_sets == 10 + assert num_of_waits == 5 @pytest.mark.parametrize( @@ -89,6 +93,7 @@ def test_setup_panda_correctly_configures_table( exposure_time_s: float, sim_run_engine: RunEngineSimulator, panda, + smargon, ): sample_velocity_mm_per_s = get_smargon_speed(x_step_size, time_between_x_steps_ms) params = PandAGridScanParams( @@ -105,7 +110,7 @@ def test_setup_panda_correctly_configures_table( setup_panda_for_flyscan( panda, params, - 0, + smargon, exposure_time_s, time_between_x_steps_ms, sample_velocity_mm_per_s, @@ -183,7 +188,7 @@ def test_setup_panda_correctly_configures_table( ) -def test_wait_between_setting_table_and_arming_panda(RE: RunEngine, panda): +def test_wait_between_setting_table_and_arming_panda(RE: RunEngine, panda, smargon): bps_wait_done = False def handle_wait(*args, **kwargs): @@ -211,7 +216,7 @@ def assert_set_table_has_been_waited_on(*args, **kwargs): setup_panda_for_flyscan( panda, PandAGridScanParams(transmission_fraction=0.01), - 1, + smargon, 0.1, 101.1, get_smargon_speed(0.1, 1), @@ -223,9 +228,9 @@ def assert_set_table_has_been_waited_on(*args, **kwargs): # It also would be useful to have some system tests which check that (at least) # all the blocks which were enabled on setup are also disabled on tidyup -def test_disarm_panda_disables_correct_blocks(sim_run_engine, panda): +def test_disarm_panda_disables_correct_blocks(sim_run_engine, panda, smargon): num_of_sets, num_of_waits = run_simulating_setup_panda_functions( - "disarm", panda, sim_run_engine + "disarm", panda, sim_run_engine, smargon ) assert num_of_sets == 5 assert num_of_waits == 1 diff --git a/tests/unit_tests/hyperion/device_setup_plans/test_utils.py b/tests/unit_tests/hyperion/device_setup_plans/test_utils.py index 82ab01e21..d92cea39a 100644 --- a/tests/unit_tests/hyperion/device_setup_plans/test_utils.py +++ b/tests/unit_tests/hyperion/device_setup_plans/test_utils.py @@ -26,7 +26,7 @@ class MyTestException(Exception): def test_given_plan_raises_when_exception_raised_then_eiger_disarmed_and_correct_exception_returned( - mock_eiger, detector_motion, RE + beamstop_i03, mock_eiger, detector_motion, RE ): def my_plan(): yield from bps.null() @@ -37,7 +37,7 @@ def my_plan(): with pytest.raises(MyTestException): RE( start_preparing_data_collection_then_do_plan( - eiger, detector_motion, 100, my_plan() + beamstop_i03, eiger, detector_motion, 100, my_plan() ) ) @@ -53,7 +53,7 @@ def null_plan(): def test_given_shutter_open_fails_then_eiger_disarmed_and_correct_exception_returned( - mock_eiger, null_plan, RE + beamstop_i03, mock_eiger, null_plan, RE ): detector_motion = MagicMock() status = Status() @@ -63,7 +63,7 @@ def test_given_shutter_open_fails_then_eiger_disarmed_and_correct_exception_retu with pytest.raises(FailedStatus) as e: RE( start_preparing_data_collection_then_do_plan( - mock_eiger, detector_motion, 100, null_plan + beamstop_i03, mock_eiger, detector_motion, 100, null_plan ) ) assert e.value.args[0] is status @@ -74,7 +74,7 @@ def test_given_shutter_open_fails_then_eiger_disarmed_and_correct_exception_retu def test_given_detector_move_fails_then_eiger_disarmed_and_correct_exception_returned( - mock_eiger, detector_motion, null_plan, RE + beamstop_i03, mock_eiger, detector_motion, null_plan, RE ): status = Status() status.set_exception(MyTestException()) @@ -83,7 +83,7 @@ def test_given_detector_move_fails_then_eiger_disarmed_and_correct_exception_ret with pytest.raises(FailedStatus) as e: RE( start_preparing_data_collection_then_do_plan( - mock_eiger, detector_motion, 100, null_plan + beamstop_i03, mock_eiger, detector_motion, 100, null_plan ) ) assert e.value.args[0] is status diff --git a/tests/unit_tests/hyperion/experiment_plans/conftest.py b/tests/unit_tests/hyperion/experiment_plans/conftest.py index f46a0b9f4..8b3336331 100644 --- a/tests/unit_tests/hyperion/experiment_plans/conftest.py +++ b/tests/unit_tests/hyperion/experiment_plans/conftest.py @@ -12,6 +12,7 @@ from dodal.devices.eiger import EigerDetector from dodal.devices.fast_grid_scan import PandAFastGridScan, ZebraFastGridScan from dodal.devices.flux import Flux +from dodal.devices.i03.beamstop import Beamstop from dodal.devices.oav.oav_detector import OAV from dodal.devices.oav.pin_image_recognition import PinTipDetection from dodal.devices.robot import BartRobot @@ -25,6 +26,13 @@ from ophyd_async.fastcs.panda import HDFPanda from ophyd_async.testing import set_mock_value +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( + GridscanISPyBCallback, +) +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( + IspybIds, + StoreInIspyb, +) from mx_bluesky.hyperion.experiment_plans.common.xrc_result import XRayCentreResult from mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan import ( GridDetectThenXRayCentreComposite, @@ -38,13 +46,6 @@ from mx_bluesky.hyperion.external_interaction.callbacks.common.callback_util import ( create_gridscan_callbacks, ) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, -) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import ( - IspybIds, - StoreInIspyb, -) from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan @@ -107,10 +108,12 @@ def make_event_doc(data, descriptor="abc123") -> Event: def grid_detect_devices( aperture_scatterguard: ApertureScatterguard, backlight: Backlight, + beamstop_i03: Beamstop, detector_motion: DetectorMotion, eiger: EigerDetector, smargon: Smargon, oav: OAV, + ophyd_pin_tip_detection: PinTipDetection, zocalo: ZocaloResults, synchrotron: Synchrotron, fast_grid_scan: ZebraFastGridScan, @@ -126,12 +129,13 @@ def grid_detect_devices( aperture_scatterguard=aperture_scatterguard, attenuator=attenuator, backlight=backlight, + beamstop=beamstop_i03, detector_motion=detector_motion, eiger=eiger, zebra_fast_grid_scan=fast_grid_scan, flux=MagicMock(spec=Flux), oav=oav, - pin_tip_detection=MagicMock(spec=PinTipDetection), + pin_tip_detection=ophyd_pin_tip_detection, smargon=smargon, synchrotron=synchrotron, s4_slit_gaps=MagicMock(spec=S4SlitGaps), @@ -184,7 +188,7 @@ def run_generic_ispyb_handler_setup( ispyb_handler.activity_gated_start( { "subplan_name": CONST.PLAN.GRIDSCAN_OUTER, - "hyperion_parameters": params.model_dump_json(), + "mx_bluesky_parameters": params.model_dump_json(), } # type: ignore ) ispyb_handler.activity_gated_descriptor( @@ -229,14 +233,14 @@ def modified_store_grid_scan_mock(*args, dcids=(0, 0), dcgid=0, **kwargs): def mock_subscriptions(test_fgs_params): with ( patch( - "mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback.ZocaloTrigger", + "mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback.ZocaloTrigger", modified_interactor_mock, ), patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb.append_to_comment" + "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb.append_to_comment" ), patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb.begin_deposition", + "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb.begin_deposition", new=MagicMock( return_value=IspybIds( data_collection_ids=(0, 0), data_collection_group_id=0 @@ -244,7 +248,7 @@ def mock_subscriptions(test_fgs_params): ), ), patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb.update_deposition", + "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb.update_deposition", new=MagicMock( return_value=IspybIds( data_collection_ids=(0, 0), @@ -278,6 +282,7 @@ def robot_load_composite( eiger, xbpm_feedback, attenuator, + beamstop_i03, fast_grid_scan, undulator, undulator_dcm, @@ -303,6 +308,7 @@ def robot_load_composite( attenuator=attenuator, aperture_scatterguard=aperture_scatterguard, backlight=backlight, + beamstop=beamstop_i03, detector_motion=detector_motion, eiger=eiger, zebra_fast_grid_scan=fast_grid_scan, diff --git a/tests/unit_tests/hyperion/experiment_plans/test_flyscan_xray_centre_plan.py b/tests/unit_tests/hyperion/experiment_plans/test_flyscan_xray_centre_plan.py index 63f18f137..d9bb63011 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_flyscan_xray_centre_plan.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_flyscan_xray_centre_plan.py @@ -24,11 +24,31 @@ from ophyd_async.fastcs.panda import DatasetTable, PandaHdf5DatasetType from ophyd_async.testing import set_mock_value +from mx_bluesky.common.external_interaction.callbacks.common.logging_callback import ( + VerbosePlanExecutionLoggingCallback, +) +from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import ( + PlanReactiveCallback, +) +from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import ( + ZocaloCallback, +) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( + GridscanISPyBCallback, + ispyb_activation_wrapper, +) +from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import ( + GridscanNexusFileCallback, +) +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( + IspybIds, +) +from mx_bluesky.common.utils.exceptions import WarningException +from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER from mx_bluesky.hyperion.device_setup_plans.read_hardware_for_setup import ( read_hardware_during_collection, read_hardware_pre_collection, ) -from mx_bluesky.hyperion.exceptions import WarningException from mx_bluesky.hyperion.experiment_plans.common.xrc_result import XRayCentreResult from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import ( CrystalNotFoundException, @@ -47,27 +67,7 @@ from mx_bluesky.hyperion.external_interaction.callbacks.common.callback_util import ( create_gridscan_callbacks, ) -from mx_bluesky.hyperion.external_interaction.callbacks.logging_callback import ( - VerbosePlanExecutionLoggingCallback, -) -from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import ( - PlanReactiveCallback, -) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, - ispyb_activation_wrapper, -) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.nexus_callback import ( - GridscanNexusFileCallback, -) -from mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback import ( - ZocaloCallback, -) from mx_bluesky.hyperion.external_interaction.config_server import HyperionFeatureFlags -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import ( - IspybIds, -) -from mx_bluesky.hyperion.log import ISPYB_LOGGER from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan from tests.conftest import ( @@ -75,14 +75,13 @@ create_dummy_scan_spec, ) -from ....conftest import simulate_xrc_result +from ....conftest import TestData, simulate_xrc_result from ....system_tests.hyperion.external_interaction.conftest import ( TEST_RESULT_BELOW_THRESHOLD, TEST_RESULT_LARGE, TEST_RESULT_MEDIUM, TEST_RESULT_SMALL, ) -from ..external_interaction.callbacks.conftest import TestData from .conftest import ( assert_event, mock_zocalo_trigger, @@ -129,7 +128,7 @@ def ispyb_plan(test_fgs_params: HyperionThreeDGridScan): @bpp.run_decorator( # attach experiment metadata to the start document md={ "subplan_name": CONST.PLAN.GRIDSCAN_OUTER, - "hyperion_parameters": test_fgs_params.model_dump_json(), + "mx_bluesky_parameters": test_fgs_params.model_dump_json(), } ) def standalone_read_hardware_for_ispyb( @@ -169,7 +168,7 @@ def _custom_msg(command_name: str): @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb", + "mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb", modified_store_grid_scan_mock, ) class TestFlyscanXrayCentrePlan: @@ -202,9 +201,7 @@ def test_when_run_gridscan_called_ispyb_deposition_made_and_records_errors( error = None with pytest.raises(FailedStatus) as exc: - with patch.object( - fake_fgs_composite.sample_motors.omega, "set" - ) as mock_set: + with patch.object(fake_fgs_composite.smargon.omega, "set") as mock_set: error = AssertionError("Test Exception") mock_set.return_value = FailedStatus(error) @@ -266,7 +263,7 @@ def test_read_hardware_for_ispyb_updates_from_ophyd_devices( ) ) - test_ispyb_callback = PlanReactiveCallback(ISPYB_LOGGER) + test_ispyb_callback = PlanReactiveCallback(ISPYB_ZOCALO_CALLBACK_LOGGER) test_ispyb_callback.active = True with patch.multiple( @@ -359,7 +356,10 @@ def plan(): actual = x_ray_centre_event_handler.xray_centre_results expected = XRayCentreResult( centre_of_mass_mm=np.array([0.05, 0.15, 0.25]), - bounding_box_mm=(np.array([0.2, 0.2, 0.2]), np.array([0.8, 0.8, 0.7])), + bounding_box_mm=( + np.array([0.15, 0.15, 0.15]), + np.array([0.75, 0.75, 0.65]), + ), max_count=105062, total_count=2387574, ) @@ -413,7 +413,7 @@ def test_results_adjusted_and_passed_to_move_xyz( move_aperture.assert_has_calls([ap_call_large, ap_call_large, ap_call_medium]) mv_to_centre = call( - fgs_composite_with_panda_pcap.sample_motors, + fgs_composite_with_panda_pcap.smargon, 0.05, pytest.approx(0.15), 0.25, @@ -436,21 +436,21 @@ def test_results_passed_to_move_motors( motor_position = test_fgs_params.FGS_params.grid_position_to_motor_position( np.array([1, 2, 3]) ) - RE(move_x_y_z(fake_fgs_composite.sample_motors, *motor_position)) + RE(move_x_y_z(fake_fgs_composite.smargon, *motor_position)) bps_abs_set.assert_has_calls( [ call( - fake_fgs_composite.sample_motors.x, + fake_fgs_composite.smargon.x, motor_position[0], group="move_x_y_z", ), call( - fake_fgs_composite.sample_motors.y, + fake_fgs_composite.smargon.y, motor_position[1], group="move_x_y_z", ), call( - fake_fgs_composite.sample_motors.z, + fake_fgs_composite.smargon.z, motor_position[2], group="move_x_y_z", ), @@ -471,7 +471,7 @@ def test_results_passed_to_move_motors( autospec=True, ) @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback.ZocaloTrigger", + "mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback.ZocaloTrigger", modified_interactor_mock, ) def test_individual_plans_triggered_once_and_only_once_in_composite_run( @@ -755,7 +755,7 @@ def test_GIVEN_scan_not_valid_THEN_wait_for_GRIDSCAN_raises_and_sleeps_called( autospec=True, ) @patch( - "mx_bluesky.hyperion.external_interaction.nexus.write_nexus.NexusWriter", + "mx_bluesky.common.external_interaction.nexus.write_nexus.NexusWriter", autospec=True, spec_set=True, ) @@ -796,11 +796,11 @@ def test_when_grid_scan_ran_then_eiger_disarmed_before_zocalo_end( with ( patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter.create_nexus_file", + "mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter.create_nexus_file", autospec=True, ), patch( - "mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback.ZocaloTrigger", + "mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback.ZocaloTrigger", lambda _: modified_interactor_mock(mock_parent.run_end), ), ): @@ -969,7 +969,7 @@ class CompleteException(Exception): autospec=True, ) @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback.ZocaloTrigger", + "mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback.ZocaloTrigger", autospec=True, ) @patch( diff --git a/tests/unit_tests/hyperion/experiment_plans/test_grid_detect_then_xray_centre_plan.py b/tests/unit_tests/hyperion/experiment_plans/test_grid_detect_then_xray_centre_plan.py index df4eab13d..e3bc7fcc9 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_grid_detect_then_xray_centre_plan.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_grid_detect_then_xray_centre_plan.py @@ -11,55 +11,31 @@ from dodal.devices.aperturescatterguard import ApertureValue from dodal.devices.backlight import BacklightPosition from dodal.devices.oav.oav_parameters import OAVParameters +from dodal.devices.oav.pin_image_recognition import PinTipDetection from ophyd_async.testing import get_mock_put, set_mock_value +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( + ispyb_activation_wrapper, +) from mx_bluesky.common.parameters.gridscan import GridScanWithEdgeDetect from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import ( _fire_xray_centre_result_event, ) from mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan import ( GridDetectThenXRayCentreComposite, - OavGridDetectionComposite, detect_grid_and_do_gridscan, grid_detect_then_xray_centre, ) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( - ispyb_activation_wrapper, -) from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.gridscan import ( HyperionThreeDGridScan, ) -from ..conftest import OavGridSnapshotTestEvents +from ....conftest import OavGridSnapshotTestEvents from .conftest import FLYSCAN_RESULT_LOW, FLYSCAN_RESULT_MED, sim_fire_event_on_open_run -def _fake_grid_detection( - devices: OavGridDetectionComposite, - parameters: OAVParameters, - snapshot_template: str, - snapshot_dir: str, - grid_width_microns: float = 0, - box_size_um: float = 0.0, -): - oav = devices.oav - set_mock_value(oav.grid_snapshot.box_width, 635) - # first grid detection: x * y - set_mock_value(oav.grid_snapshot.num_boxes_x, 10) - set_mock_value(oav.grid_snapshot.num_boxes_y, 4) - yield from bps.create(CONST.DESCRIPTORS.OAV_GRID_SNAPSHOT_TRIGGERED) - yield from bps.read(oav) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809 - yield from bps.read(devices.smargon) - yield from bps.save() - - # second grid detection: x * z, so num_boxes_y refers to smargon z - set_mock_value(oav.grid_snapshot.num_boxes_x, 10) - set_mock_value(oav.grid_snapshot.num_boxes_y, 1) - yield from bps.create(CONST.DESCRIPTORS.OAV_GRID_SNAPSHOT_TRIGGERED) - yield from bps.read(oav) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809 - yield from bps.read(devices.smargon) - yield from bps.save() +def _fake_flyscan(*args): yield from _fire_xray_centre_result_event([FLYSCAN_RESULT_MED, FLYSCAN_RESULT_LOW]) @@ -84,24 +60,18 @@ def grid_detect_devices_with_oav_config_params( return grid_detect_devices -@patch( - "mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan.grid_detection_plan", - autospec=True, -) @patch( "mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan.flyscan_xray_centre_no_move", autospec=True, ) async def test_detect_grid_and_do_gridscan( mock_flyscan: MagicMock, - mock_grid_detection_plan: MagicMock, + pin_tip_detection_with_found_pin: PinTipDetection, grid_detect_devices_with_oav_config_params: GridDetectThenXRayCentreComposite, RE: RunEngine, test_full_grid_scan_params: GridScanWithEdgeDetect, test_config_files: dict, ): - mock_grid_detection_plan.side_effect = _fake_grid_detection - composite = grid_detect_devices_with_oav_config_params RE( @@ -112,8 +82,6 @@ async def test_detect_grid_and_do_gridscan( test_full_grid_scan_params, ) ) - # Verify we called the grid detection plan - mock_grid_detection_plan.assert_called_once() # Check backlight was moved OUT get_mock_put(composite.backlight.position).assert_called_once_with( @@ -141,25 +109,20 @@ def _do_detect_grid_and_gridscan_then_wait_for_backlight( yield from bps.wait(CONST.WAIT.GRID_READY_FOR_DC) -@patch( - "mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan.grid_detection_plan", - autospec=True, -) @patch( "mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan.flyscan_xray_centre_no_move", autospec=True, ) def test_when_full_grid_scan_run_then_parameters_sent_to_fgs_as_expected( mock_flyscan: MagicMock, - mock_grid_detection_plan: MagicMock, grid_detect_devices_with_oav_config_params: GridDetectThenXRayCentreComposite, RE: RunEngine, test_full_grid_scan_params: GridScanWithEdgeDetect, test_config_files: dict, + pin_tip_detection_with_found_pin: PinTipDetection, ): oav_params = OAVParameters("xrayCentring", test_config_files["oav_config_json"]) - mock_grid_detection_plan.side_effect = _fake_grid_detection RE( ispyb_activation_wrapper( detect_grid_and_do_gridscan( @@ -173,10 +136,9 @@ def test_when_full_grid_scan_run_then_parameters_sent_to_fgs_as_expected( params: HyperionThreeDGridScan = mock_flyscan.call_args[0][1] - assert params.detector_params.num_triggers == 50 - - assert params.FGS_params.x_axis.full_steps == 10 - assert params.FGS_params.y_axis.end == pytest.approx(1.511, 0.001) + assert params.detector_params.num_triggers == 180 + assert params.FGS_params.x_axis.full_steps == 15 + assert params.FGS_params.y_axis.end == pytest.approx(-0.0649, 0.001) # Parameters can be serialized params.model_dump_json() @@ -241,22 +203,18 @@ def test_detect_grid_and_do_gridscan_does_not_activate_ispyb_callback( "mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan.change_aperture_then_move_to_xtal", autospec=True, ) -@patch( - "mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan.grid_detection_plan", - autospec=True, - side_effect=_fake_grid_detection, -) @patch( "mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan.flyscan_xray_centre_no_move", autospec=True, + side_effect=_fake_flyscan, ) def test_grid_detect_then_xray_centre_centres_on_the_first_flyscan_result( mock_flyscan: MagicMock, - mock_grid_detection_plan: MagicMock, mock_change_aperture_then_move_to_xtal: MagicMock, grid_detect_devices_with_oav_config_params: GridDetectThenXRayCentreComposite, test_full_grid_scan_params: GridScanWithEdgeDetect, test_config_files: dict[str, str], + pin_tip_detection_with_found_pin: PinTipDetection, RE: RunEngine, ): RE( @@ -267,6 +225,7 @@ def test_grid_detect_then_xray_centre_centres_on_the_first_flyscan_result( ) ) mock_change_aperture_then_move_to_xtal.assert_called_once() + assert ( mock_change_aperture_then_move_to_xtal.mock_calls[0].args[0] == FLYSCAN_RESULT_MED diff --git a/tests/unit_tests/hyperion/experiment_plans/test_grid_detection_plan.py b/tests/unit_tests/hyperion/experiment_plans/test_grid_detection_plan.py index e0ff13ad4..40be33a4b 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_grid_detection_plan.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_grid_detection_plan.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import Any, Literal from unittest.mock import DEFAULT, AsyncMock, MagicMock, patch @@ -17,19 +18,19 @@ from numpy._typing._array_like import NDArray from ophyd_async.testing import set_mock_value -from mx_bluesky.hyperion.exceptions import WarningException -from mx_bluesky.hyperion.experiment_plans.oav_grid_detection_plan import ( - OavGridDetectionComposite, - get_min_and_max_y_of_pin, - grid_detection_plan, -) -from mx_bluesky.hyperion.external_interaction.callbacks.grid_detection_callback import ( +from mx_bluesky.common.external_interaction.callbacks.common.grid_detection_callback import ( GridDetectionCallback, ) -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( +from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import ( GridscanISPyBCallback, ispyb_activation_wrapper, ) +from mx_bluesky.common.utils.exceptions import WarningException +from mx_bluesky.hyperion.experiment_plans.oav_grid_detection_plan import ( + OavGridDetectionComposite, + get_min_and_max_y_of_pin, + grid_detection_plan, +) from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan from .conftest import assert_event @@ -88,11 +89,11 @@ def fake_devices( yield composite, mock_save_image -def do_grid_and_edge_detect(composite, parameters): +def do_grid_and_edge_detect(composite, parameters, tmp_dir): yield from grid_detection_plan( composite, parameters=parameters, - snapshot_dir="tmp", + snapshot_dir=f"{tmp_dir}", snapshot_template="test_{angle}", grid_width_microns=161.2, box_size_um=20, @@ -108,13 +109,14 @@ def test_grid_detection_plan_runs_and_triggers_snapshots( RE: RunEngine, test_config_files: dict[str, str], fake_devices: tuple[OavGridDetectionComposite, MagicMock], + tmp_path: Path, ): params = OAVParameters("loopCentring", test_config_files["oav_config_json"]) composite, image_save = fake_devices composite.oav.grid_snapshot._save_image = (mock_save := AsyncMock()) - RE(bpp.run_wrapper(do_grid_and_edge_detect(composite, params))) + RE(bpp.run_wrapper(do_grid_and_edge_detect(composite, params, tmp_path))) assert image_save.await_count == 4 assert mock_save.call_count == 2 @@ -129,6 +131,7 @@ async def test_grid_detection_plan_gives_warning_error_if_tip_not_found( RE: RunEngine, test_config_files: dict[str, str], fake_devices: tuple[OavGridDetectionComposite, MagicMock], + tmp_path: Path, ): composite, _ = fake_devices @@ -145,7 +148,7 @@ async def test_grid_detection_plan_gives_warning_error_if_tip_not_found( params = OAVParameters("loopCentring", test_config_files["oav_config_json"]) with pytest.raises(WarningException) as excinfo: - RE(do_grid_and_edge_detect(composite, params)) + RE(do_grid_and_edge_detect(composite, params, tmp_path)) assert "No pin found" in excinfo.value.args[0] @@ -159,6 +162,7 @@ async def test_given_when_grid_detect_then_start_position_as_expected( fake_devices: tuple[OavGridDetectionComposite, MagicMock], RE: RunEngine, test_config_files: dict[str, str], + tmp_path: Path, ): params = OAVParameters("loopCentring", test_config_files["oav_config_json"]) box_size_um = 0.2 @@ -174,7 +178,7 @@ def decorated(): yield from grid_detection_plan( composite, parameters=params, - snapshot_dir="tmp", + snapshot_dir=f"{tmp_path}", snapshot_template="test_{angle}", grid_width_microns=161.2, box_size_um=box_size_um, @@ -204,13 +208,14 @@ def test_when_grid_detection_plan_run_twice_then_values_do_not_persist_in_callba fake_devices: tuple[OavGridDetectionComposite, MagicMock], RE: RunEngine, test_config_files: dict[str, str], + tmp_path: Path, ): params = OAVParameters("loopCentring", test_config_files["oav_config_json"]) composite, _ = fake_devices for _ in range(2): - RE(bpp.run_wrapper(do_grid_and_edge_detect(composite, params))) + RE(bpp.run_wrapper(do_grid_and_edge_detect(composite, params, tmp_path))) @patch( @@ -223,6 +228,7 @@ async def test_when_grid_detection_plan_run_then_ispyb_callback_gets_correct_val RE: RunEngine, test_config_files: dict[str, str], test_fgs_params: HyperionThreeDGridScan, + tmp_path: Path, ): params = OAVParameters("loopCentring", test_config_files["oav_config_json"]) composite, _ = fake_devices @@ -232,7 +238,7 @@ async def test_when_grid_detection_plan_run_then_ispyb_callback_gets_correct_val with patch.multiple(cb, activity_gated_start=DEFAULT, activity_gated_event=DEFAULT): RE( ispyb_activation_wrapper( - do_grid_and_edge_detect(composite, params), test_fgs_params + do_grid_and_edge_detect(composite, params, tmp_path), test_fgs_params ) ) @@ -250,9 +256,9 @@ async def test_when_grid_detection_plan_run_then_ispyb_callback_gets_correct_val "oav-grid_snapshot-box_width": pytest.approx(12, abs=1), "oav-microns_per_pixel_x": 1.58, "oav-microns_per_pixel_y": 1.58, - "oav-grid_snapshot-last_path_full_overlay": "tmp/test_0_grid_overlay.png", - "oav-grid_snapshot-last_path_outer": "tmp/test_0_outer_overlay.png", - "oav-grid_snapshot-last_saved_path": "tmp/test_0.png", + "oav-grid_snapshot-last_path_full_overlay": f"{tmp_path}/test_0_grid_overlay.png", + "oav-grid_snapshot-last_path_outer": f"{tmp_path}/test_0_outer_overlay.png", + "oav-grid_snapshot-last_saved_path": f"{tmp_path}/test_0.png", }, ) assert_event( @@ -265,9 +271,9 @@ async def test_when_grid_detection_plan_run_then_ispyb_callback_gets_correct_val "oav-grid_snapshot-box_width": pytest.approx(12, abs=1), "oav-microns_per_pixel_x": 1.58, "oav-microns_per_pixel_y": 1.58, - "oav-grid_snapshot-last_path_full_overlay": "tmp/test_90_grid_overlay.png", - "oav-grid_snapshot-last_path_outer": "tmp/test_90_outer_overlay.png", - "oav-grid_snapshot-last_saved_path": "tmp/test_90.png", + "oav-grid_snapshot-last_path_full_overlay": f"{tmp_path}/test_90_grid_overlay.png", + "oav-grid_snapshot-last_path_outer": f"{tmp_path}/test_90_outer_overlay.png", + "oav-grid_snapshot-last_saved_path": f"{tmp_path}/test_90.png", }, ) @@ -282,6 +288,7 @@ def test_when_grid_detection_plan_run_then_grid_detection_callback_gets_correct_ RE: RunEngine, test_config_files: dict[str, str], test_fgs_params: HyperionThreeDGridScan, + tmp_path: Path, ): params = OAVParameters("loopCentring", test_config_files["oav_config_json"]) composite, _ = fake_devices @@ -291,7 +298,7 @@ def test_when_grid_detection_plan_run_then_grid_detection_callback_gets_correct_ RE( ispyb_activation_wrapper( - do_grid_and_edge_detect(composite, params), test_fgs_params + do_grid_and_edge_detect(composite, params, tmp_path), test_fgs_params ) ) @@ -327,6 +334,7 @@ async def test_when_detected_grid_has_odd_y_steps_then_add_a_y_step_and_shift_gr sim_run_engine: RunEngineSimulator, test_config_files: dict[str, str], odd: bool, + tmp_path: Path, ): composite, _ = fake_devices params = OAVParameters("loopCentring", test_config_files["oav_config_json"]) @@ -367,14 +375,16 @@ def record_set(msg: Msg): sim_run_engine.add_read_handler_for(composite.oav.microns_per_pixel_x, 1.58) sim_run_engine.add_read_handler_for(composite.oav.microns_per_pixel_y, 1.58) - msgs = sim_run_engine.simulate_plan(do_grid_and_edge_detect(composite, params)) + msgs = sim_run_engine.simulate_plan( + do_grid_and_edge_detect(composite, params, tmp_path) + ) expected_min_y = initial_min_y - box_size_y_pixels / 2 if odd else initial_min_y expected_y_steps = 2 if odd: fake_logger.debug.assert_called_once_with( - f"Forcing number of rows in first grid to be even: Adding an extra row onto bottom of first grid and shifting grid upwards by {box_size_y_pixels/2}" + f"Forcing number of rows in first grid to be even: Adding an extra row onto bottom of first grid and shifting grid upwards by {box_size_y_pixels / 2}" ) else: fake_logger.debug.assert_not_called() diff --git a/tests/unit_tests/hyperion/experiment_plans/test_load_centre_collect_full_plan.py b/tests/unit_tests/hyperion/experiment_plans/test_load_centre_collect_full_plan.py index 31e3d6f96..5350ca885 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_load_centre_collect_full_plan.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_load_centre_collect_full_plan.py @@ -1,5 +1,6 @@ import dataclasses from collections.abc import Sequence +from typing import Any from unittest.mock import AsyncMock, MagicMock, call, patch import numpy @@ -7,6 +8,7 @@ from bluesky.protocols import Location from bluesky.simulators import RunEngineSimulator, assert_message_and_return_remaining from bluesky.utils import Msg +from dodal.devices.i03.beamstop import BeamstopPositions from dodal.devices.oav.oav_parameters import OAVParameters from dodal.devices.oav.pin_image_recognition import PinTipDetection from dodal.devices.synchrotron import SynchrotronMode @@ -15,7 +17,8 @@ from pydantic import ValidationError from mx_bluesky.common.parameters.robot_load import RobotLoadAndEnergyChange -from mx_bluesky.hyperion.exceptions import WarningException +from mx_bluesky.common.utils.exceptions import WarningException +from mx_bluesky.hyperion.device_setup_plans.check_beamstop import BeamstopException from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import ( CrystalNotFoundException, ) @@ -44,6 +47,8 @@ sim_fire_event_on_open_run, ) +GOOD_TEST_LOAD_CENTRE_COLLECT_MULTI_ROTATION = "tests/test_data/parameter_json_files/good_test_load_centre_collect_params_multi_rotation.json" + def find_a_pin(pin_tip_detection): def set_good_position(): @@ -55,7 +60,10 @@ def set_good_position(): @pytest.fixture def composite( - robot_load_composite, fake_create_rotation_devices, sim_run_engine + robot_load_composite, + fake_create_rotation_devices, + pin_tip_detection_with_found_pin, + sim_run_engine, ) -> LoadCentreCollectComposite: rlaec_args = { field.name: getattr(robot_load_composite, field.name) @@ -67,6 +75,7 @@ def composite( } composite = LoadCentreCollectComposite(**(rlaec_args | rotation_args)) + composite.pin_tip_detection = pin_tip_detection_with_found_pin minaxis = Location(setpoint=-2, readback=-2) maxaxis = Location(setpoint=2, readback=2) tip_x_px, tip_y_px, top_edge_array, bottom_edge_array = pin_tip_edge_data() @@ -109,12 +118,15 @@ def composite( sim_run_engine.add_read_handler_for( composite.pin_tip_detection.triggered_tip, (tip_x_px, tip_y_px) ) - composite.pin_tip_detection.trigger = MagicMock( - side_effect=find_a_pin(composite.pin_tip_detection) - ) return composite +@pytest.fixture +def load_centre_collect_params_multi(): + params = raw_params_from_file(GOOD_TEST_LOAD_CENTRE_COLLECT_MULTI_ROTATION) + return LoadCentreCollect(**params) + + @pytest.fixture def load_centre_collect_params(): params = raw_params_from_file( @@ -159,6 +171,105 @@ def test_can_serialize_load_centre_collect_params(load_centre_collect_params): load_centre_collect_params.model_dump_json() +def test_params_good_multi_rotation_load_centre_collect_params( + load_centre_collect_params_multi, +): + params = raw_params_from_file(GOOD_TEST_LOAD_CENTRE_COLLECT_MULTI_ROTATION) + LoadCentreCollect(**params) + + +def test_params_with_varying_frames_per_rotation_is_rejected(): + params = raw_params_from_file(GOOD_TEST_LOAD_CENTRE_COLLECT_MULTI_ROTATION) + params["multi_rotation_scan"]["rotation_scans"][0]["scan_width_deg"] = 180 + params["multi_rotation_scan"]["rotation_scans"][1]["scan_width_deg"] = 90 + with pytest.raises( + ValidationError, + match="Sweeps with different numbers of frames are not supported.", + ): + LoadCentreCollect(**params) + + +@pytest.mark.parametrize( + "param, value", + [ + ["x_start_um", 1.0], + ["y_start_um", 2.0], + ["z_start_um", 3.0], + ], +) +def test_params_with_start_xyz_is_rejected(param: str, value: float): + params = raw_params_from_file(GOOD_TEST_LOAD_CENTRE_COLLECT_MULTI_ROTATION) + params["multi_rotation_scan"]["rotation_scans"][1][param] = value + with pytest.raises( + ValidationError, + match="Specifying start xyz for sweeps is not supported in combination with centring.", + ): + LoadCentreCollect(**params) + + +def test_params_with_different_energy_for_rotation_gridscan_rejected(): + params = raw_params_from_file(GOOD_TEST_LOAD_CENTRE_COLLECT_MULTI_ROTATION) + params["multi_rotation_scan"]["demand_energy_ev"] = 11000 + params["robot_load_then_centre"]["demand_energy_ev"] = 11100 + with pytest.raises( + ValidationError, + match="Setting a different energy for gridscan and rotation is not supported.", + ): + LoadCentreCollect(**params) + + +@pytest.mark.parametrize( + "key, value", + [ + # MxBlueskyParameters + ["parameter_model_version", "1.2.3"], + # WithSample + ["sample_id", 12345], + ["sample_puck", 1], + ["sample_pin", 2], + # WithVisit + ["beamline", "i03"], + ["visit", "cm12345"], + ["insertion_prefix", "SR03"], + ["detector_distance_mm", 123], + ["det_dist_to_beam_converter_path", "/foo/bar"], + ], +) +def test_params_with_unexpected_info_in_robot_load_rejected(key: str, value: Any): + params = raw_params_from_file(GOOD_TEST_LOAD_CENTRE_COLLECT_MULTI_ROTATION) + params["robot_load_then_centre"][key] = value + with pytest.raises( + ValidationError, match="Unexpected keys in robot_load_then_centre" + ): + LoadCentreCollect(**params) + + +@pytest.mark.parametrize( + "key, value", + [ + # MxBlueskyParameters + ["parameter_model_version", "1.2.3"], + # WithSample + ["sample_id", 12345], + ["sample_puck", 1], + ["sample_pin", 2], + # WithVisit + ["beamline", "i03"], + ["visit", "cm12345"], + ["insertion_prefix", "SR03"], + ["detector_distance_mm", 123], + ["det_dist_to_beam_converter_path", "/foo/bar"], + ], +) +def test_params_with_unexpected_info_in_multi_rotation_scan_rejected( + key: str, value: Any +): + params = raw_params_from_file(GOOD_TEST_LOAD_CENTRE_COLLECT_MULTI_ROTATION) + params["multi_rotation_scan"][key] = value + with pytest.raises(ValidationError, match="Unexpected keys in multi_rotation_scan"): + LoadCentreCollect(**params) + + def test_can_serialize_load_centre_collect_robot_load_params( load_centre_collect_params, ): @@ -334,6 +445,24 @@ def test_load_centre_collect_full_plan_skips_collect_if_no_diffraction( mock_rotation_scan.assert_not_called() +def test_load_centre_collect_fails_with_exception_when_no_beamstop( + composite: LoadCentreCollectComposite, + load_centre_collect_params: LoadCentreCollect, + oav_parameters_for_rotation: OAVParameters, + sim_run_engine: RunEngineSimulator, +): + sim_run_engine.add_read_handler_for( + composite.beamstop.selected_pos, BeamstopPositions.UNKNOWN + ) + + with pytest.raises(BeamstopException): + sim_run_engine.simulate_plan( + load_centre_collect_full( + composite, load_centre_collect_params, oav_parameters_for_rotation + ) + ) + + def test_can_deserialize_top_n_by_max_count_params( load_centre_collect_with_top_n_params, ): diff --git a/tests/unit_tests/hyperion/experiment_plans/test_multi_rotation_scan_plan.py b/tests/unit_tests/hyperion/experiment_plans/test_multi_rotation_scan_plan.py index 9173296bf..696931781 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_multi_rotation_scan_plan.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_multi_rotation_scan_plan.py @@ -17,6 +17,8 @@ from dodal.devices.synchrotron import SynchrotronMode from ophyd_async.testing import set_mock_value +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import StoreInIspyb +from mx_bluesky.common.external_interaction.nexus.nexus_utils import AxisDirection from mx_bluesky.hyperion.experiment_plans.rotation_scan_plan import ( RotationScanComposite, calculate_motion_profile, @@ -28,7 +30,6 @@ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.nexus_callback import ( RotationNexusFileCallback, ) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import StoreInIspyb from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.rotation import MultiRotationScan, RotationScan @@ -36,10 +37,10 @@ DocumentCapturer, extract_metafile, fake_read, + mx_acquisition_from_conn, raw_params_from_file, ) from ..external_interaction.conftest import * # noqa # for fixtures -from ..external_interaction.conftest import mx_acquisition_from_conn TEST_OFFSET = 1 TEST_SHUTTER_OPENING_DEGREES = 2.5 @@ -202,9 +203,9 @@ def test_full_multi_rotation_plan_docs_emitted( assert DocumentCapturer.is_match( scan_docs[0], "start", - has_fields=["trigger_zocalo_on", "hyperion_parameters"], + has_fields=["trigger_zocalo_on", "mx_bluesky_parameters"], ) - params = RotationScan(**json.loads(scan_docs[0][1]["hyperion_parameters"])) + params = RotationScan(**json.loads(scan_docs[0][1]["mx_bluesky_parameters"])) assert params == scan assert len(events := DocumentCapturer.get_matches(scan_docs, "event")) == 3 DocumentCapturer.assert_events_and_data_in_order( @@ -257,7 +258,8 @@ def test_full_multi_rotation_plan_nexus_writer_called_correctly( test_multi_rotation_params.single_rotation_scans, strict=False, ): - assert call.args[0] == rotation_params + callback_params = call.args[0] + assert callback_params == rotation_params assert call.kwargs == { "omega_start_deg": rotation_params.omega_start_deg, "chi_start_deg": rotation_params.chi_start_deg, @@ -265,7 +267,9 @@ def test_full_multi_rotation_plan_nexus_writer_called_correctly( "vds_start_index": rotation_params.nexus_vds_start_img, "full_num_of_images": test_multi_rotation_params.num_images, "meta_data_run_number": first_run_number, - "rotation_direction": rotation_params.rotation_direction, + "axis_direction": AxisDirection.NEGATIVE + if rotation_params.features.omega_flip + else AxisDirection.POSITIVE, } @@ -276,6 +280,7 @@ def test_full_multi_rotation_plan_nexus_writer_called_correctly( def test_full_multi_rotation_plan_nexus_files_written_correctly( _, RE: RunEngine, + feature_flags_update_with_omega_flip: MagicMock, test_multi_rotation_params: MultiRotationScan, fake_create_rotation_devices: RotationScanComposite, oav_parameters_for_rotation: OAVParameters, @@ -320,7 +325,7 @@ def _expected_dset_number(image_number: int): f"{tmpdir}/{meta_filename}", ) for i, scan in enumerate(multi_params.single_rotation_scans): - with h5py.File(f"{tmpdir}/{prefix}_{i+1}.nxs", "r") as written_nexus_file: + with h5py.File(f"{tmpdir}/{prefix}_{i + 1}.nxs", "r") as written_nexus_file: # check links go to the right file: detector_specific = written_nexus_file[ "entry/instrument/detector/detectorSpecific" @@ -385,7 +390,10 @@ def _expected_dset_number(image_number: int): h5py.Dataset, ) assert isinstance(omega_vec := omega_transform.attrs["vector"], np.ndarray) - assert tuple(omega_vec) == (1.0 * scan.rotation_direction.multiplier, 0, 0) + omega_flip = ( + feature_flags_update_with_omega_flip.mock_calls[0].args[0].omega_flip + ) + assert tuple(omega_vec) == (-1.0 if omega_flip else 1.0, 0, 0) @patch( diff --git a/tests/unit_tests/hyperion/experiment_plans/test_optimise_attenuation_plan.py b/tests/unit_tests/hyperion/experiment_plans/test_optimise_attenuation_plan.py index d31ca0801..cf526f220 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_optimise_attenuation_plan.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_optimise_attenuation_plan.py @@ -10,6 +10,7 @@ from ophyd_async.core import AsyncStatus from ophyd_async.testing import set_mock_value +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.experiment_plans import optimise_attenuation_plan from mx_bluesky.hyperion.experiment_plans.optimise_attenuation_plan import ( AttenuationOptimisationFailedException, @@ -23,7 +24,6 @@ is_deadtime_optimised, total_counts_optimisation, ) -from mx_bluesky.hyperion.log import LOGGER @pytest.fixture 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 1e823779e..6a1e647a1 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 @@ -7,12 +7,14 @@ from dodal.devices.aperturescatterguard import ApertureValue from dodal.devices.backlight import BacklightPosition from dodal.devices.detector.detector_motion import ShutterState +from dodal.devices.i03.beamstop import BeamstopPositions from dodal.devices.smargon import Smargon from dodal.devices.synchrotron import SynchrotronMode from mx_bluesky.common.parameters.gridscan import ( PinTipCentreThenXrayCentre, ) +from mx_bluesky.hyperion.device_setup_plans.check_beamstop import BeamstopException from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import ( _fire_xray_centre_result_event, ) @@ -357,3 +359,19 @@ def test_pin_centre_then_xray_centre_plan_goes_to_the_starting_chi_and_phi( msgs = assert_message_and_return_remaining( msgs, lambda msg: msg.command == "pin_tip_centre_plan" ) + + +def test_pin_tip_centre_then_xray_centre_fails_with_exception_when_no_beamstop( + sim_run_engine: RunEngineSimulator, + grid_detect_devices: GridDetectThenXRayCentreComposite, + test_pin_centre_then_xray_centre_params: PinTipCentreThenXrayCentre, +): + sim_run_engine.add_read_handler_for( + grid_detect_devices.beamstop.selected_pos, BeamstopPositions.UNKNOWN + ) + with pytest.raises(BeamstopException): + sim_run_engine.simulate_plan( + pin_tip_centre_then_xray_centre( + grid_detect_devices, test_pin_centre_then_xray_centre_params + ) + ) diff --git a/tests/unit_tests/hyperion/experiment_plans/test_pin_tip_centring.py b/tests/unit_tests/hyperion/experiment_plans/test_pin_tip_centring.py index 2b71257b6..68684bb53 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_pin_tip_centring.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_pin_tip_centring.py @@ -13,10 +13,10 @@ from ophyd.sim import NullStatus from ophyd_async.testing import get_mock_put, set_mock_value +from mx_bluesky.common.utils.exceptions import WarningException from mx_bluesky.hyperion.device_setup_plans.smargon import ( move_smargon_warn_on_out_of_range, ) -from mx_bluesky.hyperion.exceptions import WarningException from mx_bluesky.hyperion.experiment_plans.pin_tip_centring_plan import ( DEFAULT_STEP_SIZE, PinTipCentringComposite, diff --git a/tests/unit_tests/hyperion/experiment_plans/test_robot_load_and_change_energy.py b/tests/unit_tests/hyperion/experiment_plans/test_robot_load_and_change_energy.py index df67eaf5d..325e8b09f 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_robot_load_and_change_energy.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_robot_load_and_change_energy.py @@ -16,6 +16,8 @@ from mx_bluesky.common.parameters.robot_load import RobotLoadAndEnergyChange from mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy import ( RobotLoadAndEnergyChangeComposite, + SampleLocation, + do_robot_load, prepare_for_robot_load, robot_load_and_change_energy_plan, take_robot_snapshots, @@ -165,6 +167,41 @@ async def test_when_prepare_for_robot_load_called_then_moves_as_expected( aperture_scatterguard.set.assert_called_once_with(ApertureValue.ROBOT_LOAD) # type: ignore +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy.set_energy_plan", + MagicMock(return_value=iter([])), +) +@patch( + "mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy.bps.trigger", +) +async def test_when_error_40_reset_robot_before_load( + mock_reset_error: MagicMock, + robot_load_and_energy_change_composite: RobotLoadAndEnergyChangeComposite, + robot_load_and_energy_change_params: RobotLoadAndEnergyChange, +): + assert robot_load_and_energy_change_params.sample_puck is not None + assert robot_load_and_energy_change_params.sample_pin is not None + + sample_location = SampleLocation( + robot_load_and_energy_change_params.sample_puck, + robot_load_and_energy_change_params.sample_pin, + ) + + demand_energy_ev = robot_load_and_energy_change_params.demand_energy_ev + + set_mock_value(robot_load_and_energy_change_composite.robot.error_code, 40) + + RE = RunEngine() + RE( + # Thawing time set to arbitrary value + do_robot_load( + robot_load_and_energy_change_composite, sample_location, demand_energy_ev, 0 + ) + ) + + mock_reset_error.assert_called_once() + + @patch( "mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback.ExpeyeInteraction" ) diff --git a/tests/unit_tests/hyperion/experiment_plans/test_robot_load_then_centre.py b/tests/unit_tests/hyperion/experiment_plans/test_robot_load_then_centre.py index 803458127..a65e7fd80 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_robot_load_then_centre.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_robot_load_then_centre.py @@ -5,12 +5,14 @@ from bluesky.run_engine import RunEngine from bluesky.simulators import RunEngineSimulator, assert_message_and_return_remaining from bluesky.utils import Msg +from dodal.devices.i03.beamstop import BeamstopPositions from dodal.devices.robot import SampleLocation from mx_bluesky.common.parameters.gridscan import ( PinTipCentreThenXrayCentre, RobotLoadThenCentre, ) +from mx_bluesky.hyperion.device_setup_plans.check_beamstop import BeamstopException from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import ( _fire_xray_centre_result_event, ) @@ -506,3 +508,27 @@ def test_robot_load_then_centre_sets_energy_when_no_robot_load_no_chi_change( messages = assert_message_and_return_remaining( messages, lambda msg: msg.command == "set_energy_plan" and msg.args[0] == 11100 ) + + +def test_tip_offset_um_passed_to_pin_tip_centre_plan( + robot_load_then_centre_params: RobotLoadThenCentre, +): + robot_load_then_centre_params.tip_offset_um = 100 + assert ( + robot_load_then_centre_params.pin_centre_then_xray_centre_params().tip_offset_um + == 100 + ) + + +def test_robot_load_then_centre_fails_with_exception_when_no_beamstop( + sim_run_engine: RunEngineSimulator, + robot_load_composite: RobotLoadThenCentreComposite, + robot_load_then_centre_params: RobotLoadThenCentre, +): + sim_run_engine.add_read_handler_for( + robot_load_composite.beamstop.selected_pos, BeamstopPositions.UNKNOWN + ) + with pytest.raises(BeamstopException): + sim_run_engine.simulate_plan( + robot_load_then_centre(robot_load_composite, robot_load_then_centre_params) + ) diff --git a/tests/unit_tests/hyperion/experiment_plans/test_rotation_scan_plan.py b/tests/unit_tests/hyperion/experiment_plans/test_rotation_scan_plan.py index af9f2c2e0..fc89a6447 100644 --- a/tests/unit_tests/hyperion/experiment_plans/test_rotation_scan_plan.py +++ b/tests/unit_tests/hyperion/experiment_plans/test_rotation_scan_plan.py @@ -1,8 +1,8 @@ from __future__ import annotations -from itertools import takewhile +from itertools import dropwhile, takewhile from typing import Any -from unittest.mock import MagicMock, call, patch +from unittest.mock import ANY, MagicMock, Mock, call, patch import pytest from bluesky.run_engine import RunEngine @@ -10,15 +10,21 @@ from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue from dodal.devices.backlight import BacklightPosition from dodal.devices.detector.detector_motion import ShutterState +from dodal.devices.i03.beamstop import BeamstopPositions from dodal.devices.oav.oav_parameters import OAVParameters from dodal.devices.smargon import Smargon from dodal.devices.synchrotron import SynchrotronMode from dodal.devices.xbpm_feedback import Pause -from dodal.devices.zebra import PC_GATE, SOFT_IN1, Zebra +from dodal.devices.zebra import PC_GATE, SOFT_IN1, RotationDirection, Zebra from dodal.devices.zebra_controlled_shutter import ZebraShutterControl -from ophyd_async.testing import get_mock_put +from ophyd_async.testing import get_mock_put, set_mock_value +from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import ( + ZocaloCallback, +) +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import IspybIds from mx_bluesky.common.parameters.constants import DocDescriptorNames +from mx_bluesky.hyperion.device_setup_plans.check_beamstop import BeamstopException from mx_bluesky.hyperion.experiment_plans.oav_snapshot_plan import ( OAV_SNAPSHOT_GROUP, ) @@ -32,10 +38,6 @@ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_callback import ( RotationISPyBCallback, ) -from mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback import ( - ZocaloCallback, -) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import IspybIds from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.rotation import RotationScan @@ -134,6 +136,7 @@ def setup_and_run_rotation_plan_for_tests_nomove( def test_rotation_scan_calculations(test_rotation_params: RotationScan): + test_rotation_params.features.omega_flip = False test_rotation_params.exposure_time_s = 0.2 test_rotation_params.omega_start_deg = 10 @@ -668,7 +671,7 @@ def test_rotation_scan_correctly_triggers_ispyb_callback( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback.ZocaloTrigger" + "mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback.ZocaloTrigger" ) @patch( "mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_callback.StoreInIspyb" @@ -702,3 +705,114 @@ def test_rotation_scan_correctly_triggers_zocalo_callback( ), ) mock_zocalo_interactor.return_value.run_start.assert_called_once() + + +def test_rotation_scan_fails_with_exception_when_no_beamstop( + sim_run_engine: RunEngineSimulator, + fake_create_rotation_devices: RotationScanComposite, + test_rotation_params: RotationScan, + oav_parameters_for_rotation: OAVParameters, +): + sim_run_engine.add_read_handler_for( + fake_create_rotation_devices.beamstop.selected_pos, BeamstopPositions.UNKNOWN + ) + with pytest.raises(BeamstopException): + sim_run_engine.simulate_plan( + rotation_scan( + fake_create_rotation_devices, + test_rotation_params, + oav_parameters_for_rotation, + ) + ) + + +@pytest.mark.parametrize( + "omega_flip, rotation_direction, expected_start_angle, " + "expected_start_angle_with_runup, expected_zebra_direction", + [ + # see https://github.com/DiamondLightSource/mx-bluesky/issues/247 + # GDA behaviour is such that positive angles in the request result in + # negative motor angles, but positive angles in the resulting nexus file + # Should replicate GDA Output exactly + [True, RotationDirection.POSITIVE, -30, -29.85, RotationDirection.NEGATIVE], + # Should replicate GDA Output, except with /entry/data/transformation/omega + # +1, 0, 0 instead of -1, 0, 0 + [False, RotationDirection.NEGATIVE, 30, 30.15, RotationDirection.NEGATIVE], + [True, RotationDirection.NEGATIVE, -30, -30.15, RotationDirection.POSITIVE], + [False, RotationDirection.POSITIVE, 30, 29.85, RotationDirection.POSITIVE], + ], +) +@patch( + "mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback.ZocaloTrigger", + MagicMock(), +) +@patch( + "mx_bluesky.hyperion.experiment_plans.rotation_scan_plan.setup_zebra_for_rotation" +) +def test_rotation_scan_plan_with_omega_flip_inverts_motor_movements_but_not_event_params( + mock_setup_zebra_for_rotation: MagicMock, + omega_flip: bool, + rotation_direction: RotationDirection, + expected_start_angle: float, + expected_start_angle_with_runup: float, + expected_zebra_direction: RotationDirection, + test_rotation_params: RotationScan, + fake_create_rotation_devices: RotationScanComposite, + oav_parameters_for_rotation: OAVParameters, + RE: RunEngine, +): + test_rotation_params.features.omega_flip = omega_flip + test_rotation_params.rotation_direction = rotation_direction + test_rotation_params.omega_start_deg = 30 + mock_callback = Mock(spec=RotationISPyBCallback) + RE.subscribe(mock_callback) + omega_put = get_mock_put(fake_create_rotation_devices.smargon.omega.user_setpoint) + set_mock_value(fake_create_rotation_devices.smargon.omega.acceleration_time, 0.1) + with ( + patch("bluesky.plan_stubs.wait", autospec=True), + patch( + "bluesky.preprocessors.__read_and_stash_a_motor", + fake_read, + ), + ): + RE( + rotation_scan( + fake_create_rotation_devices, + test_rotation_params, + oav_parameters_for_rotation, + ), + ) + + assert omega_put.mock_calls[0:5] == [ + call(0, wait=True), + call(90, wait=True), + call(180, wait=True), + call(270, wait=True), + call(expected_start_angle_with_runup, wait=True), + ] + mock_setup_zebra_for_rotation.assert_called_once_with( + fake_create_rotation_devices.zebra, + fake_create_rotation_devices.sample_shutter, + start_angle=expected_start_angle, + scan_width=180, + direction=expected_zebra_direction, + shutter_opening_deg=ANY, + shutter_opening_s=ANY, + group="setup_zebra", + ) + rotation_outer_start_event = next( + dropwhile( + lambda _: _.args[0] != "start" + or _.args[1].get("subplan_name") != CONST.PLAN.ROTATION_OUTER, + mock_callback.mock_calls, + ) + ) + event_params = RotationScan.model_validate_json( + rotation_outer_start_event.args[1]["mx_bluesky_parameters"] + ) + # event params are not transformed + assert event_params.omega_start_deg == 30 + assert event_params.rotation_direction == rotation_direction + assert event_params.rotation_increment_deg == 0.1 + assert event_params.scan_width_deg == 180 + assert event_params.features.omega_flip == omega_flip diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/conftest.py b/tests/unit_tests/hyperion/external_interaction/callbacks/conftest.py index da3a8e77a..a38310847 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/conftest.py +++ b/tests/unit_tests/hyperion/external_interaction/callbacks/conftest.py @@ -1,36 +1,6 @@ import pytest -from dodal.devices.aperturescatterguard import ApertureValue -from dodal.devices.synchrotron import SynchrotronMode -from dodal.devices.zocalo.zocalo_results import ZOCALO_READING_PLAN_NAME -from event_model.documents import Event, EventDescriptor, RunStart, RunStop -from mx_bluesky.common.parameters.constants import ( - EnvironmentConstants, - PlanNameConstants, -) from mx_bluesky.hyperion.parameters.constants import CONST -from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan -from tests.conftest import create_dummy_scan_spec - -from .....conftest import ( - default_raw_params, - generate_xrc_result_event, - raw_params_from_file, -) -from ...conftest import OavGridSnapshotTestEvents - - -def dummy_params(): - dummy_params = HyperionThreeDGridScan(**default_raw_params()) - return dummy_params - - -def dummy_params_2d(): - raw_params = raw_params_from_file( - "tests/test_data/parameter_json_files/test_gridscan_param_defaults.json" - ) - raw_params["z_steps"] = 1 - return HyperionThreeDGridScan(**raw_params) @pytest.fixture @@ -38,235 +8,5 @@ def test_rotation_start_outer_document(dummy_rotation_params): return { "uid": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", "subplan_name": CONST.PLAN.ROTATION_OUTER, - "hyperion_parameters": dummy_rotation_params.model_dump_json(), - } - - -class TestData(OavGridSnapshotTestEvents): - DUMMY_TIME_STRING: str = "1970-01-01 00:00:00" - GOOD_ISPYB_RUN_STATUS: str = "DataCollection Successful" - BAD_ISPYB_RUN_STATUS: str = "DataCollection Unsuccessful" - test_start_document: RunStart = { # type: ignore - "uid": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "time": 1666604299.6149616, - "versions": {"ophyd": "1.6.4.post76+g0895f9f", "bluesky": "1.8.3"}, - "scan_id": 1, - "plan_type": "generator", - "plan_name": CONST.PLAN.GRIDSCAN_OUTER, - "subplan_name": CONST.PLAN.GRIDSCAN_OUTER, - CONST.TRIGGER.ZOCALO: PlanNameConstants.DO_FGS, - "hyperion_parameters": dummy_params().model_dump_json(), - } - test_gridscan3d_start_document: RunStart = { # type: ignore - "uid": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "time": 1666604299.6149616, - "versions": {"ophyd": "1.6.4.post76+g0895f9f", "bluesky": "1.8.3"}, - "scan_id": 1, - "plan_type": "generator", - "plan_name": "test", - "subplan_name": CONST.PLAN.GRID_DETECT_AND_DO_GRIDSCAN, - "hyperion_parameters": dummy_params().model_dump_json(), - } - test_gridscan2d_start_document = { - "uid": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "time": 1666604299.6149616, - "versions": {"ophyd": "1.6.4.post76+g0895f9f", "bluesky": "1.8.3"}, - "scan_id": 1, - "plan_type": "generator", - "plan_name": "test", - "subplan_name": CONST.PLAN.GRID_DETECT_AND_DO_GRIDSCAN, - "hyperion_parameters": dummy_params_2d().model_dump_json(), - } - test_rotation_start_main_document = { - "uid": "2093c941-ded1-42c4-ab74-ea99980fbbfd", - "subplan_name": CONST.PLAN.ROTATION_MAIN, - "zocalo_environment": EnvironmentConstants.ZOCALO_ENV, - } - test_gridscan_outer_start_document = { - "uid": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "time": 1666604299.6149616, - "versions": {"ophyd": "1.6.4.post76+g0895f9f", "bluesky": "1.8.3"}, - "scan_id": 1, - "plan_type": "generator", - "plan_name": CONST.PLAN.GRIDSCAN_OUTER, - "subplan_name": CONST.PLAN.GRIDSCAN_OUTER, - "zocalo_environment": EnvironmentConstants.ZOCALO_ENV, - CONST.TRIGGER.ZOCALO: PlanNameConstants.DO_FGS, - "hyperion_parameters": dummy_params().model_dump_json(), - } - test_rotation_event_document_during_data_collection: Event = { - "descriptor": "bd45c2e5-2b85-4280-95d7-a9a15800a78b", - "time": 2666604299.928203, - "data": { - "aperture_scatterguard-aperture-x": 15, - "aperture_scatterguard-aperture-y": 16, - "aperture_scatterguard-aperture-z": 2, - "aperture_scatterguard-scatterguard-x": 18, - "aperture_scatterguard-scatterguard-y": 19, - "aperture_scatterguard-selected_aperture": ApertureValue.MEDIUM, - "aperture_scatterguard-radius": 50, - "attenuator-actual_transmission": 0.98, - "flux_flux_reading": 9.81, - "dcm-energy_in_kev": 11.105, - }, - "timestamps": {"det1": 1666604299.8220396, "det2": 1666604299.8235943}, - "seq_num": 1, - "uid": "2093c941-ded1-42c4-ab74-ea99980fbbfd", - "filled": {}, - } - test_rotation_stop_main_document: RunStop = { - "run_start": "2093c941-ded1-42c4-ab74-ea99980fbbfd", - "time": 1666604300.0310638, - "uid": "65b2bde5-5740-42d7-9047-e860e06fbe15", - "exit_status": "success", - "reason": "Test succeeded", - "num_events": {"fake_ispyb_params": 1, "primary": 1}, - } - test_run_gridscan_start_document: RunStart = { # type: ignore - "uid": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "time": 1666604299.6149616, - "versions": {"ophyd": "1.6.4.post76+g0895f9f", "bluesky": "1.8.3"}, - "scan_id": 1, - "plan_type": "generator", - "plan_name": CONST.PLAN.GRIDSCAN_AND_MOVE, - "subplan_name": CONST.PLAN.GRIDSCAN_MAIN, - } - test_do_fgs_start_document: RunStart = { # type: ignore - "uid": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "time": 1666604299.6149616, - "versions": {"ophyd": "1.6.4.post76+g0895f9f", "bluesky": "1.8.3"}, - "scan_id": 1, - "plan_type": "generator", - "plan_name": CONST.PLAN.GRIDSCAN_AND_MOVE, - "subplan_name": PlanNameConstants.DO_FGS, - "scan_points": create_dummy_scan_spec(10, 20, 30), - } - test_descriptor_document_oav_rotation_snapshot: EventDescriptor = { - "uid": "c7d698ce-6d49-4c56-967e-7d081f964573", - "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "name": CONST.DESCRIPTORS.OAV_ROTATION_SNAPSHOT_TRIGGERED, - } # type: ignore - test_descriptor_document_pre_data_collection: EventDescriptor = { - "uid": "bd45c2e5-2b85-4280-95d7-a9a15800a78b", - "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "name": CONST.DESCRIPTORS.HARDWARE_READ_PRE, - } # type: ignore - test_descriptor_document_during_data_collection: EventDescriptor = { - "uid": "bd45c2e5-2b85-4280-95d7-a9a15800a78b", - "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "name": CONST.DESCRIPTORS.HARDWARE_READ_DURING, - } # type: ignore - test_descriptor_document_zocalo_hardware: EventDescriptor = { - "uid": "f082901b-7453-4150-8ae5-c5f98bb34406", - "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "name": CONST.DESCRIPTORS.ZOCALO_HW_READ, - } # type: ignore - test_event_document_oav_rotation_snapshot: Event = { - "descriptor": "c7d698ce-6d49-4c56-967e-7d081f964573", - "time": 1666604299.828203, - "timestamps": {}, - "seq_num": 1, - "uid": "32d7c25c-c310-4292-ac78-36ce6509be3d", - "data": {"oav-snapshot-last_saved_path": "snapshot_0"}, - } - test_event_document_pre_data_collection: Event = { - "descriptor": "bd45c2e5-2b85-4280-95d7-a9a15800a78b", - "time": 1666604299.828203, - "data": { - "s4_slit_gaps_xgap": 0.1234, - "s4_slit_gaps_ygap": 0.2345, - "synchrotron-synchrotron_mode": SynchrotronMode.USER, - "undulator-current_gap": 1.234, - "smargon-x": 0.158435435, - "smargon-y": 0.023547354, - "smargon-z": 0.00345684712, - "dcm-energy_in_kev": 11.105, - }, - "timestamps": {"det1": 1666604299.8220396, "det2": 1666604299.8235943}, - "seq_num": 1, - "uid": "29033ecf-e052-43dd-98af-c7cdd62e8173", - "filled": {}, - } - test_event_document_during_data_collection: Event = { - "descriptor": "bd45c2e5-2b85-4280-95d7-a9a15800a78b", - "time": 2666604299.928203, - "data": { - "aperture_scatterguard-aperture-x": 15, - "aperture_scatterguard-aperture-y": 16, - "aperture_scatterguard-aperture-z": 2, - "aperture_scatterguard-scatterguard-x": 18, - "aperture_scatterguard-scatterguard-y": 19, - "aperture_scatterguard-selected_aperture": ApertureValue.MEDIUM, - "aperture_scatterguard-radius": 50, - "attenuator-actual_transmission": 1, - "flux_flux_reading": 10, - "dcm-energy_in_kev": 11.105, - "eiger_bit_depth": "16", - }, - "timestamps": { - "det1": 1666604299.8220396, - "det2": 1666604299.8235943, - "eiger_bit_depth": 1666604299.8220396, - }, - "seq_num": 1, - "uid": "29033ecf-e052-43dd-98af-c7cdd62e8174", - "filled": {}, - } - test_event_document_zocalo_hardware: Event = { - "uid": "29033ecf-e052-43dd-98af-c7cdd62e8175", - "time": 1709654583.9770422, - "data": {"eiger_odin_file_writer_id": "test_path"}, - "timestamps": {"eiger_odin_file_writer_id": 1666604299.8220396}, - "seq_num": 1, - "filled": {}, - "descriptor": "f082901b-7453-4150-8ae5-c5f98bb34406", - } - test_stop_document: RunStop = { - "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "time": 1666604300.0310638, - "uid": "65b2bde5-5740-42d7-9047-e860e06fbe15", - "exit_status": "success", - "reason": "", - "num_events": {"fake_ispyb_params": 1, "primary": 1}, - } - test_run_gridscan_stop_document: RunStop = { - "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "time": 1666604300.0310638, - "uid": "65b2bde5-5740-42d7-9047-e860e06fbe15", - "exit_status": "success", - "reason": "", - "num_events": {"fake_ispyb_params": 1, "primary": 1}, - } - test_do_fgs_gridscan_stop_document: RunStop = { - "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "time": 1666604300.0310638, - "uid": "65b2bde5-5740-42d7-9047-e860e06fbe15", - "exit_status": "success", - "reason": "", - "num_events": {"fake_ispyb_params": 1, "primary": 1}, - } - test_failed_stop_document: RunStop = { - "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "time": 1666604300.0310638, - "uid": "65b2bde5-5740-42d7-9047-e860e06fbe15", - "exit_status": "fail", - "reason": "could not connect to devices", - "num_events": {"fake_ispyb_params": 1, "primary": 1}, - } - test_run_gridscan_failed_stop_document: RunStop = { - "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "time": 1666604300.0310638, - "uid": "65b2bde5-5740-42d7-9047-e860e06fbe15", - "exit_status": "fail", - "reason": "could not connect to devices", - "num_events": {"fake_ispyb_params": 1, "primary": 1}, + "mx_bluesky_parameters": dummy_rotation_params.model_dump_json(), } - test_descriptor_document_zocalo_reading: EventDescriptor = { - "uid": "unique_id_zocalo_reading", - "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", - "name": ZOCALO_READING_PLAN_NAME, - } # type:ignore - test_zocalo_reading_event: Event = { - "descriptor": "unique_id_zocalo_reading", - "data": generate_xrc_result_event("zocalo", []), - } # type:ignore diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/robot_load/test_robot_load_ispyb_callback.py b/tests/unit_tests/hyperion/external_interaction/callbacks/robot_load/test_robot_load_ispyb_callback.py index 687f91224..4b67d9b69 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/robot_load/test_robot_load_ispyb_callback.py +++ b/tests/unit_tests/hyperion/external_interaction/callbacks/robot_load/test_robot_load_ispyb_callback.py @@ -9,10 +9,10 @@ from dodal.devices.webcam import Webcam from ophyd_async.testing import set_mock_value +from mx_bluesky.common.external_interaction.ispyb.exp_eye_store import BLSampleStatus from mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback import ( RobotLoadISPyBCallback, ) -from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import BLSampleStatus from mx_bluesky.hyperion.parameters.constants import CONST VISIT = "cm31105-4" diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/rotation/test_ispyb_callback.py b/tests/unit_tests/hyperion/external_interaction/callbacks/rotation/test_ispyb_callback.py index 4becd6809..b7ff00c02 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/rotation/test_ispyb_callback.py +++ b/tests/unit_tests/hyperion/external_interaction/callbacks/rotation/test_ispyb_callback.py @@ -6,17 +6,17 @@ RotationISPyBCallback, ) -from ...conftest import ( +from ......conftest import ( EXPECTED_END_TIME, EXPECTED_START_TIME, TEST_DATA_COLLECTION_GROUP_ID, TEST_DATA_COLLECTION_IDS, TEST_SAMPLE_ID, TEST_SESSION_ID, + TestData, assert_upsert_call_with, mx_acquisition_from_conn, ) -from ..conftest import TestData EXPECTED_DATA_COLLECTION = { "visitid": TEST_SESSION_ID, @@ -55,7 +55,7 @@ def rotation_start_outer_doc_without_snapshots( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def test_activity_gated_start( @@ -82,7 +82,7 @@ def test_activity_gated_start( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def test_activity_gated_start_with_snapshot_parameters( @@ -109,7 +109,7 @@ def test_activity_gated_start_with_snapshot_parameters( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def test_hardware_read_events( @@ -141,8 +141,8 @@ def test_hardware_read_events( "synchrotronmode": "User", "undulatorgap1": 1.234, "comments": "Sample position (µm): (158, 24, 3) test ", - "resolution": 1.1830593328548429, - "wavelength": 1.1164718451643736, + "resolution": 1.1830593331191241, + "wavelength": 1.11647184541378, }, ) expected_data = TestData.test_event_document_pre_data_collection["data"] @@ -159,7 +159,7 @@ def test_hardware_read_events( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def test_flux_read_events( @@ -195,16 +195,16 @@ def test_flux_read_events( "focal_spot_size_at_sampley": 0.02, "beamsize_at_samplex": 0.05, "beamsize_at_sampley": 0.02, - "wavelength": 1.1164718451643736, + "wavelength": 1.11647184541378, "transmission": 98, "flux": 9.81, - "resolution": 1.1830593328548429, + "resolution": 1.1830593331191241, }, ) @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def test_oav_rotation_snapshot_triggered_event( @@ -247,7 +247,7 @@ def test_oav_rotation_snapshot_triggered_event( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", + "mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping.get_current_time_string", new=MagicMock(return_value=EXPECTED_START_TIME), ) def test_activity_gated_stop(mock_ispyb_conn, test_rotation_start_outer_document): @@ -262,7 +262,7 @@ def test_activity_gated_stop(mock_ispyb_conn, test_rotation_start_outer_document mx.upsert_data_collection.reset_mock() with patch( - "mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store.get_current_time_string", + "mx_bluesky.common.external_interaction.ispyb.ispyb_store.get_current_time_string", new=MagicMock(return_value=EXPECTED_END_TIME), ): callback.activity_gated_stop(TestData.test_rotation_stop_main_document) @@ -291,10 +291,10 @@ def test_comment_correct_after_hardware_read( mock_ispyb_conn, dummy_rotation_params, test_rotation_start_outer_document ): callback = RotationISPyBCallback() - test_rotation_start_outer_document["hyperion_parameters"] = ( - test_rotation_start_outer_document[ - "hyperion_parameters" - ].replace('"comment":"test"', '"comment":"a lovely unit test"') + test_rotation_start_outer_document["mx_bluesky_parameters"] = ( + test_rotation_start_outer_document["mx_bluesky_parameters"].replace( + '"comment":"test"', '"comment":"a lovely unit test"' + ) ) callback.activity_gated_start(test_rotation_start_outer_document) # pyright: ignore callback.activity_gated_start( @@ -320,7 +320,7 @@ def test_comment_correct_after_hardware_read( "synchrotronmode": "User", "undulatorgap1": 1.234, "comments": "Sample position (µm): (158, 24, 3) a lovely unit test ", - "resolution": 1.1830593328548429, - "wavelength": 1.1164718451643736, + "resolution": 1.1830593331191241, + "wavelength": 1.11647184541378, }, ) diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/sample_handling/test_sample_handling_callback.py b/tests/unit_tests/hyperion/external_interaction/callbacks/sample_handling/test_sample_handling_callback.py index f5143e8f3..fc1769839 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/sample_handling/test_sample_handling_callback.py +++ b/tests/unit_tests/hyperion/external_interaction/callbacks/sample_handling/test_sample_handling_callback.py @@ -4,15 +4,14 @@ from bluesky.preprocessors import run_decorator from bluesky.run_engine import RunEngine -from mx_bluesky.hyperion.exceptions import SampleException +from mx_bluesky.common.external_interaction.ispyb.exp_eye_store import BLSampleStatus +from mx_bluesky.common.utils.exceptions import SampleException from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import ( CrystalNotFoundException, ) from mx_bluesky.hyperion.external_interaction.callbacks.sample_handling.sample_handling_callback import ( SampleHandlingCallback, - sample_handling_callback_decorator, ) -from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import BLSampleStatus TEST_SAMPLE_ID = 123456 @@ -23,10 +22,9 @@ "activate_callbacks": ["SampleHandlingCallback"], } ) -@sample_handling_callback_decorator() -def plan_with_general_exception(exception_type: type): +def plan_with_general_exception(exception_type: type, msg: str): yield from [] - raise exception_type("Test failure") + raise exception_type(msg) @run_decorator( @@ -35,21 +33,24 @@ def plan_with_general_exception(exception_type: type): "activate_callbacks": ["SampleHandlingCallback"], } ) -@sample_handling_callback_decorator() def plan_with_normal_completion(): yield from [] @pytest.mark.parametrize( - "exception_type, expected_sample_status", + "exception_type, expected_sample_status, message", [ - [AssertionError, BLSampleStatus.ERROR_BEAMLINE], - [SampleException, BLSampleStatus.ERROR_SAMPLE], - [CrystalNotFoundException, BLSampleStatus.ERROR_SAMPLE], + [AssertionError, BLSampleStatus.ERROR_BEAMLINE, "Test failure"], + [SampleException, BLSampleStatus.ERROR_SAMPLE, "Test failure"], + [CrystalNotFoundException, BLSampleStatus.ERROR_SAMPLE, "Test failure"], + [AssertionError, BLSampleStatus.ERROR_BEAMLINE, None], ], ) def test_sample_handling_callback_intercepts_general_exception( - RE: RunEngine, exception_type: type, expected_sample_status: BLSampleStatus + RE: RunEngine, + exception_type: type, + expected_sample_status: BLSampleStatus, + message: str, ): callback = SampleHandlingCallback() RE.subscribe(callback) @@ -63,7 +64,7 @@ def test_sample_handling_callback_intercepts_general_exception( ), pytest.raises(exception_type), ): - RE(plan_with_general_exception(exception_type)) + RE(plan_with_general_exception(exception_type, message)) mock_expeye.update_sample_status.assert_called_once_with( TEST_SAMPLE_ID, expected_sample_status ) diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/test_external_callbacks.py b/tests/unit_tests/hyperion/external_interaction/callbacks/test_external_callbacks.py index 30b3f9403..99d6cd253 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/test_external_callbacks.py +++ b/tests/unit_tests/hyperion/external_interaction/callbacks/test_external_callbacks.py @@ -7,13 +7,13 @@ from bluesky.callbacks.zmq import Proxy, RemoteDispatcher from dodal.log import LOGGER as DODAL_LOGGER +from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, NEXUS_LOGGER from mx_bluesky.hyperion.external_interaction.callbacks.__main__ import ( main, setup_callbacks, setup_logging, setup_threads, ) -from mx_bluesky.hyperion.log import ISPYB_LOGGER, NEXUS_LOGGER @patch( @@ -50,15 +50,15 @@ def test_setup_callbacks(): return_value=True, ) def test_setup_logging(parse_callback_cli_args): - assert DODAL_LOGGER.parent != ISPYB_LOGGER - assert len(ISPYB_LOGGER.handlers) == 0 + assert DODAL_LOGGER.parent != ISPYB_ZOCALO_CALLBACK_LOGGER + assert len(ISPYB_ZOCALO_CALLBACK_LOGGER.handlers) == 0 assert len(NEXUS_LOGGER.handlers) == 0 setup_logging(parse_callback_cli_args()) - assert len(ISPYB_LOGGER.handlers) == 4 + assert len(ISPYB_ZOCALO_CALLBACK_LOGGER.handlers) == 4 assert len(NEXUS_LOGGER.handlers) == 4 - assert DODAL_LOGGER.parent == ISPYB_LOGGER + assert DODAL_LOGGER.parent == ISPYB_ZOCALO_CALLBACK_LOGGER setup_logging(parse_callback_cli_args()) - assert len(ISPYB_LOGGER.handlers) == 4 + assert len(ISPYB_ZOCALO_CALLBACK_LOGGER.handlers) == 4 assert len(NEXUS_LOGGER.handlers) == 4 diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/test_rotation_callbacks.py b/tests/unit_tests/hyperion/external_interaction/callbacks/test_rotation_callbacks.py index 8fc77460c..2738226fd 100644 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/test_rotation_callbacks.py +++ b/tests/unit_tests/hyperion/external_interaction/callbacks/test_rotation_callbacks.py @@ -4,7 +4,15 @@ from bluesky.run_engine import RunEngine from event_model import RunStart +from mx_bluesky.common.external_interaction.ispyb.data_model import ( + ScanDataInfo, +) +from mx_bluesky.common.external_interaction.ispyb.ispyb_store import ( + IspybIds, + StoreInIspyb, +) from mx_bluesky.common.parameters.components import IspybExperimentType +from mx_bluesky.common.utils.exceptions import ISPyBDepositionNotMade from mx_bluesky.hyperion.experiment_plans.rotation_scan_plan import rotation_scan from mx_bluesky.hyperion.external_interaction.callbacks.common.callback_util import ( create_rotation_callbacks, @@ -15,14 +23,6 @@ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.nexus_callback import ( RotationNexusFileCallback, ) -from mx_bluesky.hyperion.external_interaction.exceptions import ISPyBDepositionNotMade -from mx_bluesky.hyperion.external_interaction.ispyb.data_model import ( - ScanDataInfo, -) -from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import ( - IspybIds, - StoreInIspyb, -) from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.rotation import RotationScan @@ -99,7 +99,7 @@ def test_nexus_handler_only_writes_once( autospec=True, ) @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback.ZocaloTrigger", + "mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback.ZocaloTrigger", autospec=True, ) @patch( @@ -133,7 +133,7 @@ def test_zocalo_start_and_end_not_triggered_if_ispyb_ids_not_present( autospec=True, ) @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback.ZocaloTrigger", + "mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback.ZocaloTrigger", autospec=True, ) @patch( @@ -172,7 +172,7 @@ def test_ispyb_triggered_before_zocalo( @patch( - "mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback.ZocaloTrigger", + "mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback.ZocaloTrigger", autospec=True, ) @patch( @@ -293,7 +293,7 @@ def test_ispyb_handler_stores_sampleid_for_full_collection_not_screening( params.ispyb_experiment_type = IspybExperimentType.CHARACTERIZATION assert params.num_images == n_images doc["subplan_name"] = CONST.PLAN.ROTATION_OUTER # type: ignore - doc["hyperion_parameters"] = params.model_dump_json() # type: ignore + doc["mx_bluesky_parameters"] = params.model_dump_json() # type: ignore cb.start(doc) assert (cb.last_sample_id == 987678) is store_id diff --git a/tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/conftest.py b/tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/conftest.py deleted file mode 100644 index 7605c7395..000000000 --- a/tests/unit_tests/hyperion/external_interaction/callbacks/xray_centre/conftest.py +++ /dev/null @@ -1,60 +0,0 @@ -from unittest.mock import patch - -import pytest - -from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( - GridscanISPyBCallback, -) - - -@pytest.fixture -def nexus_writer(): - with patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.nexus_callback.NexusWriter" - ) as nw: - yield nw - - -@pytest.fixture -def mock_ispyb_get_time(): - with patch( - "mx_bluesky.hyperion.external_interaction.ispyb.ispyb_utils.get_current_time_string" - ) as p: - yield p - - -@pytest.fixture -def mock_ispyb_store_grid_scan(): - with patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb" - ) as p: - yield p - - -@pytest.fixture -def mock_ispyb_update_time_and_status(): - with patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb._update_scan_with_end_time_and_status" - ) as p: - yield p - - -@pytest.fixture -def mock_ispyb_begin_deposition(): - with patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb.begin_deposition" - ) as p: - yield p - - -@pytest.fixture -def mock_ispyb_end_deposition(): - with patch( - "mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb.end_deposition" - ) as p: - yield p - - -@pytest.fixture -def ispyb_handler(): - return GridscanISPyBCallback() diff --git a/tests/unit_tests/hyperion/external_interaction/conftest.py b/tests/unit_tests/hyperion/external_interaction/conftest.py index d7c15ff80..71d7c0110 100644 --- a/tests/unit_tests/hyperion/external_interaction/conftest.py +++ b/tests/unit_tests/hyperion/external_interaction/conftest.py @@ -1,63 +1,15 @@ import os -from collections.abc import Callable, Sequence -from copy import deepcopy -from typing import Any -from unittest.mock import MagicMock, mock_open, patch -import bluesky.plan_stubs as bps -import bluesky.preprocessors as bpp import pytest -from bluesky.run_engine import RunEngine -from ispyb.sp.mxacquisition import MXAcquisition -from ophyd.sim import SynAxis -from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import ( - PlanReactiveCallback, -) +from mx_bluesky.common.utils.utils import convert_angstrom_to_eV from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan from mx_bluesky.hyperion.parameters.rotation import RotationScan -from mx_bluesky.hyperion.utils.utils import convert_angstrom_to_eV - -from ....conftest import raw_params_from_file - - -class MockReactiveCallback(PlanReactiveCallback): - activity_gated_start: MagicMock - activity_gated_descriptor: MagicMock - activity_gated_event: MagicMock - activity_gated_stop: MagicMock - - def __init__(self, *, emit: Callable[..., Any] | None = None) -> None: - super().__init__(MagicMock(), emit=emit) - self.activity_gated_start = MagicMock(name="activity_gated_start") # type: ignore - self.activity_gated_descriptor = MagicMock(name="activity_gated_descriptor") # type: ignore - self.activity_gated_event = MagicMock(name="activity_gated_event") # type: ignore - self.activity_gated_stop = MagicMock(name="activity_gated_stop") # type: ignore - - -@pytest.fixture -def mocked_test_callback(): - t = MockReactiveCallback() - return t - - -@pytest.fixture -def RE_with_mock_callback(mocked_test_callback): - RE = RunEngine() - RE.subscribe(mocked_test_callback) - yield RE, mocked_test_callback - - -def get_test_plan(callback_name): - s = SynAxis(name="fake_signal") - @bpp.run_decorator(md={"activate_callbacks": [callback_name]}) - def test_plan(): - yield from bps.create() - yield from bps.read(s) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809 - yield from bps.save() - - return test_plan, s +from ....conftest import ( + default_raw_gridscan_params, + raw_params_from_file, +) @pytest.fixture @@ -80,7 +32,7 @@ def test_rotation_params(): @pytest.fixture(params=[1050]) def test_fgs_params(request): assert request.param % 25 == 0, "Please use a multiple of 25 images" - params = HyperionThreeDGridScan(**default_raw_params()) + params = HyperionThreeDGridScan(**default_raw_gridscan_params()) params.demand_energy_ev = convert_angstrom_to_eV(1.0) params.use_roi_mode = True first_scan_img = (request.param // 10) * 6 @@ -95,123 +47,6 @@ def test_fgs_params(request): yield params -def _mock_ispyb_conn(base_ispyb_conn, position_id, dcgid, dcids, giids): - def upsert_data_collection(values): - kvpairs = remap_upsert_columns( - list(MXAcquisition.get_data_collection_params()), values - ) - if kvpairs["id"]: - return kvpairs["id"] - else: - return next(upsert_data_collection.i) # pyright: ignore - - mx_acq = base_ispyb_conn.return_value.mx_acquisition - mx_acq.upsert_data_collection.side_effect = upsert_data_collection - mx_acq.update_dc_position.return_value = position_id - mx_acq.upsert_data_collection_group.return_value = dcgid - - def upsert_dc_grid(values): - kvpairs = remap_upsert_columns(list(MXAcquisition.get_dc_grid_params()), values) - if kvpairs["id"]: - return kvpairs["id"] - else: - return next(upsert_dc_grid.i) # pyright: ignore - - upsert_data_collection.i = iter(dcids) # pyright: ignore - upsert_dc_grid.i = iter(giids) # pyright: ignore - - mx_acq.upsert_dc_grid.side_effect = upsert_dc_grid - return base_ispyb_conn - - -@pytest.fixture -def mock_ispyb_conn(base_ispyb_conn): - return _mock_ispyb_conn( - base_ispyb_conn, - TEST_POSITION_ID, - TEST_DATA_COLLECTION_GROUP_ID, - TEST_DATA_COLLECTION_IDS, - TEST_GRID_INFO_IDS, - ) - - -@pytest.fixture -def mock_ispyb_conn_multiscan(base_ispyb_conn): - return _mock_ispyb_conn( - base_ispyb_conn, - TEST_POSITION_ID, - TEST_DATA_COLLECTION_GROUP_ID, - list(range(12, 24)), - list(range(56, 68)), - ) - - -def mx_acquisition_from_conn(mock_ispyb_conn) -> MagicMock: - return mock_ispyb_conn.return_value.__enter__.return_value.mx_acquisition - - -def assert_upsert_call_with(call, param_template, expected: dict): - actual = remap_upsert_columns(list(param_template), call.args[0]) - assert actual == dict(param_template | expected) - - -TEST_DATA_COLLECTION_IDS = (12, 13) -TEST_DATA_COLLECTION_GROUP_ID = 34 -TEST_GRID_INFO_IDS = (56, 57) -TEST_POSITION_ID = 78 -TEST_SESSION_ID = 90 -EXPECTED_START_TIME = "2024-02-08 14:03:59" -EXPECTED_END_TIME = "2024-02-08 14:04:01" - - -def remap_upsert_columns(keys: Sequence[str], values: list): - return dict(zip(keys, values, strict=False)) - - -@pytest.fixture -def base_ispyb_conn(): - with patch("ispyb.open", mock_open()) as ispyb_connection: - mock_mx_acquisition = MagicMock() - mock_mx_acquisition.get_data_collection_group_params.side_effect = ( - lambda: deepcopy(MXAcquisition.get_data_collection_group_params()) - ) - - mock_mx_acquisition.get_data_collection_params.side_effect = lambda: deepcopy( - MXAcquisition.get_data_collection_params() - ) - mock_mx_acquisition.get_dc_position_params.side_effect = lambda: deepcopy( - MXAcquisition.get_dc_position_params() - ) - mock_mx_acquisition.get_dc_grid_params.side_effect = lambda: deepcopy( - MXAcquisition.get_dc_grid_params() - ) - ispyb_connection.return_value.mx_acquisition = mock_mx_acquisition - mock_core = MagicMock() - - def mock_retrieve_visit(visit_str): - assert visit_str, "No visit id supplied" - return TEST_SESSION_ID - - mock_core.retrieve_visit_id.side_effect = mock_retrieve_visit - ispyb_connection.return_value.core = mock_core - yield ispyb_connection - - -@pytest.fixture -def dummy_rotation_params(): - dummy_params = RotationScan( - **default_raw_params( - "tests/test_data/parameter_json_files/good_test_rotation_scan_parameters.json" - ) - ) - dummy_params.sample_id = TEST_SAMPLE_ID - return dummy_params - - -TEST_SAMPLE_ID = 364758 -TEST_BARCODE = "12345A" - - def default_raw_params( json_file="tests/test_data/parameter_json_files/good_test_parameters.json", ): diff --git a/tests/unit_tests/hyperion/external_interaction/nexus/test_nexus_utils.py b/tests/unit_tests/hyperion/external_interaction/nexus/test_nexus_utils.py deleted file mode 100644 index ed140a1fe..000000000 --- a/tests/unit_tests/hyperion/external_interaction/nexus/test_nexus_utils.py +++ /dev/null @@ -1,17 +0,0 @@ -import numpy as np -import pytest -from numpy.typing import DTypeLike - -from mx_bluesky.hyperion.external_interaction.nexus.nexus_utils import ( - vds_type_based_on_bit_depth, -) - - -@pytest.mark.parametrize( - "bit_depth,expected_type", - [(8, np.uint8), (16, np.uint16), (32, np.uint32), (100, np.uint16)], -) -def test_vds_type_is_expected_based_on_bit_depth( - bit_depth: int, expected_type: DTypeLike -): - assert vds_type_based_on_bit_depth(bit_depth) == expected_type 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 d5a40acbe..02ba42d6f 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 @@ -1,5 +1,6 @@ import os from contextlib import contextmanager +from pathlib import Path from typing import Literal from unittest.mock import patch @@ -15,10 +16,11 @@ ZebraGridScanParams, ) -from mx_bluesky.hyperion.external_interaction.nexus.nexus_utils import ( +from mx_bluesky.common.external_interaction.nexus.nexus_utils import ( + AxisDirection, create_beam_and_attenuator_parameters, ) -from mx_bluesky.hyperion.external_interaction.nexus.write_nexus import NexusWriter +from mx_bluesky.common.external_interaction.nexus.write_nexus import NexusWriter from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan """It's hard to effectively unit test the nexus writing so these are really system tests @@ -241,6 +243,45 @@ def test_given_dummy_data_then_datafile_written_correctly( assert_data_edge_at(nexus_writer_2.nexus_file, 419) +@pytest.mark.parametrize( + "axis_direction, expected_vector", + [[AxisDirection.NEGATIVE, [-1, 0, 0]], [AxisDirection.POSITIVE, [1, 0, 0]]], +) +def test_nexus_file_entry_data_omega_written_correctly_independent_of_omega_direction( + test_rotation_params, + axis_direction: AxisDirection, + expected_vector: list[float], + tmp_path: Path, +): + test_rotation_params.storage_directory = str(tmp_path) + det_size = ( + test_rotation_params.detector_params.detector_size_constants.det_size_pixels + ) + shape = (test_rotation_params.num_images, det_size.width, det_size.height) + nexus_writer = NexusWriter( + test_rotation_params, + shape, + test_rotation_params.scan_points, + omega_start_deg=test_rotation_params.omega_start_deg, + chi_start_deg=test_rotation_params.chi_start_deg or 0, + phi_start_deg=test_rotation_params.phi_start_deg or 0, + vds_start_index=0, + meta_data_run_number=1, + axis_direction=axis_direction, + ) + nexus_writer.beam, nexus_writer.attenuator = create_beam_and_attenuator_parameters( + 20, TEST_FLUX, 0.5 + ) + + nexus_writer.create_nexus_file(np.uint16) + with h5py.File(nexus_writer.nexus_file, "r") as nexus_file: + assert all( + nexus_file["/entry/data/omega"].attrs.get("vector") == expected_vector + ) + data = nexus_file["/entry/data/omega"][:] # type: ignore + assert all(data == test_rotation_params.scan_points["omega"]) + + def assert_x_data_stride_correct( data_path, grid_scan_params: ZebraGridScanParams, varying_axis_steps ): diff --git a/tests/unit_tests/hyperion/external_interaction/test_write_rotation_nexus.py b/tests/unit_tests/hyperion/external_interaction/test_write_rotation_nexus.py index 984cd98da..1a37e830e 100644 --- a/tests/unit_tests/hyperion/external_interaction/test_write_rotation_nexus.py +++ b/tests/unit_tests/hyperion/external_interaction/test_write_rotation_nexus.py @@ -10,6 +10,8 @@ from bluesky.run_engine import RunEngine from h5py import Dataset, ExternalLink, Group +from mx_bluesky.common.external_interaction.nexus.write_nexus import NexusWriter +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.device_setup_plans.read_hardware_for_setup import ( read_hardware_during_collection, ) @@ -19,8 +21,6 @@ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.nexus_callback import ( RotationNexusFileCallback, ) -from mx_bluesky.hyperion.external_interaction.nexus.write_nexus import NexusWriter -from mx_bluesky.hyperion.log import LOGGER from mx_bluesky.hyperion.parameters.constants import CONST from mx_bluesky.hyperion.parameters.rotation import RotationScan @@ -59,7 +59,7 @@ def fake_rotation_scan( @bpp.run_decorator( # attach experiment metadata to the start document md={ "subplan_name": CONST.PLAN.ROTATION_OUTER, - "hyperion_parameters": parameters.model_dump_json(), + "mx_bluesky_parameters": parameters.model_dump_json(), "activate_callbacks": "RotationNexusFileCallback", } ) @@ -126,7 +126,7 @@ def test_rotation_scan_nexus_output_compared_to_existing_full_compare( RE = RunEngine({}) with patch( - "mx_bluesky.hyperion.external_interaction.nexus.write_nexus.get_start_and_predicted_end_time", + "mx_bluesky.common.external_interaction.nexus.write_nexus.get_start_and_predicted_end_time", return_value=("test_time", "test_time"), ): RE( @@ -242,7 +242,7 @@ def test_rotation_scan_nexus_output_compared_to_existing_file( RE = RunEngine({}) with patch( - "mx_bluesky.hyperion.external_interaction.nexus.write_nexus.get_start_and_predicted_end_time", + "mx_bluesky.common.external_interaction.nexus.write_nexus.get_start_and_predicted_end_time", return_value=("test_time", "test_time"), ): RE( @@ -343,7 +343,7 @@ def test_rotation_scan_nexus_output_compared_to_existing_file( "bit_depth,expected_type", [(8, np.uint8), (16, np.uint16), (32, np.uint32), (100, np.uint16)], ) -@patch("mx_bluesky.hyperion.external_interaction.nexus.write_nexus.NXmxFileWriter") +@patch("mx_bluesky.common.external_interaction.nexus.write_nexus.NXmxFileWriter") def test_given_detector_bit_depth_changes_then_vds_datatype_as_expected( mock_nexus_writer, test_params: RotationScan, @@ -358,7 +358,7 @@ def test_given_detector_bit_depth_changes_then_vds_datatype_as_expected( RE = RunEngine({}) with patch( - "mx_bluesky.hyperion.external_interaction.nexus.write_nexus.get_start_and_predicted_end_time", + "mx_bluesky.common.external_interaction.nexus.write_nexus.get_start_and_predicted_end_time", return_value=("test_time", "test_time"), ): RE( @@ -386,9 +386,9 @@ def _compare_actual_and_expected(path: list[str], actual, expected, exceptions: keys_not_in_actual = ( expected.keys() - actual.keys() - exceptions.get("_missing", set()) ) - assert ( - len(keys_not_in_actual) == 0 - ), f"Missing entries in group {path_str}, {keys_not_in_actual}" + assert len(keys_not_in_actual) == 0, ( + f"Missing entries in group {path_str}, {keys_not_in_actual}" + ) keys_to_compare = actual.keys() keys_to_ignore = exceptions.get("_ignore", set()) @@ -409,9 +409,9 @@ def _compare_actual_and_expected(path: list[str], actual, expected, exceptions: if isinstance(actual_link, ExternalLink): if exception: actual_link_path = f"{actual_link.filename}//{actual_link.path}" - assert ( - actual_link_path == exception - ), f"Actual and expected external links differ {actual_link_path}, {exception}" + assert actual_link_path == exception, ( + f"Actual and expected external links differ {actual_link_path}, {exception}" + ) else: LOGGER.debug( f"Skipping external link {item_path_str} -> {actual_link.path}" @@ -438,41 +438,48 @@ def _compare_actual_and_expected(path: list[str], actual, expected, exceptions: elif (expected_class == Dataset) and key not in keys_to_ignore: if isinstance(expected_value, Dataset): # Only check shape if we didn't override the expected value - assert ( - actual_value.shape == expected_value.shape - ), f"Actual and expected shapes differ for {item_path_str}: {actual_value.shape}, {expected_value.shape}" + assert actual_value.shape == expected_value.shape, ( + f"Actual and expected shapes differ for {item_path_str}: {actual_value.shape}, {expected_value.shape}" + ) else: assert hasattr(actual_value, "shape"), f"No shape for {item_path_str}" expected_shape = np.shape(expected_value) # type: ignore - assert ( - actual_value.shape == expected_shape - ), f"{item_path_str} data shape not expected shape{actual_value.shape}, {expected_shape}" + assert actual_value.shape == expected_shape, ( + f"{item_path_str} data shape not expected shape{actual_value.shape}, {expected_shape}" + ) if actual_value.shape == (): if callable(exception): assert exceptions.get(key)(actual_value, expected_value) # type: ignore elif np.isscalar(exception): - assert ( - actual_value[()] == exception - ), f"{item_path_str} actual and expected did not match {actual_value[()]}, {exception}." + assert actual_value[()] == exception, ( + f"{item_path_str} actual and expected did not match {actual_value[()]}, {exception}." + ) else: - assert ( - actual_class == expected_class - ), f"{item_path_str} Actual and expected class don't match {actual_class}, {expected_class}" + assert actual_class == expected_class, ( + f"{item_path_str} Actual and expected class don't match {actual_class}, {expected_class}" + ) + # fmt: off assert ( actual_value[()] == expected_value[()] # type: ignore - ), f"Actual and expected values differ for {item_path_str}: {actual_value[()]} != {expected_value[()]}" # type: ignore + ), ( + f"Actual and expected values differ for {item_path_str}: " + f"{actual_value[()]} != {expected_value[()]}" # type: ignore + ) + # fmt: on else: actual_value_str = np.array2string(actual_value, threshold=10) expected_value_str = np.array2string(expected_value, threshold=10) # type: ignore if callable(exception): - assert exception( - actual_value, expected_value - ), f"Actual and expected values differ for {item_path_str}: {actual_value_str} != {expected_value_str}, according to {exception}" + assert exception(actual_value, expected_value), ( + f"Actual and expected values differ for {item_path_str}: {actual_value_str} != {expected_value_str}, according to {exception}" + ) else: assert np.array_equal( actual_value, expected_value, # type: ignore - ), f"Actual and expected values differ for {item_path_str}: {actual_value_str} != {expected_value_str}" + ), ( + f"Actual and expected values differ for {item_path_str}: {actual_value_str} != {expected_value_str}" + ) def test_override_parameters_override(test_params: RotationScan): diff --git a/tests/unit_tests/hyperion/test_exceptions.py b/tests/unit_tests/hyperion/test_exceptions.py index 56d513c14..291995557 100644 --- a/tests/unit_tests/hyperion/test_exceptions.py +++ b/tests/unit_tests/hyperion/test_exceptions.py @@ -1,7 +1,10 @@ import pytest from bluesky.plan_stubs import null -from mx_bluesky.hyperion.exceptions import WarningException, catch_exception_and_warn +from mx_bluesky.common.utils.exceptions import ( + WarningException, + catch_exception_and_warn, +) class _TestException(Exception): diff --git a/tests/unit_tests/hyperion/test_main_system.py b/tests/unit_tests/hyperion/test_main_system.py index f32123d7a..a2c6e3d14 100644 --- a/tests/unit_tests/hyperion/test_main_system.py +++ b/tests/unit_tests/hyperion/test_main_system.py @@ -15,10 +15,12 @@ import flask import pytest from blueapi.core import BlueskyContext -from dodal.devices.attenuator import Attenuator +from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator from dodal.devices.zebra import Zebra from flask.testing import FlaskClient +from mx_bluesky.common.utils.exceptions import WarningException +from mx_bluesky.common.utils.log import LOGGER from mx_bluesky.hyperion.__main__ import ( Actions, BlueskyRunner, @@ -27,9 +29,7 @@ create_targets, setup_context, ) -from mx_bluesky.hyperion.exceptions import WarningException from mx_bluesky.hyperion.experiment_plans.experiment_registry import PLAN_REGISTRY -from mx_bluesky.hyperion.log import LOGGER from mx_bluesky.hyperion.parameters.cli import parse_cli_args from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan from mx_bluesky.hyperion.utils.context import device_composite_from_context @@ -429,7 +429,7 @@ class MockCommand: ) def test_when_blueskyrunner_initiated_then_plans_are_setup_and_devices_connected(): zebra = MagicMock(spec=Zebra) - attenuator = MagicMock(spec=Attenuator) + attenuator = MagicMock(spec=BinaryFilterAttenuator) context = BlueskyContext() context.register_device(zebra, "zebra") @@ -437,7 +437,7 @@ def test_when_blueskyrunner_initiated_then_plans_are_setup_and_devices_connected @dataclass class FakeComposite: - attenuator: Attenuator + attenuator: BinaryFilterAttenuator zebra: Zebra # A fake setup for a plan that uses two devices: attenuator and zebra. diff --git a/tests/unit_tests/hyperion/test_utils.py b/tests/unit_tests/hyperion/test_utils.py index 67c5458ef..dad3045ef 100644 --- a/tests/unit_tests/hyperion/test_utils.py +++ b/tests/unit_tests/hyperion/test_utils.py @@ -2,7 +2,7 @@ import pytest -from mx_bluesky.hyperion.utils.utils import ( +from mx_bluesky.common.utils.utils import ( convert_angstrom_to_eV, convert_eV_to_angstrom, energy_to_bragg_angle, diff --git a/utility_scripts/generate_plantuml.py b/utility_scripts/generate_plantuml.py new file mode 100755 index 000000000..38a49c2e7 --- /dev/null +++ b/utility_scripts/generate_plantuml.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +from inspect import get_annotations, getmodule, getmro, isclass +from typing import get_type_hints + +from blueapi.core.bluesky_types import is_bluesky_plan_generator +from blueapi.core.context import load_module_all +from pydantic import BaseModel + +from mx_bluesky.common.parameters.components import MxBlueskyParameters +from mx_bluesky.hyperion import experiment_plans + + +def main(): + """ + Generate the PlantUML source for a diagram of the parameter models on the standard output. + """ + print(""" +'This file is auto-generated by generate_plantuml.py +@startuml hyperion_parameter_model +title Hyperion Parameter Model +set namespaceSeparator none +""") + + parameter_types = [ + get_type_hints(obj)["parameters"] + for obj in load_module_all(experiment_plans) + if is_bluesky_plan_generator(obj) + ] + experiment_types = set() + all_types = set() + for parameter_type in parameter_types: + experiment_types.add(parameter_type) + all_types.add(parameter_type) + + for experiment_type in experiment_types: + for base in getmro(experiment_type): + if issubclass(base, BaseModel) and base is not BaseModel: + all_types.add(base) + + types_by_package = {} + for t in all_types: + mod = getmodule(t) + assert mod + types_by_package.setdefault(mod.__package__, []).append(t) + + mx_bluesky_param_types = set() + mixin_types = set() + + for t in types_by_package["mx_bluesky.common.parameters"]: + if issubclass(t, MxBlueskyParameters): + mx_bluesky_param_types.add(t) + else: + mixin_types.add(t) + + print("package mx_bluesky.common.parameters {") + print("together {") + for t in mixin_types: + generate_class(t) + print("}") + print("together {") + for t in mx_bluesky_param_types: + generate_class(t) + print("}") + print("}") + + print("package mx_bluesky.hyperion.parameters {") + for t in types_by_package["mx_bluesky.hyperion.parameters"]: + generate_class(t) + print("}") + + for t in all_types: + for base in t.__bases__: + if base is not object and base is not BaseModel: + print(f"{base.__name__} <|-- {t.__name__}") + + print("@enduml") + + +def generate_class(t): + print(f"class {t.__name__}{{") + for field_name, field_type in get_annotations(t).items(): + print(f"\t{generate_type(field_type)} {field_name}") + print("}") + + +def generate_type(field_type): + return ( + field_type.__name__ + if isclass(field_type) and issubclass(field_type, BaseModel) + else str(field_type) + ) + + +if __name__ == "__main__": + main()