diff --git a/run_artemis.sh b/run_artemis.sh index 70c40dfd5..5a7d59532 100755 --- a/run_artemis.sh +++ b/run_artemis.sh @@ -112,7 +112,7 @@ if [[ $START == 1 ]]; then ARTEMIS_LOG_DIR=/dls_sw/$BEAMLINE/logs/bluesky fi fi - echo "Logging to $ARTEMIS_LOG_DIR" + echo "$(date) Logging to $ARTEMIS_LOG_DIR" export ARTEMIS_LOG_DIR mkdir -p $ARTEMIS_LOG_DIR start_log_path=$ARTEMIS_LOG_DIR/start_log.txt @@ -129,15 +129,16 @@ if [[ $START == 1 ]]; then for i in "${!args[@]}" do if [ "${args[$i]}" != false ]; then commands+="${arg_strings[$i]} "; fi; - done + done python -m artemis `echo $commands;`>$start_log_path 2>&1 & - echo "Waiting for Artemis to boot" + echo "$(date) Waiting for Artemis to boot" - for i in {1..10} + for i in {1..30} do curl --head -X GET http://localhost:5005/status >/dev/null + echo "$(date)" ret_value=$? if [ $ret_value -ne 0 ]; then sleep 1 @@ -147,10 +148,10 @@ if [[ $START == 1 ]]; then done if [ $ret_value -ne 0 ]; then - echo "Artemis Failed to start!!!!" + echo "$(date) Artemis Failed to start!!!!" exit 1 else - echo "Artemis started" + echo "$(date) Artemis started" fi fi diff --git a/setup.cfg b/setup.cfg index 98f4de518..b38c32d34 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,7 @@ install_requires = xarray doct databroker - dodal @ git+https://github.com/DiamondLightSource/python-dodal.git@c2cf54029018fa82b568fa067e2ef479d10cf634 + dodal @ git+https://github.com/DiamondLightSource/python-dodal.git@2dbcb05d2ce6223880c24fca3c6b8a9b88b2ba03 pydantic<2.0 # See https://github.com/DiamondLightSource/python-artemis/issues/774 diff --git a/src/artemis/device_setup_plans/setup_oav.py b/src/artemis/device_setup_plans/setup_oav.py index e0ace9640..fc25aa085 100644 --- a/src/artemis/device_setup_plans/setup_oav.py +++ b/src/artemis/device_setup_plans/setup_oav.py @@ -76,13 +76,15 @@ def pre_centring_setup_oav(oav: OAV, parameters: OAVParameters): parameters.minimum_height, ) - # Connect MXSC output to MJPG input yield from start_mxsc( oav, parameters.min_callback_time, parameters.detection_script_filename, ) + # Connect MXSC output to MJPG input for debugging + yield from bps.abs_set(oav.snapshot.input_plugin, "OAV.MXSC") + zoom_level_str = f"{float(parameters.zoom)}x" if zoom_level_str not in oav.zoom_controller.allowed_zoom_levels: raise OAVError_ZoomLevelNotFound( diff --git a/src/artemis/experiment_plans/fast_grid_scan_plan.py b/src/artemis/experiment_plans/fast_grid_scan_plan.py index 8965fb385..fac4ee27d 100755 --- a/src/artemis/experiment_plans/fast_grid_scan_plan.py +++ b/src/artemis/experiment_plans/fast_grid_scan_plan.py @@ -14,6 +14,7 @@ Backlight, EigerDetector, FastGridScan, + Flux, S4SlitGaps, Smargon, Synchrotron, @@ -54,6 +55,7 @@ class FGSComposite: backlight: Backlight eiger: EigerDetector fast_grid_scan: FastGridScan + flux: Flux s4_slit_gaps: S4SlitGaps sample_motors: Smargon synchrotron: Synchrotron @@ -74,6 +76,7 @@ def __init__( wait_for_connection=False, fake_with_ophyd_sim=fake, params=detector_params ) self.fast_grid_scan = i03.fast_grid_scan(fake_with_ophyd_sim=fake) + self.flux = i03.flux(fake_with_ophyd_sim=fake) self.s4_slit_gaps = i03.s4_slit_gaps(fake_with_ophyd_sim=fake) self.sample_motors = i03.smargon(fake_with_ophyd_sim=fake) self.undulator = i03.undulator(fake_with_ophyd_sim=fake) @@ -128,6 +131,7 @@ def read_hardware_for_ispyb( undulator: Undulator, synchrotron: Synchrotron, s4_slit_gaps: S4SlitGaps, + flux: Flux, ): artemis.log.LOGGER.info( "Reading status of beamline parameters for ispyb deposition." @@ -139,6 +143,7 @@ def read_hardware_for_ispyb( yield from bps.read(synchrotron.machine_status.synchrotron_mode) yield from bps.read(s4_slit_gaps.xgap) yield from bps.read(s4_slit_gaps.ygap) + yield from bps.read(flux.flux_reading) yield from bps.save() @@ -208,6 +213,7 @@ def run_gridscan( fgs_composite.undulator, fgs_composite.synchrotron, fgs_composite.s4_slit_gaps, + fgs_composite.flux, ) fgs_motors = fgs_composite.fast_grid_scan @@ -218,24 +224,21 @@ def run_gridscan( @bpp.set_run_key_decorator("do_fgs") @bpp.run_decorator(md={"subplan_name": "do_fgs"}) + @bpp.contingency_decorator( + except_plan=lambda e: (yield from bps.stop(fgs_composite.eiger)), + else_plan=lambda: (yield from bps.unstage(fgs_composite.eiger)), + ) def do_fgs(): - try: - yield from bps.wait() # Wait for all moves to complete - yield from bps.kickoff(fgs_motors) - yield from bps.complete(fgs_motors, wait=True) - finally: - yield from bps.unstage(fgs_composite.eiger) - - # Wait for arming to finish - artemis.log.LOGGER.info("Waiting for arming...") - yield from bps.wait("arming") - artemis.log.LOGGER.info("Arming finished") + yield from bps.wait() # Wait for all moves to complete + yield from bps.kickoff(fgs_motors) + yield from bps.complete(fgs_motors, wait=True) + + yield from bps.stage(fgs_composite.eiger) with TRACER.start_span("do_fgs"): yield from do_fgs() - with TRACER.start_span("move_to_z_0"): - yield from bps.abs_set(fgs_motors.z_steps, 0, wait=False) + yield from bps.abs_set(fgs_motors.z_steps, 0, wait=False) @bpp.set_run_key_decorator("run_gridscan_and_move") diff --git a/src/artemis/experiment_plans/full_grid_scan.py b/src/artemis/experiment_plans/full_grid_scan.py index b6202da50..07e420041 100644 --- a/src/artemis/experiment_plans/full_grid_scan.py +++ b/src/artemis/experiment_plans/full_grid_scan.py @@ -1,7 +1,9 @@ from __future__ import annotations +import json from typing import TYPE_CHECKING, Callable +import numpy as np from bluesky import plan_stubs as bps from bluesky import preprocessors as bpp from dodal.beamlines import i03 @@ -19,15 +21,15 @@ create_devices as oav_create_devices, ) from artemis.experiment_plans.oav_grid_detection_plan import grid_detection_plan -from artemis.external_interaction.callbacks.fgs.fgs_callback_collection import ( - FGSCallbackCollection, -) from artemis.external_interaction.callbacks.oav_snapshot_callback import ( OavSnapshotCallback, ) from artemis.log import LOGGER from artemis.parameters.beamline_parameters import get_beamline_parameters -from artemis.parameters.plan_specific.fgs_internal_params import GridScanParams +from artemis.parameters.plan_specific.fgs_internal_params import ( + FGSInternalParameters, + GridScanParams, +) if TYPE_CHECKING: from artemis.parameters.plan_specific.grid_scan_with_edge_detect_params import ( @@ -65,6 +67,17 @@ def wait_for_det_to_finish_moving(detector: DetectorMotion, timeout=120): raise TimeoutError("Detector not finished moving") +def create_parameters_for_fast_grid_scan( + grid_scan_with_edge_params: GridScanWithEdgeDetectInternalParameters, + grid_parameters: GridScanParams, +) -> FGSInternalParameters: + params_json = json.loads(grid_scan_with_edge_params.json()) + params_json["experiment_params"] = json.loads(grid_parameters.json()) + fast_grid_scan_parameters = FGSInternalParameters(**params_json) + LOGGER.info(f"Parameters for FGS: {fast_grid_scan_parameters}") + return fast_grid_scan_parameters + + def start_arming_then_do_grid( parameters: GridScanWithEdgeDetectInternalParameters, backlight: Backlight, @@ -76,7 +89,7 @@ def start_arming_then_do_grid( # Start stage with asynchronous arming here yield from bps.abs_set(eiger.do_arm, 1, group="arming") - yield from bpp.finalize_wrapper( + yield from bpp.contingency_wrapper( detect_grid_and_do_gridscan( parameters, backlight, @@ -84,7 +97,7 @@ def start_arming_then_do_grid( detector_motion, oav_params, ), - bps.unstage(eiger), + except_plan=lambda e: (yield from bps.stop(eiger)), ) @@ -96,7 +109,7 @@ def detect_grid_and_do_gridscan( oav_params: OAVParameters, ): experiment_params: GridScanWithEdgeDetectParams = parameters.experiment_params - fgs_params = GridScanParams(dwell_time=experiment_params.exposure_time * 1000) + grid_params = GridScanParams(dwell_time=experiment_params.exposure_time * 1000) detector_params = parameters.artemis_params.detector_params snapshot_template = ( @@ -121,15 +134,15 @@ def run_grid_detection_plan( yield from run_grid_detection_plan( oav_params, - fgs_params, + grid_params, snapshot_template, experiment_params.snapshot_dir, ) # Hack because GDA only passes 3 values to ispyb - out_upper_left = oav_callback.out_upper_left[0] + [ - oav_callback.out_upper_left[1][1] - ] + out_upper_left = np.array( + oav_callback.out_upper_left[0] + [oav_callback.out_upper_left[1][1]] + ) # Hack because the callback returns the list in inverted order parameters.artemis_params.ispyb_params.xtal_snapshots_omega_start = ( @@ -140,12 +153,9 @@ def run_grid_detection_plan( ) parameters.artemis_params.ispyb_params.upper_left = out_upper_left - parameters.experiment_params = fgs_params - - parameters.artemis_params.detector_params.num_triggers = fgs_params.get_num_images() - - LOGGER.info(f"Parameters for FGS: {parameters}") - subscriptions = FGSCallbackCollection.from_params(parameters) + fast_grid_scan_parameters = create_parameters_for_fast_grid_scan( + parameters, grid_params + ) yield from bps.abs_set(backlight.pos, Backlight.OUT) LOGGER.info( @@ -156,7 +166,7 @@ def run_grid_detection_plan( ) yield from wait_for_det_to_finish_moving(detector_motion) - yield from fgs_get_plan(parameters, subscriptions) + yield from fgs_get_plan(fast_grid_scan_parameters) def get_plan( diff --git a/src/artemis/experiment_plans/oav_grid_detection_plan.py b/src/artemis/experiment_plans/oav_grid_detection_plan.py index 9ecb131e3..4843cb9a5 100644 --- a/src/artemis/experiment_plans/oav_grid_detection_plan.py +++ b/src/artemis/experiment_plans/oav_grid_detection_plan.py @@ -8,10 +8,9 @@ import numpy as np from bluesky.preprocessors import finalize_wrapper from dodal.beamlines import i03 -from dodal.devices.areadetector.plugins.MXSC import PinTipDetect from dodal.devices.fast_grid_scan import GridScanParams from dodal.devices.oav.oav_calculations import camera_coordinates_to_xyz -from dodal.devices.oav.oav_detector import OAV +from dodal.devices.oav.oav_detector import MXSC, OAV from dodal.devices.smargon import Smargon from artemis.device_setup_plans.setup_oav import pre_centring_setup_oav @@ -49,10 +48,16 @@ def grid_detection_plan( ) -def wait_for_tip_to_be_found(pin_tip: PinTipDetect): +def wait_for_tip_to_be_found(mxsc: MXSC): + pin_tip = mxsc.pin_tip yield from bps.trigger(pin_tip, wait=True) found_tip = yield from bps.rd(pin_tip) if found_tip == pin_tip.INVALID_POSITION: + top_edge = yield from bps.rd(mxsc.top) + bottom_edge = yield from bps.rd(mxsc.bottom) + LOGGER.info( + f"No tip found with top/bottom of {list(top_edge), list(bottom_edge)}" + ) raise WarningException( f"No pin found after {pin_tip.validity_timeout.get()} seconds" ) @@ -104,7 +109,7 @@ def grid_detection_main_plan( # See #673 for improvements yield from bps.sleep(0.3) - tip_x_px, tip_y_px = yield from wait_for_tip_to_be_found(oav.mxsc.pin_tip) + tip_x_px, tip_y_px = yield from wait_for_tip_to_be_found(oav.mxsc) LOGGER.info(f"Tip is at x,y: {tip_x_px},{tip_y_px}") @@ -208,5 +213,7 @@ def grid_detection_main_plan( def reset_oav(): + """Changes the MJPG stream to look at the camera without the edge detection and turns off the edge detcetion plugin.""" oav = i03.oav() + yield from bps.abs_set(oav.snapshot.input_plugin, "OAV.CAM") yield from bps.abs_set(oav.mxsc.enable_callbacks, 0) diff --git a/src/artemis/experiment_plans/tests/conftest.py b/src/artemis/experiment_plans/tests/conftest.py index f70c69ab9..0495848b3 100644 --- a/src/artemis/experiment_plans/tests/conftest.py +++ b/src/artemis/experiment_plans/tests/conftest.py @@ -4,6 +4,7 @@ from bluesky.run_engine import RunEngine from dodal.beamlines import i03 from dodal.devices.aperturescatterguard import AperturePositions +from dodal.devices.smargon import Smargon from artemis.experiment_plans.fast_grid_scan_plan import FGSComposite from artemis.external_interaction.callbacks.fgs.fgs_callback_collection import ( @@ -44,7 +45,7 @@ def eiger(): @pytest.fixture -def smargon(): +def smargon() -> Smargon: smargon = i03.smargon(fake_with_ophyd_sim=True) smargon.x.user_setpoint._use_limits = False smargon.y.user_setpoint._use_limits = False @@ -104,7 +105,7 @@ def test_full_grid_scan_params(): @pytest.fixture -def fake_fgs_composite(test_fgs_params: InternalParameters): +def fake_fgs_composite(smargon: Smargon, test_fgs_params: InternalParameters): fake_composite = FGSComposite( aperture_positions=AperturePositions( LARGE=(1, 2, 3, 4, 5), @@ -128,6 +129,8 @@ def fake_fgs_composite(test_fgs_params: InternalParameters): fake_composite.fast_grid_scan.scan_invalid.sim_put(False) fake_composite.fast_grid_scan.position_counter.sim_put(0) + fake_composite.sample_motors = smargon + return fake_composite diff --git a/src/artemis/experiment_plans/tests/test_fast_grid_scan_plan.py b/src/artemis/experiment_plans/tests/test_fast_grid_scan_plan.py index 8202dc957..60e6ac42d 100644 --- a/src/artemis/experiment_plans/tests/test_fast_grid_scan_plan.py +++ b/src/artemis/experiment_plans/tests/test_fast_grid_scan_plan.py @@ -84,13 +84,16 @@ def test_read_hardware_for_ispyb_updates_from_ophyd_devices( fake_fgs_composite.s4_slit_gaps.xgap.user_readback.sim_put(xgap_test_value) fake_fgs_composite.s4_slit_gaps.ygap.user_readback.sim_put(ygap_test_value) + flux_test_value = 10.0 + fake_fgs_composite.flux.flux_reading.sim_put(flux_test_value) + test_ispyb_callback = FGSISPyBHandlerCallback(test_fgs_params) test_ispyb_callback.ispyb = MagicMock() RE.subscribe(test_ispyb_callback) - def standalone_read_hardware_for_ispyb(und, syn, slits): + def standalone_read_hardware_for_ispyb(und, syn, slits, fl): yield from bps.open_run() - yield from read_hardware_for_ispyb(und, syn, slits) + yield from read_hardware_for_ispyb(und, syn, slits, fl) yield from bps.close_run() RE( @@ -98,6 +101,7 @@ def standalone_read_hardware_for_ispyb(und, syn, slits): fake_fgs_composite.undulator, fake_fgs_composite.synchrotron, fake_fgs_composite.s4_slit_gaps, + fake_fgs_composite.flux, ) ) params = test_ispyb_callback.params @@ -333,71 +337,52 @@ def test_when_grid_scan_ran_then_eiger_disarmed_before_zocalo_end( mock_parent.assert_has_calls([call.disarm(), call.run_end(0), call.run_end(0)]) -@patch("artemis.experiment_plans.fast_grid_scan_plan.bps.abs_set") -@patch("artemis.experiment_plans.fast_grid_scan_plan.bps.kickoff") +@patch("artemis.experiment_plans.fast_grid_scan_plan.bps.wait") @patch("artemis.experiment_plans.fast_grid_scan_plan.bps.complete") -@patch("artemis.experiment_plans.fast_grid_scan_plan.bps.mv") -@patch("artemis.experiment_plans.fast_grid_scan_plan.wait_for_fgs_valid") -def test_when_exception_occurs_during_running_then_eiger_disarmed( - wait_for_valid, - mock_mv, +def test_fgs_arms_eiger_without_grid_detect( mock_complete, - mock_kickoff, - mock_abs_set, + mock_wait, fake_fgs_composite: FGSComposite, test_fgs_params: FGSInternalParameters, - mock_subscriptions: FGSCallbackCollection, RE: RunEngine, ): - fake_fgs_composite.eiger.disarm_detector = MagicMock() - - fake_fgs_composite.eiger.filewriters_finished = Status() - fake_fgs_composite.eiger.filewriters_finished.set_finished() - fake_fgs_composite.eiger.odin.check_odin_state = MagicMock(return_value=True) - fake_fgs_composite.eiger.odin.file_writer.num_captured.sim_put(1200) - fake_fgs_composite.eiger.filewriters_finished = Status() - fake_fgs_composite.eiger.filewriters_finished.set_finished() - - fake_fgs_composite.eiger.stage = MagicMock( - return_value=Status(None, None, 0, True, True) - ) - - mock_complete.side_effect = Exception() - - with pytest.raises(Exception): - RE( - run_gridscan_and_move( - fake_fgs_composite, - test_fgs_params, - mock_subscriptions, - ) - ) + fake_fgs_composite.eiger.stage = MagicMock() + fake_fgs_composite.eiger.unstage = MagicMock() - fake_fgs_composite.eiger.disarm_detector.assert_called_once() + RE(run_gridscan(fake_fgs_composite, test_fgs_params)) + fake_fgs_composite.eiger.stage.assert_called_once() + fake_fgs_composite.eiger.unstage.assert_called_once() -# Eiger is armed if eiger.armed_status is complete and fan ready is called. This test is very slow - could mocking more functions could speed it up -def test_fgs_arms_eiger_without_grid_detect( +@patch("artemis.experiment_plans.fast_grid_scan_plan.bps.wait") +@patch("artemis.experiment_plans.fast_grid_scan_plan.bps.complete") +def test_when_grid_scan_fails_then_detector_disarmed_and_correct_exception_returned( + mock_complete, + mock_wait, fake_fgs_composite: FGSComposite, test_fgs_params: FGSInternalParameters, - mock_subscriptions: FGSCallbackCollection, RE: RunEngine, ): - def get_good_status(): - status = Status() - status.set_finished() - return status + class CompleteException(Exception): + pass - fake_fgs_composite.eiger.odin.check_odin_state = MagicMock(return_value=True) - fake_fgs_composite.eiger.odin.check_odin_initialised = MagicMock( - return_value=[True, True] - ) - fake_fgs_composite.eiger.set_odin_pvs = MagicMock(return_value=get_good_status()) - fake_fgs_composite.eiger.stale_params.sim_put(0) - fake_fgs_composite.eiger._wait_fan_ready = MagicMock(return_value=get_good_status()) - fake_fgs_composite.eiger._wait_for_odin_status = MagicMock( - return_value=get_good_status() + mock_complete.side_effect = CompleteException() + + fake_fgs_composite.eiger.stage = MagicMock( + return_value=Status(None, None, 0, True, True) ) - RE(bps.stage(fake_fgs_composite.eiger)) - fake_fgs_composite.eiger._wait_fan_ready.assert_called_once() + fake_fgs_composite.eiger.odin.check_odin_state = MagicMock() + + fake_fgs_composite.eiger.disarm_detector = MagicMock() + fake_fgs_composite.eiger.disable_roi_mode = MagicMock() + + # Without the complete finishing we will not get all the images + fake_fgs_composite.eiger.ALL_FRAMES_TIMEOUT = 0.1 + + # Want to get the underlying completion error, not the one raised from unstage + with pytest.raises(CompleteException): + RE(run_gridscan(fake_fgs_composite, test_fgs_params)) + + fake_fgs_composite.eiger.disable_roi_mode.assert_called() + fake_fgs_composite.eiger.disarm_detector.assert_called() 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 fbf4c20a9..b8793261e 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 @@ -9,6 +9,7 @@ from dodal.devices.detector_motion import DetectorMotion from dodal.devices.eiger import EigerDetector from dodal.devices.oav.oav_parameters import OAVParameters +from numpy.testing import assert_array_equal from artemis.experiment_plans.full_grid_scan import ( create_devices, @@ -17,6 +18,10 @@ start_arming_then_do_grid, wait_for_det_to_finish_moving, ) +from artemis.external_interaction.callbacks.oav_snapshot_callback import ( + OavSnapshotCallback, +) +from artemis.parameters.plan_specific.fgs_internal_params import FGSInternalParameters from artemis.parameters.plan_specific.grid_scan_with_edge_detect_params import ( GridScanWithEdgeDetectInternalParameters, ) @@ -35,12 +40,12 @@ def _fake_grid_detection( out_parameters.y2_start = 0 out_parameters.z1_start = 0 out_parameters.z2_start = 0 - out_parameters.x_steps = 0 - out_parameters.y_steps = 0 - out_parameters.z_steps = 0 - out_parameters.x_step_size = 0 - out_parameters.y_step_size = 0 - out_parameters.z_step_size = 0 + out_parameters.x_steps = 10 + out_parameters.y_steps = 2 + out_parameters.z_steps = 2 + out_parameters.x_step_size = 1 + out_parameters.y_step_size = 1 + out_parameters.z_step_size = 1 return [] @@ -75,22 +80,25 @@ def test_wait_for_detector(RE): RE(wait_for_det_to_finish_moving(d_m, 0.5)) -def test_get_plan(test_fgs_params, test_config_files, mock_subscriptions): - with patch("artemis.experiment_plans.full_grid_scan.i03"), patch( - "artemis.experiment_plans.full_grid_scan.FGSCallbackCollection.from_params", - lambda _: mock_subscriptions, - ): +def test_get_plan(test_fgs_params, test_config_files): + with patch("artemis.experiment_plans.full_grid_scan.i03"): plan = get_plan(test_fgs_params, test_config_files) assert isinstance(plan, Generator) -@patch("artemis.experiment_plans.full_grid_scan.wait_for_det_to_finish_moving") -@patch("artemis.experiment_plans.full_grid_scan.grid_detection_plan") -@patch("artemis.experiment_plans.full_grid_scan.fgs_get_plan") -@patch("artemis.experiment_plans.full_grid_scan.OavSnapshotCallback") +@patch( + "artemis.experiment_plans.full_grid_scan.wait_for_det_to_finish_moving", + autospec=True, +) +@patch("artemis.experiment_plans.full_grid_scan.grid_detection_plan", autospec=True) +@patch("artemis.experiment_plans.full_grid_scan.fgs_get_plan", autospec=True) +@patch( + "artemis.experiment_plans.full_grid_scan.OavSnapshotCallback", + autospec=True, +) def test_detect_grid_and_do_gridscan( - mock_oav_callback: MagicMock, + mock_oav_callback_init: MagicMock, mock_fast_grid_scan_plan: MagicMock, mock_grid_detection_plan: MagicMock, mock_wait_for_detector: MagicMock, @@ -99,19 +107,17 @@ def test_detect_grid_and_do_gridscan( aperture_scatterguard: ApertureScatterguard, RE: RunEngine, test_full_grid_scan_params: GridScanWithEdgeDetectInternalParameters, - mock_subscriptions: MagicMock, test_config_files: Dict, ): - mock_oav_callback.snapshot_filenames = [[], []] - mock_oav_callback.out_upper_left = [[1, 1], [1, 1]] + mock_oav_callback = OavSnapshotCallback() + mock_oav_callback.out_upper_left = [[0, 1], [2, 3]] + mock_oav_callback.snapshot_filenames = [["test"], ["test3"]] + mock_oav_callback_init.return_value = mock_oav_callback mock_grid_detection_plan.side_effect = _fake_grid_detection with patch.object( aperture_scatterguard, "set", MagicMock() - ) as mock_aperture_scatterguard, patch( - "artemis.external_interaction.callbacks.fgs.fgs_callback_collection.FGSCallbackCollection.from_params", - return_value=mock_subscriptions, - ): + ) as mock_aperture_scatterguard: RE( detect_grid_and_do_gridscan( parameters=test_full_grid_scan_params, @@ -125,7 +131,7 @@ def test_detect_grid_and_do_gridscan( mock_grid_detection_plan.assert_called_once() # Verify callback to oav snaposhot was called - mock_oav_callback.assert_called_once() + mock_oav_callback_init.assert_called_once() # Check backlight was moved OUT assert backlight.pos.get() == Backlight.OUT @@ -139,27 +145,100 @@ def test_detect_grid_and_do_gridscan( mock_wait_for_detector.assert_called_once() # Check we called out to underlying fast grid scan plan - mock_fast_grid_scan_plan.assert_called_once_with(ANY, mock_subscriptions) + mock_fast_grid_scan_plan.assert_called_once_with(ANY) + + +@patch( + "artemis.experiment_plans.full_grid_scan.wait_for_det_to_finish_moving", + autospec=True, +) +@patch("artemis.experiment_plans.full_grid_scan.grid_detection_plan", autospec=True) +@patch("artemis.experiment_plans.full_grid_scan.fgs_get_plan", autospec=True) +@patch("artemis.experiment_plans.full_grid_scan.OavSnapshotCallback", autospec=True) +def test_when_full_grid_scan_run_then_parameters_sent_to_fgs_as_expected( + mock_oav_callback_init: MagicMock, + mock_fast_grid_scan_plan: MagicMock, + mock_grid_detection_plan: MagicMock, + _: MagicMock, + eiger: EigerDetector, + backlight: Backlight, + detector_motion: DetectorMotion, + aperture_scatterguard: ApertureScatterguard, + RE: RunEngine, + test_full_grid_scan_params: GridScanWithEdgeDetectInternalParameters, + test_config_files: Dict, +): + mock_oav_callback = OavSnapshotCallback() + mock_oav_callback.snapshot_filenames = [["a", "b", "c"], ["d", "e", "f"]] + mock_oav_callback.out_upper_left = [[1, 2], [1, 3]] + + mock_oav_callback_init.return_value = mock_oav_callback + + mock_grid_detection_plan.side_effect = _fake_grid_detection + + with patch.object(eiger.do_arm, "set", MagicMock()), patch.object( + aperture_scatterguard, "set", MagicMock() + ): + RE( + detect_grid_and_do_gridscan( + parameters=test_full_grid_scan_params, + backlight=backlight, + aperture_scatterguard=aperture_scatterguard, + detector_motion=detector_motion, + oav_params=OAVParameters("xrayCentring", **test_config_files), + ) + ) + + params: FGSInternalParameters = mock_fast_grid_scan_plan.call_args[0][0] + + assert isinstance(params, FGSInternalParameters) + + ispyb_params = params.artemis_params.ispyb_params + assert_array_equal(ispyb_params.upper_left, [1, 2, 3]) + assert ispyb_params.xtal_snapshots_omega_start == [ + "c", + "b", + "a", + ] + assert ispyb_params.xtal_snapshots_omega_end == [ + "f", + "e", + "d", + ] + + assert params.artemis_params.detector_params.num_triggers == 40 + + assert params.experiment_params.x_axis.full_steps == 10 + assert params.experiment_params.y_axis.end == 1 + + # Parameters can be serialized + params.json() @patch("artemis.experiment_plans.full_grid_scan.grid_detection_plan") @patch("artemis.experiment_plans.full_grid_scan.OavSnapshotCallback") -def test_grid_detection_running_when_exception_raised_then_eiger_unstaged( +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, ): - mock_grid_detection_plan.side_effect = Exception() - eiger: EigerDetector = MagicMock(spec=EigerDetector) + 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, ): - with pytest.raises(Exception): + with pytest.raises(DetectException): RE( start_arming_then_do_grid( parameters=test_full_grid_scan_params, @@ -172,6 +251,6 @@ def test_grid_detection_running_when_exception_raised_then_eiger_unstaged( ) # Check detector was armed - eiger.do_arm.set.assert_called_once_with(1) + eiger.async_stage.assert_called_once() - eiger.unstage.assert_called_once() + eiger.disarm_detector.assert_called_once() diff --git a/src/artemis/experiment_plans/tests/test_grid_detection_plan.py b/src/artemis/experiment_plans/tests/test_grid_detection_plan.py index b14463f0b..2cdd2f825 100644 --- a/src/artemis/experiment_plans/tests/test_grid_detection_plan.py +++ b/src/artemis/experiment_plans/tests/test_grid_detection_plan.py @@ -139,3 +139,42 @@ def test_create_devices(create_device: MagicMock): ], any_order=True, ) + + +@patch("dodal.beamlines.beamline_utils.active_device_is_same_type", lambda a, b: True) +@patch("bluesky.plan_stubs.wait") +@patch("bluesky.plan_stubs.mv") +@patch("dodal.devices.areadetector.plugins.MJPG.requests") +@patch("dodal.devices.areadetector.plugins.MJPG.Image") +def test_when_grid_detection_plan_run_twice_then_values_do_not_persist_in_callback( + mock_image_class: MagicMock, + mock_requests: MagicMock, + bps_mv: MagicMock, + bps_wait: MagicMock, + RE: RunEngine, + test_config_files, +): + mock_image = MagicMock() + mock_image_class.open.return_value = mock_image + oav, smargon, bl = fake_create_devices() + + oav.mxsc.pin_tip.tip_x.sim_put(100) + oav.mxsc.pin_tip.tip_y.sim_put(100) + + params = OAVParameters(context="loopCentring", **test_config_files) + gridscan_params = GridScanParams() + + for _ in range(2): + cb = OavSnapshotCallback() + RE.subscribe(cb) + + RE( + grid_detection_plan( + parameters=params, + out_parameters=gridscan_params, + snapshot_dir="tmp", + snapshot_template="test_{angle}", + ) + ) + assert len(cb.snapshot_filenames) == 2 + assert len(cb.out_upper_left) == 2 diff --git a/src/artemis/external_interaction/callbacks/oav_snapshot_callback.py b/src/artemis/external_interaction/callbacks/oav_snapshot_callback.py index 58325c4d7..51354b8eb 100644 --- a/src/artemis/external_interaction/callbacks/oav_snapshot_callback.py +++ b/src/artemis/external_interaction/callbacks/oav_snapshot_callback.py @@ -2,8 +2,13 @@ class OavSnapshotCallback(CallbackBase): - snapshot_filenames: list = [] - out_upper_left: list = [] + snapshot_filenames: list + out_upper_left: list + + def __init__(self, *args) -> None: + super().__init__(*args) + self.snapshot_filenames = [] + self.out_upper_left = [] def event(self, doc): data = doc.get("data") diff --git a/src/artemis/external_interaction/ispyb/ispyb_dataclass.py b/src/artemis/external_interaction/ispyb/ispyb_dataclass.py index 05a2a016a..bcdb519f6 100644 --- a/src/artemis/external_interaction/ispyb/ispyb_dataclass.py +++ b/src/artemis/external_interaction/ispyb/ispyb_dataclass.py @@ -67,7 +67,6 @@ def _parse_position( return np.array(position) transmission: float - flux: float wavelength: float beam_size_x: float beam_size_y: float @@ -80,6 +79,7 @@ def _parse_position( sample_barcode: Optional[str] = None # Optional from GDA as populated by Ophyd + flux: Optional[float] = None undulator_gap: Optional[float] = None synchrotron_mode: Optional[str] = None slit_gap_size_x: Optional[float] = None diff --git a/src/artemis/parameters/plan_specific/tests/test_fgs_internal_parameters.py b/src/artemis/parameters/plan_specific/tests/test_fgs_internal_parameters.py index 342264422..f41a7b9e9 100644 --- a/src/artemis/parameters/plan_specific/tests/test_fgs_internal_parameters.py +++ b/src/artemis/parameters/plan_specific/tests/test_fgs_internal_parameters.py @@ -11,6 +11,7 @@ def test_FGS_parameters_load_from_file(): "src/artemis/parameters/tests/test_data/good_test_parameters.json" ) internal_parameters = FGSInternalParameters(**params) + internal_parameters.json() assert isinstance(internal_parameters.experiment_params, GridScanParams) diff --git a/src/artemis/system_tests/test_main_system.py b/src/artemis/system_tests/test_main_system.py index cd40a39b4..021187f92 100644 --- a/src/artemis/system_tests/test_main_system.py +++ b/src/artemis/system_tests/test_main_system.py @@ -300,6 +300,7 @@ def test_cli_args_parse(): assert test_args == ("DEBUG", True, True, True) +@patch("dodal.beamlines.i03.Flux") @patch("dodal.beamlines.i03.DetectorMotion") @patch("dodal.beamlines.i03.OAV") @patch("dodal.beamlines.i03.ApertureScatterguard") @@ -327,6 +328,7 @@ def test_when_blueskyrunner_initiated_then_plans_are_setup_and_devices_connected aperture_scatterguard, oav, detector_motion, + flux, ): type_comparison.return_value = True BlueskyRunner(MagicMock(), skip_startup_connection=False) @@ -341,6 +343,7 @@ def test_when_blueskyrunner_initiated_then_plans_are_setup_and_devices_connected aperture_scatterguard.return_value.wait_for_connection.assert_called() oav.return_value.wait_for_connection.assert_called() detector_motion.return_value.wait_for_connection.assert_called() + flux.return_value.wait_for_connection.assert_called() @patch("artemis.experiment_plans.fast_grid_scan_plan.EigerDetector")