diff --git a/src/artemis/device_setup_plans/unit_tests/test_utils.py b/src/artemis/device_setup_plans/unit_tests/test_utils.py new file mode 100644 index 000000000..3a350486c --- /dev/null +++ b/src/artemis/device_setup_plans/unit_tests/test_utils.py @@ -0,0 +1,56 @@ +from unittest.mock import MagicMock + +import pytest +from bluesky import RunEngine +from bluesky import plan_stubs as bps +from bluesky.run_engine import RunEngine +from dodal.beamlines import i03 + +from artemis.device_setup_plans.utils import ( + start_preparing_data_collection_then_do_plan, +) + + +def test_given_plan_raises_when_exception_raised_then_eiger_disarmed_and_correct_exception_returned(): + class TestException(Exception): + pass + + def my_plan(): + yield from bps.null() + raise TestException() + + eiger = i03.eiger(fake_with_ophyd_sim=True) + eiger.detector_params = MagicMock() + eiger.async_stage = MagicMock() + eiger.disarm_detector = MagicMock() + + RE = RunEngine() + + with pytest.raises(TestException): + RE( + start_preparing_data_collection_then_do_plan( + eiger, MagicMock(), 0.2, my_plan() + ) + ) + + # Check detector was armed + eiger.async_stage.assert_called_once() + + eiger.disarm_detector.assert_called_once() + + +def test_when_preparing_data_collection_then_transmission_set(): + def my_plan(): + yield from bps.null() + + attenuator = MagicMock() + RE = RunEngine() + + RE( + start_preparing_data_collection_then_do_plan( + MagicMock(), attenuator, 0.2, my_plan() + ) + ) + + # Check transmission set + attenuator.set.assert_called_once_with(0.2) diff --git a/src/artemis/device_setup_plans/utils.py b/src/artemis/device_setup_plans/utils.py new file mode 100644 index 000000000..5f4c38f29 --- /dev/null +++ b/src/artemis/device_setup_plans/utils.py @@ -0,0 +1,30 @@ +from typing import Generator + +from bluesky import plan_stubs as bps +from bluesky import preprocessors as bpp +from bluesky.utils import Msg +from dodal.devices.attenuator import Attenuator +from dodal.devices.eiger import EigerDetector + + +def start_preparing_data_collection_then_do_plan( + eiger: EigerDetector, + attenuator: Attenuator, + transmission_fraction: float, + plan_to_run: Generator[Msg, None, None], + group="ready_for_data_collection", +) -> Generator[Msg, None, None]: + """Starts preparing for the next data collection by arming the eiger and setting the + transmission. Then runs the given plan. If the plan fails it will stop disarm the eiger. + """ + yield from bps.abs_set(eiger.do_arm, 1, group=group) + yield from bps.abs_set( + attenuator, + transmission_fraction, + group=group, + ) + + yield from bpp.contingency_wrapper( + plan_to_run, + except_plan=lambda e: (yield from bps.stop(eiger)), + ) diff --git a/src/artemis/experiment_plans/experiment_registry.py b/src/artemis/experiment_plans/experiment_registry.py index 07e5b9571..80ad9adcc 100644 --- a/src/artemis/experiment_plans/experiment_registry.py +++ b/src/artemis/experiment_plans/experiment_registry.py @@ -7,6 +7,7 @@ from artemis.experiment_plans import ( fast_grid_scan_plan, full_grid_scan_plan, + pin_centre_then_xray_centre_plan, rotation_scan_plan, stepped_grid_scan_plan, ) @@ -24,6 +25,10 @@ GridScanWithEdgeDetectInternalParameters, GridScanWithEdgeDetectParams, ) +from artemis.parameters.plan_specific.pin_centre_then_xray_centre_params import ( + PinCentreThenXrayCentreInternalParameters, + PinCentreThenXrayCentreParams, +) from artemis.parameters.plan_specific.rotation_scan_internal_params import ( RotationInternalParameters, RotationScanParams, @@ -62,6 +67,12 @@ def do_nothing(): "experiment_param_type": RotationScanParams, "callback_collection_type": RotationCallbackCollection, }, + "pin_tip_centre_then_xray_centre": { + "setup": pin_centre_then_xray_centre_plan.create_devices, + "internal_param_type": PinCentreThenXrayCentreInternalParameters, + "experiment_param_type": PinCentreThenXrayCentreParams, + "callback_collection_type": NullPlanCallbackCollection, + }, "stepped_grid_scan": { "setup": stepped_grid_scan_plan.create_devices, "run": stepped_grid_scan_plan.get_plan, diff --git a/src/artemis/experiment_plans/full_grid_scan_plan.py b/src/artemis/experiment_plans/full_grid_scan_plan.py index 7c6ba436b..81bca50a0 100644 --- a/src/artemis/experiment_plans/full_grid_scan_plan.py +++ b/src/artemis/experiment_plans/full_grid_scan_plan.py @@ -15,6 +15,9 @@ from dodal.devices.eiger import EigerDetector from dodal.devices.oav.oav_parameters import OAV_CONFIG_FILE_DEFAULTS, OAVParameters +from artemis.device_setup_plans.utils import ( + start_preparing_data_collection_then_do_plan, +) from artemis.experiment_plans.fast_grid_scan_plan import ( create_devices as fgs_create_devices, ) @@ -80,35 +83,6 @@ def create_parameters_for_fast_grid_scan( return fast_grid_scan_parameters -def start_arming_then_do_grid( - parameters: GridScanWithEdgeDetectInternalParameters, - attenuator: Attenuator, - backlight: Backlight, - eiger: EigerDetector, - aperture_scatterguard: ApertureScatterguard, - detector_motion: DetectorMotion, - oav_params: OAVParameters, -): - # Start stage with asynchronous arming here - yield from bps.abs_set(eiger.do_arm, 1, group="ready_for_data_collection") - yield from bps.abs_set( - attenuator, - parameters.artemis_params.ispyb_params.transmission_fraction, - group="ready_for_data_collection", - ) - - yield from bpp.contingency_wrapper( - detect_grid_and_do_gridscan( - parameters, - backlight, - aperture_scatterguard, - detector_motion, - oav_params, - ), - except_plan=lambda e: (yield from bps.stop(eiger)), - ) - - def detect_grid_and_do_gridscan( parameters: GridScanWithEdgeDetectInternalParameters, backlight: Backlight, @@ -196,12 +170,13 @@ def full_grid_scan( oav_params = OAVParameters("xrayCentring", **oav_param_files) - return start_arming_then_do_grid( - parameters, - attenuator, - backlight, + plan_to_perform = detect_grid_and_do_gridscan( + parameters, backlight, aperture_scatterguard, detector_motion, oav_params + ) + + return start_preparing_data_collection_then_do_plan( eiger, - aperture_scatterguard, - detector_motion, - oav_params, + attenuator, + parameters.artemis_params.ispyb_params.transmission_fraction, + plan_to_perform, ) diff --git a/src/artemis/experiment_plans/pin_centre_then_xray_centre_plan.py b/src/artemis/experiment_plans/pin_centre_then_xray_centre_plan.py new file mode 100644 index 000000000..f8524794f --- /dev/null +++ b/src/artemis/experiment_plans/pin_centre_then_xray_centre_plan.py @@ -0,0 +1,89 @@ +import json + +from blueapi.core import MsgGenerator +from dodal.beamlines import i03 +from dodal.devices.attenuator import Attenuator +from dodal.devices.eiger import EigerDetector +from dodal.devices.oav.oav_parameters import OAV_CONFIG_FILE_DEFAULTS, OAVParameters + +from artemis.device_setup_plans.utils import ( + start_preparing_data_collection_then_do_plan, +) +from artemis.experiment_plans.full_grid_scan_plan import ( + create_devices as full_grid_create_devices, +) +from artemis.experiment_plans.full_grid_scan_plan import detect_grid_and_do_gridscan +from artemis.experiment_plans.pin_tip_centring_plan import ( + create_devices as pin_tip_create_devices, +) +from artemis.experiment_plans.pin_tip_centring_plan import pin_tip_centre_plan +from artemis.log import LOGGER +from artemis.parameters.plan_specific.grid_scan_with_edge_detect_params import ( + GridScanWithEdgeDetectInternalParameters, +) +from artemis.parameters.plan_specific.pin_centre_then_xray_centre_params import ( + PinCentreThenXrayCentreInternalParameters, +) + + +def create_devices(): + full_grid_create_devices() + pin_tip_create_devices() + + +def create_parameters_for_grid_detection( + pin_centre_parameters: PinCentreThenXrayCentreInternalParameters, +) -> GridScanWithEdgeDetectInternalParameters: + params_json = json.loads(pin_centre_parameters.json()) + grid_detect_and_xray_centre = GridScanWithEdgeDetectInternalParameters( + **params_json + ) + LOGGER.info( + f"Parameters for grid detect and xray centre: {grid_detect_and_xray_centre}" + ) + return grid_detect_and_xray_centre + + +def pin_centre_then_xray_centre_plan( + parameters: PinCentreThenXrayCentreInternalParameters, + oav_config_files=OAV_CONFIG_FILE_DEFAULTS, +): + """Plan that perfoms a pin tip centre followed by an xray centre to completely + centre the sample""" + oav_config_files["oav_config_json"] = parameters.experiment_params.oav_centring_file + + yield from pin_tip_centre_plan( + parameters.experiment_params.tip_offset_microns, oav_config_files + ) + grid_detect_params = create_parameters_for_grid_detection(parameters) + + backlight = i03.backlight() + aperture_scattergaurd = i03.aperture_scatterguard() + detector_motion = i03.detector_motion() + oav_params = OAVParameters("xrayCentring", **oav_config_files) + + yield from detect_grid_and_do_gridscan( + grid_detect_params, + backlight, + aperture_scattergaurd, + detector_motion, + oav_params, + ) + + +def pin_tip_centre_then_xray_centre( + parameters: PinCentreThenXrayCentreInternalParameters, +) -> MsgGenerator: + """Starts preparing for collection then performs the pin tip centre and xray centre""" + + eiger: EigerDetector = i03.eiger() + attenuator: Attenuator = i03.attenuator() + + eiger.set_detector_parameters(parameters.artemis_params.detector_params) + + return start_preparing_data_collection_then_do_plan( + eiger, + attenuator, + parameters.artemis_params.ispyb_params.transmission_fraction, + pin_centre_then_xray_centre_plan(parameters), + ) diff --git a/src/artemis/experiment_plans/pin_tip_centring_plan.py b/src/artemis/experiment_plans/pin_tip_centring_plan.py index 4376175e8..a62d57df8 100644 --- a/src/artemis/experiment_plans/pin_tip_centring_plan.py +++ b/src/artemis/experiment_plans/pin_tip_centring_plan.py @@ -89,7 +89,7 @@ def move_smargon_warn_on_out_of_range(smargon: Smargon, position: np.ndarray): def pin_tip_centre_plan( - tip_offset_microns: float = 900, + tip_offset_microns: float, oav_config_files: Dict[str, str] = OAV_CONFIG_FILE_DEFAULTS, ): """Finds the tip of the pin and moves to roughly the centre based on this tip. Does @@ -116,6 +116,10 @@ def offset_and_move(tip: Pixel): LOGGER.info(f"Tip offset in pixels: {tip_offset_px}") + # need to wait for the OAV image to update + # See #673 for improvements + yield from bps.sleep(0.3) + yield from pre_centring_setup_oav(oav, oav_params) tip = yield from move_pin_into_view(oav, smargon) diff --git a/src/artemis/experiment_plans/tests/test_full_grid_scan_plan.py b/src/artemis/experiment_plans/tests/test_full_grid_scan_plan.py index 916801d5e..727c87dda 100644 --- a/src/artemis/experiment_plans/tests/test_full_grid_scan_plan.py +++ b/src/artemis/experiment_plans/tests/test_full_grid_scan_plan.py @@ -2,7 +2,7 @@ from unittest.mock import ANY, MagicMock, patch import pytest -from bluesky import RunEngine +from bluesky.run_engine import RunEngine from dodal.beamlines.i03 import detector_motion from dodal.devices.aperturescatterguard import AperturePositions, ApertureScatterguard from dodal.devices.backlight import Backlight @@ -15,7 +15,6 @@ create_devices, detect_grid_and_do_gridscan, full_grid_scan, - start_arming_then_do_grid, wait_for_det_to_finish_moving, ) from artemis.external_interaction.callbacks.oav_snapshot_callback import ( @@ -222,78 +221,3 @@ def test_when_full_grid_scan_run_then_parameters_sent_to_fgs_as_expected( # Parameters can be serialized params.json() - - -@patch( - "artemis.experiment_plans.full_grid_scan_plan.grid_detection_plan", autospec=True -) -@patch( - "artemis.experiment_plans.full_grid_scan_plan.OavSnapshotCallback", - autospec=True, - spec_set=True, -) -def test_grid_detection_running_when_exception_raised_then_eiger_disarmed_and_correct_exception_returned( - mock_oav_callback: MagicMock, - mock_grid_detection_plan: MagicMock, - eiger: EigerDetector, - RE: RunEngine, - test_full_grid_scan_params: GridScanWithEdgeDetectInternalParameters, - mock_subscriptions: MagicMock, - test_config_files: Dict, -): - class DetectException(Exception): - pass - - mock_grid_detection_plan.side_effect = DetectException() - eiger.detector_params = MagicMock() - eiger.async_stage = MagicMock() - eiger.disarm_detector = MagicMock() - - with patch( - "artemis.external_interaction.callbacks.fgs.fgs_callback_collection.FGSCallbackCollection.from_params", - return_value=mock_subscriptions, - autospec=True, - ): - with pytest.raises(DetectException): - RE( - start_arming_then_do_grid( - parameters=test_full_grid_scan_params, - attenuator=MagicMock(), - backlight=MagicMock(), - eiger=eiger, - aperture_scatterguard=MagicMock(), - detector_motion=MagicMock(), - oav_params=OAVParameters("xrayCentring", **test_config_files), - ) - ) - - # Check detector was armed - eiger.async_stage.assert_called_once() - - eiger.disarm_detector.assert_called_once() - - -@patch("artemis.experiment_plans.full_grid_scan_plan.detect_grid_and_do_gridscan") -def test_when_start_arming_then_transmission_set( - mock_grid_detection_plan: MagicMock, - RE: RunEngine, - test_full_grid_scan_params: GridScanWithEdgeDetectInternalParameters, - test_config_files: Dict, -): - attenuator = MagicMock() - RE( - start_arming_then_do_grid( - parameters=test_full_grid_scan_params, - attenuator=attenuator, - backlight=MagicMock(), - eiger=MagicMock(), - aperture_scatterguard=MagicMock(), - detector_motion=MagicMock(), - oav_params=OAVParameters("xrayCentring", **test_config_files), - ) - ) - - # Check transmission set - attenuator.set.assert_called_once_with( - test_full_grid_scan_params.artemis_params.ispyb_params.transmission_fraction - ) diff --git a/src/artemis/experiment_plans/tests/test_pin_centre_then_xray_centre_plan.py b/src/artemis/experiment_plans/tests/test_pin_centre_then_xray_centre_plan.py new file mode 100644 index 000000000..ebbca5256 --- /dev/null +++ b/src/artemis/experiment_plans/tests/test_pin_centre_then_xray_centre_plan.py @@ -0,0 +1,67 @@ +from unittest.mock import MagicMock, patch + +import pytest +from bluesky.run_engine import RunEngine + +from artemis.experiment_plans.pin_centre_then_xray_centre_plan import ( + create_parameters_for_grid_detection, + pin_centre_then_xray_centre_plan, +) +from artemis.parameters.external_parameters import from_file as raw_params_from_file +from artemis.parameters.plan_specific.grid_scan_with_edge_detect_params import ( + GridScanWithEdgeDetectParams, +) +from artemis.parameters.plan_specific.pin_centre_then_xray_centre_params import ( + PinCentreThenXrayCentreInternalParameters, +) + + +@pytest.fixture +def test_pin_centre_then_xray_centre_params(): + params = raw_params_from_file( + "src/artemis/parameters/tests/test_data/good_test_pin_centre_then_xray_centre_parameters.json" + ) + return PinCentreThenXrayCentreInternalParameters(**params) + + +def test_when_create_parameters_for_grid_detection_thne_parameters_created( + test_pin_centre_then_xray_centre_params: PinCentreThenXrayCentreInternalParameters, +): + grid_detect_params = create_parameters_for_grid_detection( + test_pin_centre_then_xray_centre_params + ) + + assert isinstance( + grid_detect_params.experiment_params, GridScanWithEdgeDetectParams + ) + assert grid_detect_params.experiment_params.exposure_time == 0.1 + + +@patch( + "artemis.experiment_plans.pin_centre_then_xray_centre_plan.pin_tip_centre_plan", + autospec=True, +) +@patch( + "artemis.experiment_plans.pin_centre_then_xray_centre_plan.detect_grid_and_do_gridscan", + autospec=True, +) +@patch( + "artemis.experiment_plans.pin_centre_then_xray_centre_plan.i03", + autospec=True, +) +def test_when_pin_centre_xray_centre_called_then_plan_runs_correctly( + mock_i03, + mock_detect_and_do_gridscan: MagicMock, + mock_pin_tip_centre: MagicMock, + test_pin_centre_then_xray_centre_params: PinCentreThenXrayCentreInternalParameters, + test_config_files, +): + RE = RunEngine() + RE( + pin_centre_then_xray_centre_plan( + test_pin_centre_then_xray_centre_params, test_config_files + ) + ) + + mock_detect_and_do_gridscan.assert_called_once() + mock_pin_tip_centre.assert_called_once() diff --git a/src/artemis/parameters/plan_specific/grid_scan_with_edge_detect_params.py b/src/artemis/parameters/plan_specific/grid_scan_with_edge_detect_params.py index c12e6795d..48070c8b6 100644 --- a/src/artemis/parameters/plan_specific/grid_scan_with_edge_detect_params.py +++ b/src/artemis/parameters/plan_specific/grid_scan_with_edge_detect_params.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any import numpy as np from dodal.devices.detector import TriggerMode @@ -32,7 +32,7 @@ class GridScanWithEdgeDetectParams(AbstractExperimentParameterBase): omega_start: float # This is the correct grid size for single pin - grid_width_microns: Optional[float] = 161 + grid_width_microns: float = 600 def get_num_images(self): return 0 @@ -83,7 +83,7 @@ def _preprocess_artemis_params( all_params["num_triggers"] = all_params["num_images"] all_params["num_images_per_trigger"] = 1 all_params["trigger_mode"] = TriggerMode.FREE_RUN - all_params["upper_left"] = np.array([0, 0, 0]) + all_params["upper_left"] = np.zeros(3, dtype=np.int32) return GridscanArtemisParameters( **extract_artemis_params_from_flat_dict( all_params, cls._artemis_param_key_definitions() @@ -91,7 +91,7 @@ def _preprocess_artemis_params( ) def get_data_shape(self): - raise Exception("Data shape does not apply to this type of experiment!") + raise TypeError("Data shape does not apply to this type of experiment!") - def get_scan_points(cls): - raise Exception("Scan points do not apply to this type of experiment!") + def get_scan_points(self): + raise TypeError("Scan points do not apply to this type of experiment!") diff --git a/src/artemis/parameters/plan_specific/pin_centre_then_xray_centre_params.py b/src/artemis/parameters/plan_specific/pin_centre_then_xray_centre_params.py new file mode 100644 index 000000000..737bc9be7 --- /dev/null +++ b/src/artemis/parameters/plan_specific/pin_centre_then_xray_centre_params.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from typing import Any + +import numpy as np +from dodal.devices.detector import TriggerMode +from dodal.parameters.experiment_parameter_base import AbstractExperimentParameterBase +from pydantic import validator +from pydantic.dataclasses import dataclass + +from artemis.external_interaction.ispyb.ispyb_dataclass import GridscanIspybParams +from artemis.parameters.internal_parameters import ( + ArtemisParameters, + InternalParameters, + extract_artemis_params_from_flat_dict, + extract_experiment_params_from_flat_dict, +) +from artemis.parameters.plan_specific.fgs_internal_params import ( + GridscanArtemisParameters, +) + + +@dataclass +class PinCentreThenXrayCentreParams(AbstractExperimentParameterBase): + """ + Holder class for the parameters of a plan that does a pin centre then xray centre + """ + + exposure_time: float + snapshot_dir: str + detector_distance: float + omega_start: float + + tip_offset_microns: float = 0 + oav_centring_file: str = "/dls_sw/i03/software/gda/configurations/i03-config/etc/OAVCentring_hyperion.json" + + # Width for single pin + grid_width_microns: float = 600 + + def get_num_images(self): + return 0 + + +class PinCentreThenXrayCentreInternalParameters(InternalParameters): + experiment_params: PinCentreThenXrayCentreParams + artemis_params: GridscanArtemisParameters + + class Config: + arbitrary_types_allowed = True + json_encoders = { + **ArtemisParameters.Config.json_encoders, + } + + @staticmethod + def _artemis_param_key_definitions() -> tuple[list[str], list[str], list[str]]: + ( + artemis_param_field_keys, + detector_field_keys, + ispyb_field_keys, + ) = InternalParameters._artemis_param_key_definitions() + ispyb_field_keys += list(GridscanIspybParams.__annotations__.keys()) + return artemis_param_field_keys, detector_field_keys, ispyb_field_keys + + @validator("experiment_params", pre=True) + def _preprocess_experiment_params( + cls, + experiment_params: dict[str, Any], + ): + return PinCentreThenXrayCentreParams( + **extract_experiment_params_from_flat_dict( + PinCentreThenXrayCentreParams, experiment_params + ) + ) + + @validator("artemis_params", pre=True) + def _preprocess_artemis_params( + cls, all_params: dict[str, Any], values: dict[str, Any] + ): + experiment_params: PinCentreThenXrayCentreParams = values["experiment_params"] + all_params["num_images"] = experiment_params.get_num_images() + all_params["position"] = np.array(all_params["position"]) + all_params["omega_increment"] = 0 + all_params["num_triggers"] = all_params["num_images"] + all_params["num_images_per_trigger"] = 1 + all_params["trigger_mode"] = TriggerMode.FREE_RUN + all_params["upper_left"] = np.zeros(3, dtype=np.int32) + return GridscanArtemisParameters( + **extract_artemis_params_from_flat_dict( + all_params, cls._artemis_param_key_definitions() + ) + ) + + def get_data_shape(self): + raise TypeError("Data shape does not apply to this type of experiment!") + + def get_scan_points(self): + raise TypeError("Scan points do not apply to this type of experiment!") diff --git a/src/artemis/parameters/tests/test_data/good_test_pin_centre_then_xray_centre_parameters.json b/src/artemis/parameters/tests/test_data/good_test_pin_centre_then_xray_centre_parameters.json new file mode 100644 index 000000000..18fff4ba6 --- /dev/null +++ b/src/artemis/parameters/tests/test_data/good_test_pin_centre_then_xray_centre_parameters.json @@ -0,0 +1,49 @@ +{ + "params_version": "2.2.0", + "artemis_params": { + "beamline": "BL03S", + "insertion_prefix": "SR03S", + "detector": "EIGER2_X_16M", + "zocalo_environment": "devrmq", + "experiment_type": "pin_tip_centre_then_xray_centre", + "detector_params": { + "current_energy_ev": 100, + "directory": "/tmp", + "prefix": "file_name", + "run_number": 0, + "use_roi_mode": false, + "det_dist_to_beam_converter_path": "src/artemis/unit_tests/test_lookup_table.txt" + }, + "ispyb_params": { + "visit_path": "/tmp/cm31105-4/", + "microns_per_pixel_x": 1.0, + "microns_per_pixel_y": 1.0, + "position": [ + 10.0, + 20.0, + 30.0 + ], + "transmission_fraction": 1.0, + "flux": 10.0, + "wavelength": 0.01, + "beam_size_x": 1.0, + "beam_size_y": 1.0, + "slit_gap_size_x": 1.0, + "slit_gap_size_y": 1.0, + "focal_spot_size_x": 1.0, + "focal_spot_size_y": 1.0, + "comment": "test", + "resolution": 1.0, + "sample_barcode": "test" + } + }, + "experiment_params": { + "snapshot_dir": "/tmp", + "exposure_time": 0.1, + "detector_distance": 100.0, + "omega_start": 0.0, + "tip_offset_microns": 108.9, + "grid_width_microns": 290.6, + "oav_centring_file": "src/artemis/experiment_plans/tests/test_data/OAVCentring.json" + } +} \ No newline at end of file