From af118c64e3ce03fbd226fd57f8f3e65d912925d9 Mon Sep 17 00:00:00 2001 From: rtuck99 Date: Tue, 6 Aug 2024 11:20:28 +0100 Subject: [PATCH 1/6] (#842) Move the ispyb activation so that messages can be logged from pin tip detection (#1502) --- .../grid_detect_then_xray_centre_plan.py | 19 +- .../pin_centre_then_xray_centre_plan.py | 30 +-- .../test_ispyb_dev_connection.py | 2 +- tests/unit_tests/conftest.py | 57 ++++++ .../test_grid_detect_then_xray_centre_plan.py | 187 +++++++++++++++--- .../test_pin_centre_then_xray_centre_plan.py | 42 ++++ .../callbacks/conftest.py | 48 +---- 7 files changed, 283 insertions(+), 102 deletions(-) diff --git a/src/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py b/src/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py index 88910bc9e..0ed219f3e 100644 --- a/src/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +++ b/src/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py @@ -108,16 +108,6 @@ def detect_grid_and_do_gridscan( composite: GridDetectThenXRayCentreComposite, parameters: GridScanWithEdgeDetect, oav_params: OAVParameters, -): - yield from ispyb_activation_wrapper( - _detect_grid_and_do_gridscan(composite, parameters, oav_params), parameters - ) - - -def _detect_grid_and_do_gridscan( - composite: GridDetectThenXRayCentreComposite, - parameters: GridScanWithEdgeDetect, - oav_params: OAVParameters, ): assert composite.aperture_scatterguard.aperture_positions is not None @@ -202,10 +192,13 @@ def grid_detect_then_xray_centre( oav_params = OAVParameters("xrayCentring", oav_config) - plan_to_perform = detect_grid_and_do_gridscan( - composite, + plan_to_perform = ispyb_activation_wrapper( + detect_grid_and_do_gridscan( + composite, + parameters, + oav_params, + ), parameters, - oav_params, ) return start_preparing_data_collection_then_do_plan( diff --git a/src/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py b/src/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py index d0c769aea..15a5ff68e 100644 --- a/src/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +++ b/src/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py @@ -17,6 +17,9 @@ PinTipCentringComposite, pin_tip_centre_plan, ) +from hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( + ispyb_activation_wrapper, +) from hyperion.log import LOGGER from hyperion.parameters.constants import CONST from hyperion.parameters.gridscan import ( @@ -61,21 +64,24 @@ def pin_centre_then_xray_centre_plan( pin_tip_detection=composite.pin_tip_detection, ) - yield from pin_tip_centre_plan( - pin_tip_centring_composite, - parameters.tip_offset_um, - oav_config_file, - ) + def _pin_centre_then_xray_centre_plan(): + yield from pin_tip_centre_plan( + pin_tip_centring_composite, + parameters.tip_offset_um, + oav_config_file, + ) - grid_detect_params = create_parameters_for_grid_detection(parameters) + grid_detect_params = create_parameters_for_grid_detection(parameters) - oav_params = OAVParameters("xrayCentring", oav_config_file) + oav_params = OAVParameters("xrayCentring", oav_config_file) - yield from detect_grid_and_do_gridscan( - composite, - grid_detect_params, - oav_params, - ) + yield from detect_grid_and_do_gridscan( + composite, + grid_detect_params, + oav_params, + ) + + yield from ispyb_activation_wrapper(_pin_centre_then_xray_centre_plan(), parameters) def pin_tip_centre_then_xray_centre( diff --git a/tests/system_tests/external_interaction/test_ispyb_dev_connection.py b/tests/system_tests/external_interaction/test_ispyb_dev_connection.py index 01c57a171..97ab98451 100644 --- a/tests/system_tests/external_interaction/test_ispyb_dev_connection.py +++ b/tests/system_tests/external_interaction/test_ispyb_dev_connection.py @@ -784,7 +784,7 @@ def test_ispyb_deposition_in_rotation_plan( assert dcid is not None assert ( fetch_comment(dcid) - == "Sample position: (1.0, 2.0, 3.0) test Aperture: Small. " + == "Sample position (µm): (1000, 2000, 3000) test Aperture: Small. " ) expected_values = EXPECTED_DATACOLLECTION_FOR_ROTATION | { diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index ad994c979..904f230c1 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -2,6 +2,9 @@ from unittest.mock import patch import pytest +from event_model import Event, EventDescriptor + +from hyperion.parameters.constants import CONST BANNED_PATHS = [Path("/dls"), Path("/dls_sw")] @@ -21,3 +24,57 @@ def patched_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_grid_snapshot_microns_per_pixel_x": 1.25, + "oav_grid_snapshot_microns_per_pixel_y": 1.5, + "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_grid_snapshot_microns_per_pixel_x": 1.25, + "oav_grid_snapshot_microns_per_pixel_y": 1.5, + "smargon-omega": -90, + "smargon-x": 0, + "smargon-y": 0, + "smargon-z": 0, + }, + } diff --git a/tests/unit_tests/experiment_plans/test_grid_detect_then_xray_centre_plan.py b/tests/unit_tests/experiment_plans/test_grid_detect_then_xray_centre_plan.py index fa9a3ee18..765ce299e 100644 --- a/tests/unit_tests/experiment_plans/test_grid_detect_then_xray_centre_plan.py +++ b/tests/unit_tests/experiment_plans/test_grid_detect_then_xray_centre_plan.py @@ -3,7 +3,9 @@ import bluesky.plan_stubs as bps import pytest +from bluesky import Msg from bluesky.run_engine import RunEngine +from bluesky.simulators import RunEngineSimulator, assert_message_and_return_remaining from dodal.beamlines import i03 from dodal.devices.backlight import BacklightPosition from dodal.devices.eiger import EigerDetector @@ -17,9 +19,14 @@ detect_grid_and_do_gridscan, grid_detect_then_xray_centre, ) +from hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import ( + ispyb_activation_wrapper, +) from hyperion.parameters.constants import CONST from hyperion.parameters.gridscan import GridScanWithEdgeDetect, ThreeDGridScan +from ..conftest import OavGridSnapshotTestEvents + def _fake_grid_detection( devices: OavGridDetectionComposite, @@ -82,6 +89,20 @@ def test_full_grid_scan(test_fgs_params, test_config_files): assert isinstance(plan, Generator) +@pytest.fixture +def grid_detect_devices_with_oav_config_params( + grid_detect_devices: GridDetectThenXRayCentreComposite, test_config_files +) -> GridDetectThenXRayCentreComposite: + grid_detect_devices.oav.parameters = OAVConfigParams( + test_config_files["zoom_params_file"], test_config_files["display_config"] + ) + grid_detect_devices.oav.parameters.micronsPerXPixel = 0.806 + grid_detect_devices.oav.parameters.micronsPerYPixel = 0.806 + grid_detect_devices.oav.parameters.beam_centre_i = 549 + grid_detect_devices.oav.parameters.beam_centre_j = 347 + return grid_detect_devices + + @patch( "hyperion.experiment_plans.grid_detect_then_xray_centre_plan.grid_detection_plan", autospec=True, @@ -93,32 +114,33 @@ def test_full_grid_scan(test_fgs_params, test_config_files): async def test_detect_grid_and_do_gridscan( mock_flyscan_xray_centre_plan: MagicMock, mock_grid_detection_plan: MagicMock, - grid_detect_devices: GridDetectThenXRayCentreComposite, + grid_detect_devices_with_oav_config_params: GridDetectThenXRayCentreComposite, RE: RunEngine, smargon: Smargon, test_full_grid_scan_params: GridScanWithEdgeDetect, test_config_files: Dict, ): mock_grid_detection_plan.side_effect = _fake_grid_detection - grid_detect_devices.oav.parameters = OAVConfigParams( - test_config_files["zoom_params_file"], test_config_files["display_config"] + assert ( + grid_detect_devices_with_oav_config_params.aperture_scatterguard.aperture_positions + is not None ) - grid_detect_devices.oav.parameters.micronsPerXPixel = 0.806 - grid_detect_devices.oav.parameters.micronsPerYPixel = 0.806 - grid_detect_devices.oav.parameters.beam_centre_i = 549 - grid_detect_devices.oav.parameters.beam_centre_j = 347 - assert grid_detect_devices.aperture_scatterguard.aperture_positions is not None with patch.object( - grid_detect_devices.aperture_scatterguard, "set", MagicMock() + grid_detect_devices_with_oav_config_params.aperture_scatterguard, + "set", + MagicMock(), ) as mock_aperture_scatterguard: RE( - detect_grid_and_do_gridscan( - grid_detect_devices, - parameters=test_full_grid_scan_params, - oav_params=OAVParameters( - "xrayCentring", test_config_files["oav_config_json"] + ispyb_activation_wrapper( + detect_grid_and_do_gridscan( + grid_detect_devices_with_oav_config_params, + parameters=test_full_grid_scan_params, + oav_params=OAVParameters( + "xrayCentring", test_config_files["oav_config_json"] + ), ), + test_full_grid_scan_params, ) ) # Verify we called the grid detection plan @@ -126,13 +148,13 @@ async def test_detect_grid_and_do_gridscan( # Check backlight was moved OUT assert ( - await grid_detect_devices.backlight.position.get_value() + await grid_detect_devices_with_oav_config_params.backlight.position.get_value() == BacklightPosition.OUT ) # Check aperture was changed to SMALL mock_aperture_scatterguard.assert_called_once_with( - grid_detect_devices.aperture_scatterguard.aperture_positions.SMALL + grid_detect_devices_with_oav_config_params.aperture_scatterguard.aperture_positions.SMALL ) # Check we called out to underlying fast grid scan plan @@ -151,7 +173,7 @@ def test_when_full_grid_scan_run_then_parameters_sent_to_fgs_as_expected( mock_flyscan_xray_centre_plan: MagicMock, mock_grid_detection_plan: MagicMock, eiger: EigerDetector, - grid_detect_devices: GridDetectThenXRayCentreComposite, + grid_detect_devices_with_oav_config_params: GridDetectThenXRayCentreComposite, RE: RunEngine, test_full_grid_scan_params: GridScanWithEdgeDetect, test_config_files: Dict, @@ -161,22 +183,19 @@ def test_when_full_grid_scan_run_then_parameters_sent_to_fgs_as_expected( mock_grid_detection_plan.side_effect = _fake_grid_detection - grid_detect_devices.oav.parameters = OAVConfigParams( - test_config_files["zoom_params_file"], test_config_files["display_config"] - ) - grid_detect_devices.oav.parameters.micronsPerXPixel = 0.806 - grid_detect_devices.oav.parameters.micronsPerYPixel = 0.806 - grid_detect_devices.oav.parameters.beam_centre_i = 549 - grid_detect_devices.oav.parameters.beam_centre_j = 347 - with patch.object(eiger.do_arm, "set", MagicMock()), patch.object( - grid_detect_devices.aperture_scatterguard, "set", MagicMock() + grid_detect_devices_with_oav_config_params.aperture_scatterguard, + "set", + MagicMock(), ): RE( - detect_grid_and_do_gridscan( - grid_detect_devices, - parameters=test_full_grid_scan_params, - oav_params=oav_params, + ispyb_activation_wrapper( + detect_grid_and_do_gridscan( + grid_detect_devices_with_oav_config_params, + parameters=test_full_grid_scan_params, + oav_params=oav_params, + ), + test_full_grid_scan_params, ) ) @@ -189,3 +208,111 @@ def test_when_full_grid_scan_run_then_parameters_sent_to_fgs_as_expected( # Parameters can be serialized params.json() + + +@patch( + "hyperion.experiment_plans.grid_detect_then_xray_centre_plan.grid_detection_plan", + autospec=True, +) +@patch( + "hyperion.experiment_plans.grid_detect_then_xray_centre_plan.flyscan_xray_centre", + autospec=True, +) +def test_detect_grid_and_do_gridscan_does_not_activate_ispyb_callback( + mock_flyscan_xray_centre, + mock_grid_detection_plan, + grid_detect_devices_with_oav_config_params: GridDetectThenXRayCentreComposite, + sim_run_engine: RunEngineSimulator, + test_full_grid_scan_params: GridScanWithEdgeDetect, + test_config_files, +): + mock_grid_detection_plan.return_value = iter([Msg("save_oav_grids")]) + sim_run_engine.add_handler_for_callback_subscribes() + sim_run_engine.add_callback_handler_for_multiple( + "save_oav_grids", + [ + [ + ( + "descriptor", + OavGridSnapshotTestEvents.test_descriptor_document_oav_snapshot, # type: ignore + ), + ( + "event", + OavGridSnapshotTestEvents.test_event_document_oav_snapshot_xy, # type: ignore + ), + ( + "event", + OavGridSnapshotTestEvents.test_event_document_oav_snapshot_xz, # type: ignore + ), + ] + ], + ) + + msgs = sim_run_engine.simulate_plan( + detect_grid_and_do_gridscan( + grid_detect_devices_with_oav_config_params, + test_full_grid_scan_params, + OAVParameters("xrayCentring", test_config_files["oav_config_json"]), + ) + ) + + activations = [ + msg + for msg in msgs + if msg.command == "open_run" + and "GridscanISPyBCallback" in msg.kwargs["activate_callbacks"] + ] + assert not activations + + +@patch( + "hyperion.experiment_plans.grid_detect_then_xray_centre_plan.grid_detection_plan", + autospec=True, +) +@patch( + "hyperion.experiment_plans.grid_detect_then_xray_centre_plan.flyscan_xray_centre", + autospec=True, +) +def test_grid_detect_then_xray_centre_activates_ispyb_callback( + mock_flyscan_xray_centre, + mock_grid_detection_plan, + sim_run_engine: RunEngineSimulator, + grid_detect_devices_with_oav_config_params: GridDetectThenXRayCentreComposite, + test_full_grid_scan_params: GridScanWithEdgeDetect, + test_config_files, +): + mock_grid_detection_plan.return_value = iter([Msg("save_oav_grids")]) + + sim_run_engine.add_handler_for_callback_subscribes() + sim_run_engine.add_callback_handler_for_multiple( + "save_oav_grids", + [ + [ + ( + "descriptor", + OavGridSnapshotTestEvents.test_descriptor_document_oav_snapshot, # type: ignore + ), + ( + "event", + OavGridSnapshotTestEvents.test_event_document_oav_snapshot_xy, # type: ignore + ), + ( + "event", + OavGridSnapshotTestEvents.test_event_document_oav_snapshot_xz, # type: ignore + ), + ] + ], + ) + msgs = sim_run_engine.simulate_plan( + grid_detect_then_xray_centre( + grid_detect_devices_with_oav_config_params, + test_full_grid_scan_params, + test_config_files["oav_config_json"], + ) + ) + + assert_message_and_return_remaining( + msgs, + lambda msg: msg.command == "open_run" + and "GridscanISPyBCallback" in msg.kwargs["activate_callbacks"], + ) diff --git a/tests/unit_tests/experiment_plans/test_pin_centre_then_xray_centre_plan.py b/tests/unit_tests/experiment_plans/test_pin_centre_then_xray_centre_plan.py index 7ff6571a7..744b55d3f 100644 --- a/tests/unit_tests/experiment_plans/test_pin_centre_then_xray_centre_plan.py +++ b/tests/unit_tests/experiment_plans/test_pin_centre_then_xray_centre_plan.py @@ -147,3 +147,45 @@ def add_handlers_to_simulate_detector_motion(msg: Msg): lambda msg: msg.command == "open_run" and msg.kwargs["subplan_name"] == "do_fgs", ) + + +@patch( + "hyperion.experiment_plans.pin_centre_then_xray_centre_plan.pin_tip_centre_plan", + autospec=True, +) +@patch( + "hyperion.experiment_plans.pin_centre_then_xray_centre_plan.detect_grid_and_do_gridscan", + autospec=True, +) +def test_pin_centre_then_xray_centre_plan_activates_ispyb_callback_before_pin_tip_centre_plan( + mock_detect_grid_and_do_gridscan, + mock_pin_tip_centre_plan, + sim_run_engine: RunEngineSimulator, + test_pin_centre_then_xray_centre_params: PinTipCentreThenXrayCentre, + test_config_files, +): + mock_detect_grid_and_do_gridscan.return_value = iter( + [Msg("detect_grid_and_do_gridscan")] + ) + mock_pin_tip_centre_plan.return_value = iter([Msg("pin_tip_centre_plan")]) + + msgs = sim_run_engine.simulate_plan( + pin_centre_then_xray_centre_plan( + MagicMock(), + test_pin_centre_then_xray_centre_params, + test_config_files["oav_config_json"], + ) + ) + + msgs = assert_message_and_return_remaining( + msgs, + lambda msg: msg.command == "open_run" + and "GridscanISPyBCallback" in msg.kwargs["activate_callbacks"], + ) + msgs = assert_message_and_return_remaining( + msgs, lambda msg: msg.command == "pin_tip_centre_plan" + ) + msgs = assert_message_and_return_remaining( + msgs, lambda msg: msg.command == "detect_grid_and_do_gridscan" + ) + assert_message_and_return_remaining(msgs, lambda msg: msg.command == "close_run") diff --git a/tests/unit_tests/external_interaction/callbacks/conftest.py b/tests/unit_tests/external_interaction/callbacks/conftest.py index 761f1f0b6..ab934f77e 100644 --- a/tests/unit_tests/external_interaction/callbacks/conftest.py +++ b/tests/unit_tests/external_interaction/callbacks/conftest.py @@ -8,6 +8,7 @@ from tests.conftest import create_dummy_scan_spec from ....conftest import default_raw_params, raw_params_from_file +from ...conftest import OavGridSnapshotTestEvents def dummy_params(): @@ -32,7 +33,7 @@ def test_rotation_start_outer_document(dummy_rotation_params): } -class TestData: +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" @@ -129,11 +130,6 @@ class TestData: "zocalo_environment": "dev_artemis", "scan_points": create_dummy_scan_spec(10, 20, 30), } - 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_descriptor_document_oav_rotation_snapshot: EventDescriptor = { "uid": "c7d698ce-6d49-4c56-967e-7d081f964573", "run_start": "d8bee3ee-f614-4e7a-a516-25d6b9e87ef3", @@ -162,46 +158,6 @@ class TestData: "uid": "32d7c25c-c310-4292-ac78-36ce6509be3d", "data": {"oav_snapshot_last_saved_path": "snapshot_0"}, } - 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_grid_snapshot_microns_per_pixel_x": 1.25, - "oav_grid_snapshot_microns_per_pixel_y": 1.5, - "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, - }, - } - 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_grid_snapshot_microns_per_pixel_x": 1.25, - "oav_grid_snapshot_microns_per_pixel_y": 1.5, - "smargon-omega": -90, - }, - } test_event_document_pre_data_collection: Event = { "descriptor": "bd45c2e5-2b85-4280-95d7-a9a15800a78b", "time": 1666604299.828203, From 3eabba56d68e3f3c198fd25516060edcdc1106da Mon Sep 17 00:00:00 2001 From: rtuck99 Date: Wed, 7 Aug 2024 16:59:25 +0100 Subject: [PATCH 2/6] 1474 panda gridscan smargon speed (#1498) * (#1474) Configure panda sequencer table to generate trigger sequence internally rather than using clock generator. * Add saved new panda layout inc. screehshot. * Update unit tests * (#1474) move save-panda to dodal * (#1474) Tidy up documentation * Make pyright happy * Remove redundant comment from tests * (#1474) load the .yaml device layout as a module resource * Fix CI unit test failure due to bluesky event loop not running * Simplify the sequencer table as per PR comment * Update docstring --- docs/panda-gridscan-layout.png | Bin 0 -> 108184 bytes .../device_setup_plans/setup_panda.py | 115 ++++++------- .../flyscan_xray_centre_plan.py | 5 - .../resources/panda/panda-gridscan.yaml | 86 ++++++++-- tests/conftest.py | 2 +- .../device_setup_plans/test_setup_panda.py | 152 ++++++++++-------- .../test_flyscan_xray_centre_plan.py | 4 - 7 files changed, 213 insertions(+), 151 deletions(-) create mode 100644 docs/panda-gridscan-layout.png rename tests/test_data/flyscan_pcap_ignore_seq.yaml => src/hyperion/resources/panda/panda-gridscan.yaml (93%) mode change 100755 => 100644 diff --git a/docs/panda-gridscan-layout.png b/docs/panda-gridscan-layout.png new file mode 100644 index 0000000000000000000000000000000000000000..a3bd55c01e539d841408b5e70ace5b9a192f9973 GIT binary patch literal 108184 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 literal 0 HcmV?d00001 diff --git a/src/hyperion/device_setup_plans/setup_panda.py b/src/hyperion/device_setup_plans/setup_panda.py index e3099b390..6d24fc2df 100644 --- a/src/hyperion/device_setup_plans/setup_panda.py +++ b/src/hyperion/device_setup_plans/setup_panda.py @@ -1,5 +1,6 @@ import os from enum import Enum +from importlib import resources from pathlib import Path import bluesky.plan_stubs as bps @@ -15,11 +16,13 @@ seq_table_from_rows, ) +import hyperion.resources.panda from hyperion.log import LOGGER MM_TO_ENCODER_COUNTS = 200000 GENERAL_TIMEOUT = 60 DETECTOR_TRIGGER_WIDTH = 1e-4 +TICKS_PER_MS = 1000 # Panda sequencer prescaler will be set to us class Enabled(Enum): @@ -32,30 +35,35 @@ class PcapArm(Enum): DISARMED = "Disarm" -def get_seq_table( - parameters: PandAGridScanParams, - exposure_distance_mm, +def _get_seq_table( + parameters: PandAGridScanParams, exposure_distance_mm, time_between_steps_ms ) -> SeqTable: """ - -Exposure distance is the distance travelled by the sample each time the detector is exposed: exposure time * sample velocity - -Setting a 'signal' means trigger PCAP internally and send signal to Eiger via physical panda output - -When we wait for the position to be greater/lower, give a safe distance (X_STEP_SIZE/2 * MM_TO_ENCODER counts) to ensure the final trigger point - is captured + Generate the sequencer table for the panda. + + - Sending a 'trigger' means trigger PCAP internally and send signal to Eiger via physical panda output + SEQUENCER TABLE: - 1:Wait for physical trigger from motion script to mark start of scan / change of direction - 2:Wait for POSA (X2) to be greater than X_START, then - send a signal out every (minimum eiger exposure time + eiger dead time) - 3:Wait for POSA (X2) to be greater than X_START + X_STEP_SIZE + a safe distance for the final trigger, then cut out the signal - 4:Wait for physical trigger from motion script to mark change of direction - 5:Wait for POSA (X2) to be less than X_START + X_STEP_SIZE + exposure distance, then - send a signal out every (minimum eiger exposure time + eiger dead time) - 6:Wait for POSA (X2) to be less than (X_START - safe distance + exposure distance), then cut out signal - 7:Go back to step one. + + 1. Wait for physical trigger from motion script to mark start of scan / change of direction + 2. Wait for POSA (X2) to be greater than X_START and send x_steps triggers every time_between_steps_ms + 3. Wait for physical trigger from motion script to mark change of direction + 4. Wait for POSA (X2) to be less than X_START + X_STEP_SIZE * x_steps + exposure distance, then + send x_steps triggers every time_between_steps_ms + 5. Go back to step one. For a more detailed explanation and a diagram, see https://github.com/DiamondLightSource/hyperion/wiki/PandA-constant%E2%80%90motion-scanning - """ - safe_distance_x_counts = int(MM_TO_ENCODER_COUNTS * parameters.x_step_size / 2) + For documentation on Panda itself, see https://pandablocks.github.io/PandABlocks-FPGA/master/index.html + + Args: + exposure_distance_mm: The distance travelled by the sample each time the detector is exposed: exposure time * sample velocity + time_between_steps_ms: The time taken to traverse between each grid step. + parameters: Parameters for the panda gridscan + + Returns: + An instance of SeqTable describing the panda sequencer table + """ start_of_grid_x_counts = int(parameters.x_start * MM_TO_ENCODER_COUNTS) @@ -67,42 +75,43 @@ def get_seq_table( exposure_distance_x_counts = int(exposure_distance_mm * MM_TO_ENCODER_COUNTS) + num_pulses = parameters.x_steps + + delay_between_pulses = time_between_steps_ms * TICKS_PER_MS + + PULSE_WIDTH_US = 1 + + assert delay_between_pulses > PULSE_WIDTH_US + + # BITA_1 trigger wired from TTLIN1, this is the trigger input + + # +ve direction scan rows = [SeqTableRow(trigger=SeqTrigger.BITA_1, time2=1)] + rows.append( SeqTableRow( + repeats=num_pulses, trigger=SeqTrigger.POSA_GT, position=start_of_grid_x_counts, - time2=1, + time1=PULSE_WIDTH_US, outa1=True, - outa2=True, - ) - ) - rows.append( - SeqTableRow( - position=end_of_grid_x_counts + safe_distance_x_counts, - trigger=SeqTrigger.POSA_GT, - time2=1, + time2=delay_between_pulses - PULSE_WIDTH_US, + outa2=False, ) ) + # -ve direction scan rows.append(SeqTableRow(trigger=SeqTrigger.BITA_1, time2=1)) + rows.append( SeqTableRow( + repeats=num_pulses, trigger=SeqTrigger.POSA_LT, position=end_of_grid_x_counts + exposure_distance_x_counts, - time2=1, + time1=PULSE_WIDTH_US, outa1=True, - outa2=True, - ) - ) - - rows.append( - SeqTableRow( - trigger=SeqTrigger.POSA_LT, - position=start_of_grid_x_counts - - safe_distance_x_counts - + exposure_distance_x_counts, - time2=1, + time2=delay_between_pulses - PULSE_WIDTH_US, + outa2=False, ) ) @@ -113,7 +122,6 @@ def get_seq_table( def setup_panda_for_flyscan( panda: HDFPanda, - config_yaml_path: str, parameters: PandAGridScanParams, initial_x: float, exposure_time_s: float, @@ -127,19 +135,26 @@ def setup_panda_for_flyscan( Args: panda (HDFPanda): The PandA Ophyd device - config_yaml_path (str): Path to the yaml file containing the desired PandA PVs parameters (PandAGridScanParams): Grid parameters initial_x (float): Motor positions at time of PandA setup 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 / + time_between_x_steps_ms Returns: MsgGenerator Yields: Iterator[MsgGenerator] """ - yield from load_device(panda, config_yaml_path) + assert parameters.x_steps > 0 + assert time_between_x_steps_ms * 1000 >= exposure_time_s + assert sample_velocity_mm_per_s * exposure_time_s < parameters.x_step_size + + with resources.as_file( + resources.files(hyperion.resources.panda) / "panda-gridscan.yaml" + ) as config_yaml_path: + yield from load_device(panda, str(config_yaml_path)) # Home the PandA X encoder using current motor position yield from bps.abs_set( @@ -148,21 +163,13 @@ def setup_panda_for_flyscan( wait=True, ) - LOGGER.info(f"Setting PandA clock to period {time_between_x_steps_ms}") - - yield from bps.abs_set( - panda.clock[1].period, # type: ignore - time_between_x_steps_ms, - group="panda-config", - ) - yield from bps.abs_set( panda.pulse[1].width, DETECTOR_TRIGGER_WIDTH, group="panda-config" ) exposure_distance_mm = sample_velocity_mm_per_s * exposure_time_s - table = get_seq_table(parameters, exposure_distance_mm) + table = _get_seq_table(parameters, exposure_distance_mm, time_between_x_steps_ms) yield from bps.abs_set(panda.seq[1].table, table, group="panda-config") @@ -195,10 +202,6 @@ def disarm_panda_for_gridscan(panda, group="disarm_panda_gridscan") -> MsgGenera yield from bps.abs_set(panda.pcap.arm, PcapArm.DISARMED.value, group=group) # type: ignore yield from bps.abs_set(panda.counter[1].enable, Enabled.DISABLED.value, group=group) # type: ignore yield from bps.abs_set(panda.seq[1].enable, Enabled.DISABLED.value, group=group) - yield from bps.abs_set( - panda.clock[1].enable, Enabled.DISABLED.value, group=group - ) # While disarming the clock shouldn't be necessery, - # it will stop the eiger continuing to trigger if something in the sequencer table goes wrong yield from bps.abs_set(panda.pulse[1].enable, Enabled.DISABLED.value, group=group) yield from bps.abs_set(panda.pcap.enable, Enabled.DISABLED.value, group=group) # type: ignore yield from bps.wait(group=group, timeout=GENERAL_TIMEOUT) diff --git a/src/hyperion/experiment_plans/flyscan_xray_centre_plan.py b/src/hyperion/experiment_plans/flyscan_xray_centre_plan.py index dfa819dfc..b44d89344 100755 --- a/src/hyperion/experiment_plans/flyscan_xray_centre_plan.py +++ b/src/hyperion/experiment_plans/flyscan_xray_centre_plan.py @@ -77,10 +77,6 @@ ) from hyperion.utils.context import device_composite_from_context -PANDA_SETUP_PATH = ( - "/dls_sw/i03/software/daq_configuration/panda_configs/flyscan_pcap_ignore_seq.yaml" -) - class SmargonSpeedException(Exception): pass @@ -529,7 +525,6 @@ def _panda_triggering_setup( yield from setup_panda_for_flyscan( fgs_composite.panda, - PANDA_SETUP_PATH, parameters.panda_FGS_params, initial_xyz[0], parameters.exposure_time_s, diff --git a/tests/test_data/flyscan_pcap_ignore_seq.yaml b/src/hyperion/resources/panda/panda-gridscan.yaml old mode 100755 new mode 100644 similarity index 93% rename from tests/test_data/flyscan_pcap_ignore_seq.yaml rename to src/hyperion/resources/panda/panda-gridscan.yaml index c78a8e83b..2166865cf --- a/tests/test_data/flyscan_pcap_ignore_seq.yaml +++ b/src/hyperion/resources/panda/panda-gridscan.yaml @@ -60,6 +60,7 @@ calc.1.inpd: ZERO calc.1.label: Position calc calc.1.out_capture: 'No' + calc.1.out_dataset: '' calc.1.out_offset: 0.0 calc.1.out_scale: 1.0 calc.1.shift: 0.0 @@ -74,6 +75,7 @@ calc.2.inpd: ZERO calc.2.label: Position calc calc.2.out_capture: 'No' + calc.2.out_dataset: '' calc.2.out_offset: 0.0 calc.2.out_scale: 1.0 calc.2.shift: 0.0 @@ -81,10 +83,10 @@ calc.2.typeb: Value calc.2.typec: Value calc.2.typed: Value - clock.1.enable: SEQ1.OUTA + clock.1.enable: ZERO clock.1.enable_delay: 0 clock.1.label: Configurable clocks - clock.1.period: 0.0 + clock.1.period: 2.001 clock.2.enable: ZERO clock.2.enable_delay: 0 clock.2.label: Configurable clocks @@ -97,6 +99,7 @@ counter.1.max: 0 counter.1.min: 0 counter.1.out_capture: 'No' + counter.1.out_dataset: '' counter.1.out_offset: 0.0 counter.1.out_scale: 1.0 counter.1.start: 0 @@ -111,6 +114,7 @@ counter.2.max: 0 counter.2.min: 0 counter.2.out_capture: 'No' + counter.2.out_dataset: '' counter.2.out_offset: 0.0 counter.2.out_scale: 1.0 counter.2.start: 0 @@ -125,6 +129,7 @@ counter.3.max: 0 counter.3.min: 0 counter.3.out_capture: 'No' + counter.3.out_dataset: '' counter.3.out_offset: 0.0 counter.3.out_scale: 1.0 counter.3.start: 0 @@ -139,6 +144,7 @@ counter.4.max: 0 counter.4.min: 0 counter.4.out_capture: 'No' + counter.4.out_dataset: '' counter.4.out_offset: 0.0 counter.4.out_scale: 1.0 counter.4.start: 0 @@ -153,6 +159,7 @@ counter.5.max: 0 counter.5.min: 0 counter.5.out_capture: 'No' + counter.5.out_dataset: '' counter.5.out_offset: 0.0 counter.5.out_scale: 1.0 counter.5.start: 0 @@ -167,6 +174,7 @@ counter.6.max: 0 counter.6.min: 0 counter.6.out_capture: 'No' + counter.6.out_dataset: '' counter.6.out_offset: 0.0 counter.6.out_scale: 1.0 counter.6.start: 0 @@ -181,6 +189,7 @@ counter.7.max: 0 counter.7.min: 0 counter.7.out_capture: 'No' + counter.7.out_dataset: '' counter.7.out_offset: 0.0 counter.7.out_scale: 1.0 counter.7.start: 0 @@ -195,18 +204,20 @@ counter.8.max: 0 counter.8.min: 0 counter.8.out_capture: 'No' + counter.8.out_dataset: '' counter.8.out_offset: 0.0 counter.8.out_scale: 1.0 counter.8.start: 0 counter.8.step: 0.0 counter.8.trig: ZERO counter.8.trig_delay: 0 - data.capture: '0' - data.capturemode: FIRST_N - data.flushperiod: 1.0 - data.hdfdirectory: '' - data.hdffilename: '' - data.numcapture: 0 + data.capture: false + data.capture_mode: FIRST_N + data.createdirectory: 0 + data.flush_period: 1.0 + data.hdf_directory: '' + data.hdf_file_name: '' + data.num_capture: 0 div.1.divisor: 0.0 div.1.enable: ZERO div.1.enable_delay: 0 @@ -227,6 +238,7 @@ filter.1.label: Filter block modes are Difference and Divider filter.1.mode: difference filter.1.out_capture: 'No' + filter.1.out_dataset: '' filter.1.out_offset: 0.0 filter.1.out_scale: 1.0 filter.1.trig: ZERO @@ -237,6 +249,7 @@ filter.2.label: Filter block modes are Difference and Divider filter.2.mode: difference filter.2.out_capture: 'No' + filter.2.out_dataset: '' filter.2.out_offset: 0.0 filter.2.out_scale: 1.0 filter.2.trig: ZERO @@ -280,6 +293,7 @@ inenc.1.rst_on_z: '0' inenc.1.setp: 0 inenc.1.val_capture: Min Max Mean + inenc.1.val_dataset: '' inenc.1.val_offset: 0.0 inenc.1.val_scale: 5.0e-06 inenc.2.bits: 0.0 @@ -295,6 +309,7 @@ inenc.2.rst_on_z: '0' inenc.2.setp: 0 inenc.2.val_capture: 'No' + inenc.2.val_dataset: '' inenc.2.val_offset: 0.0 inenc.2.val_scale: 5.0e-06 inenc.3.bits: 0.0 @@ -310,6 +325,7 @@ inenc.3.rst_on_z: '0' inenc.3.setp: 0 inenc.3.val_capture: 'No' + inenc.3.val_dataset: '' inenc.3.val_offset: 0.0 inenc.3.val_scale: 5.0e-06 inenc.4.bits: 0.0 @@ -325,6 +341,7 @@ inenc.4.rst_on_z: '0' inenc.4.setp: 0 inenc.4.val_capture: 'No' + inenc.4.val_dataset: '' inenc.4.val_offset: 0.0 inenc.4.val_scale: 1.0 lut.1.func: A|B @@ -535,24 +552,32 @@ outenc.4.val: ZERO outenc.4.z: ZERO outenc.4.z_delay: 0 - pcap.arm: 0 + pcap.arm: false pcap.bits0_capture: 'No' + pcap.bits0_dataset: '' pcap.bits1_capture: 'No' + pcap.bits1_dataset: '' pcap.bits2_capture: 'No' + pcap.bits2_dataset: '' pcap.bits3_capture: 'No' + pcap.bits3_dataset: '' pcap.enable: ZERO pcap.enable_delay: 0 - pcap.gate: CLOCK1.OUT + pcap.gate: PULSE1.OUT pcap.gate_delay: 0 pcap.label: Position capture control pcap.samples_capture: 'No' + pcap.samples_dataset: '' pcap.shift_sum: 0.0 - pcap.trig: CLOCK1.OUT + pcap.trig: PULSE1.OUT pcap.trig_delay: 0 pcap.trig_edge: Rising pcap.ts_end_capture: 'No' + pcap.ts_end_dataset: '' pcap.ts_start_capture: 'No' + pcap.ts_start_dataset: '' pcap.ts_trig_capture: Value + pcap.ts_trig_dataset: '' pcomp.1.dir: Positive pcomp.1.enable: ZERO pcomp.1.enable_delay: 0 @@ -579,6 +604,7 @@ pgen.1.enable_delay: 0 pgen.1.label: Position generator pgen.1.out_capture: 'No' + pgen.1.out_dataset: '' pgen.1.out_offset: 0.0 pgen.1.out_scale: 1.0 pgen.1.repeats: 0.0 @@ -590,6 +616,7 @@ pgen.2.enable_delay: 0 pgen.2.label: Position generator pgen.2.out_capture: 'No' + pgen.2.out_dataset: '' pgen.2.out_offset: 0.0 pgen.2.out_scale: 1.0 pgen.2.repeats: 0.0 @@ -603,7 +630,7 @@ pulse.1.label: Begin FGS on trig pulse.1.pulses: 1.0 pulse.1.step: 0.0 - pulse.1.trig: CLOCK1.OUT + pulse.1.trig: SEQ1.OUTA pulse.1.trig_delay: 0 pulse.1.trig_edge: Rising pulse.1.width: 0.0001 @@ -651,7 +678,30 @@ seq.1.posc: ZERO seq.1.prescale: 0.0 seq.1.repeats: 0.0 - seq.1.table: null + seq.1.table: + outa1: [0, 1, 0, 0, 1, 0] + outa2: [0, 1, 0, 0, 1, 0] + outb1: [0, 0, 0, 0, 0, 0] + outb2: [0, 0, 0, 0, 0, 0] + outc1: [0, 0, 0, 0, 0, 0] + outc2: [0, 0, 0, 0, 0, 0] + outd1: [0, 0, 0, 0, 0, 0] + outd2: [0, 0, 0, 0, 0, 0] + oute1: [0, 0, 0, 0, 0, 0] + oute2: [0, 0, 0, 0, 0, 0] + outf1: [0, 0, 0, 0, 0, 0] + outf2: [0, 0, 0, 0, 0, 0] + position: [0, 68581, 186581, 0, 188579, 70579] + repeats: [1, 1, 1, 1, 1, 1] + time1: [0, 0, 0, 0, 0, 0] + time2: [1, 1, 1, 1, 1, 1] + trigger: + - BITA=1 + - POSA>=POSITION + - POSA>=POSITION + - BITA=1 + - POSA<=POSITION + - POSA<=POSITION seq.2.bita: ZERO seq.2.bita_delay: 0 seq.2.bitb: ZERO @@ -783,15 +833,19 @@ - POSA<=POSITION sfp3_sync_in.label: sfp panda synchronizer sfp3_sync_in.pos1_capture: 'No' + sfp3_sync_in.pos1_dataset: '' sfp3_sync_in.pos1_offset: 0.0 sfp3_sync_in.pos1_scale: 1.0 sfp3_sync_in.pos2_capture: 'No' + sfp3_sync_in.pos2_dataset: '' sfp3_sync_in.pos2_offset: 0.0 sfp3_sync_in.pos2_scale: 1.0 sfp3_sync_in.pos3_capture: 'No' + sfp3_sync_in.pos3_dataset: '' sfp3_sync_in.pos3_offset: 0.0 sfp3_sync_in.pos3_scale: 1.0 sfp3_sync_in.pos4_capture: 'No' + sfp3_sync_in.pos4_dataset: '' sfp3_sync_in.pos4_offset: 0.0 sfp3_sync_in.pos4_scale: 1.0 sfp3_sync_out.bit1: ZERO @@ -888,6 +942,9 @@ ttlout.1.label: TTL output ttlout.1.val: PULSE1.OUT ttlout.1.val_delay: 0 + ttlout.10.label: TTL output + ttlout.10.val: ZERO + ttlout.10.val_delay: 0 ttlout.2.label: TTL output ttlout.2.val: ZERO ttlout.2.val_delay: 0 @@ -912,6 +969,3 @@ ttlout.9.label: TTL output ttlout.9.val: ZERO ttlout.9.val_delay: 0 - ttlout1.0.label: TTL output - ttlout1.0.val: ZERO - ttlout1.0.val_delay: 0 diff --git a/tests/conftest.py b/tests/conftest.py index c4d91a177..27099cbe3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -604,7 +604,7 @@ def zocalo(done_status): @pytest.fixture -async def panda(): +async def panda(RE: RunEngine): class MockBlock(Device): def __init__( self, prefix: str, name: str = "", attributes: dict[str, Any] = {} diff --git a/tests/unit_tests/device_setup_plans/test_setup_panda.py b/tests/unit_tests/device_setup_plans/test_setup_panda.py index 1a5fa0dc1..9d65efe24 100644 --- a/tests/unit_tests/device_setup_plans/test_setup_panda.py +++ b/tests/unit_tests/device_setup_plans/test_setup_panda.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import NamedTuple from unittest.mock import MagicMock, patch import numpy as np @@ -12,7 +13,6 @@ from hyperion.device_setup_plans.setup_panda import ( MM_TO_ENCODER_COUNTS, disarm_panda_for_gridscan, - get_seq_table, set_and_create_panda_directory, setup_panda_for_flyscan, ) @@ -45,11 +45,10 @@ def count_commands(msg): sim.simulate_plan( setup_panda_for_flyscan( mock_panda, - "path", PandAGridScanParams(transmission_fraction=0.01), 1, - 1, - 1, + 0.1, + 100.1, smargon_speed, ) ) @@ -65,16 +64,27 @@ def test_setup_panda_performs_correct_plans(mock_load_device, sim_run_engine): "setup", sim_run_engine, mock_load_device ) mock_load_device.assert_called_once() - assert num_of_sets == 9 + assert num_of_sets == 8 assert num_of_waits == 3 +class SeqRow(NamedTuple): + repeats: int + trigger: SeqTrigger + position: int + time1: int + outa1: int + time2: int + outa2: int + + @pytest.mark.parametrize( "x_steps, x_step_size, x_start, run_up_distance_mm, time_between_x_steps_ms, exposure_time_s", [ - (10, 0.5, -1, 0.05, 10, 0.02), - (0, 5, 0, 1, 1, 0.02), - (1, 2, 1.2, 1, 10, 0.1), + (10, 0.2, 0, 0.5, 10.001, 0.01), + (10, 0.5, -1, 0.05, 10.001, 0.01), + (1, 2, 1.2, 1, 100.001, 0.1), + (10, 2, -0.5, 3, 101, 0.1), ], ) def test_setup_panda_correctly_configures_table( @@ -84,25 +94,9 @@ def test_setup_panda_correctly_configures_table( run_up_distance_mm: float, time_between_x_steps_ms: float, exposure_time_s: float, + sim_run_engine: RunEngineSimulator, + panda, ): - """The table should satisfy the following requirements: - -All the numpy arrays within the Seqtable should have a length of 6 - - -The position array should correspond to the following logic: - 1.Wait for physical trigger - 2.Wait for POSA > x_start - 3.Wait for end of row - 4.Wait for physical trigger (end of direction) - 5.Wait for POSA to go below the end of the row - 6.Wait for POSA to go below X_start - - -Time1 should be a 0 array, since we don't use the first phase in any of our panda logic - -Time2 should be a length 6 array all set to 1, so that each of the 6 steps run as quickly as possible - - -We want to send triggers between step 2 and 3, and between step 4 and 5, so we want the outa2 array - to look like [0,1,0,0,1,0] - """ - sample_velocity_mm_per_s = get_smargon_speed(x_step_size, time_between_x_steps_ms) params = PandAGridScanParams( x_steps=x_steps, @@ -112,52 +106,73 @@ def test_setup_panda_correctly_configures_table( transmission_fraction=0.01, ) - exposure_distance_mm = int(sample_velocity_mm_per_s * exposure_time_s) - - table = get_seq_table(params, exposure_distance_mm) - - np.testing.assert_array_equal(table["time2"], np.ones(6)) + exposure_distance_mm = sample_velocity_mm_per_s * exposure_time_s - safe_distance = int((params.x_step_size * MM_TO_ENCODER_COUNTS) / 2) - - exposure_distance_counts = exposure_distance_mm * MM_TO_ENCODER_COUNTS + msgs = sim_run_engine.simulate_plan( + setup_panda_for_flyscan( + panda, + params, + 0, + exposure_time_s, + time_between_x_steps_ms, + sample_velocity_mm_per_s, + ) + ) - np.testing.assert_array_equal( - table["position"], - np.array( - [ - 0, - params.x_start * MM_TO_ENCODER_COUNTS, - (params.x_start + (params.x_steps - 1) * params.x_step_size) - * MM_TO_ENCODER_COUNTS - + safe_distance, - 0, - (params.x_start + (params.x_steps - 1) * params.x_step_size) - * MM_TO_ENCODER_COUNTS - + exposure_distance_counts, - params.x_start * MM_TO_ENCODER_COUNTS - - safe_distance - + exposure_distance_counts, - ], - dtype=np.int32, + # ignore all loading operations related to loading saved panda state from yaml + msgs = [ + msg for msg in msgs if not msg.kwargs.get("group", "").startswith("load-phase") + ] + + table_msg = [ + msg + for msg in msgs + if msg.command == "set" and msg.obj.name == "panda-seq-1-table" + ][0] + + table = table_msg.args[0] + + PULSE_WIDTH_US = 1 + SPACE_WIDTH_US = int(time_between_x_steps_ms * 1000 - PULSE_WIDTH_US) + expected_seq_rows: list[SeqRow] = [ + SeqRow(1, SeqTrigger.BITA_1, 0, 0, 0, 1, 0), + SeqRow( + x_steps, + SeqTrigger.POSA_GT, + int(params.x_start * MM_TO_ENCODER_COUNTS), + PULSE_WIDTH_US, + 1, + SPACE_WIDTH_US, + 0, ), - ) + ] - np.testing.assert_array_equal( - table["trigger"], - np.array( - [ - SeqTrigger.BITA_1, - SeqTrigger.POSA_GT, - SeqTrigger.POSA_GT, - SeqTrigger.BITA_1, - SeqTrigger.POSA_LT, + exposure_distance_counts = exposure_distance_mm * MM_TO_ENCODER_COUNTS + expected_seq_rows.extend( + [ + SeqRow(1, SeqTrigger.BITA_1, 0, 0, 0, 1, 0), + SeqRow( + x_steps, SeqTrigger.POSA_LT, - ] - ), + int( + (params.x_start + (params.x_steps - 1) * params.x_step_size) + * MM_TO_ENCODER_COUNTS + + exposure_distance_counts + ), + PULSE_WIDTH_US, + 1, + SPACE_WIDTH_US, + 0, + ), + ] ) - np.testing.assert_array_equal(table["outa2"], np.array([0, 1, 0, 0, 1, 0])) + for key in SeqRow._fields: + np.testing.assert_array_equal( + table.get(key), + [getattr(row, key) for row in expected_seq_rows], + f"Sequence table for field {key} does not match", + ) def test_wait_between_setting_table_and_arming_panda(RE: RunEngine): @@ -186,11 +201,10 @@ def assert_set_table_has_been_waited_on(*args, **kwargs): RE( setup_panda_for_flyscan( MagicMock(), - "path", PandAGridScanParams(transmission_fraction=0.01), 1, - 1, - 1, + 0.1, + 101.1, get_smargon_speed(0.1, 1), ) ) @@ -202,7 +216,7 @@ def test_disarm_panda_disables_correct_blocks(sim_run_engine): num_of_sets, num_of_waits = run_simulating_setup_panda_functions( "disarm", sim_run_engine ) - assert num_of_sets == 6 + assert num_of_sets == 5 assert num_of_waits == 1 diff --git a/tests/unit_tests/experiment_plans/test_flyscan_xray_centre_plan.py b/tests/unit_tests/experiment_plans/test_flyscan_xray_centre_plan.py index aecf97970..180a3ba23 100644 --- a/tests/unit_tests/experiment_plans/test_flyscan_xray_centre_plan.py +++ b/tests/unit_tests/experiment_plans/test_flyscan_xray_centre_plan.py @@ -126,10 +126,6 @@ def mock_ispyb(): return MagicMock() -@patch( - "hyperion.experiment_plans.flyscan_xray_centre_plan.PANDA_SETUP_PATH", - "tests/test_data/flyscan_pcap_ignore_seq.yaml", -) @patch( "hyperion.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb", modified_store_grid_scan_mock, From 8456199953ca7a5ca64f7cdeb94a09d184d7435e Mon Sep 17 00:00:00 2001 From: rtuck99 Date: Thu, 8 Aug 2024 09:39:24 +0100 Subject: [PATCH 3/6] Unpin dodal before release (#1513) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 173e5c36e..52aef0717 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,7 +40,7 @@ install_requires = ophyd-async >= 0.3a5 bluesky >= 1.13.0a4 blueapi >= 0.4.3-rc1 - dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@3c2a002562659eea342e770e967678acd0e9ddfa + dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git [options.entry_points] console_scripts = From f378ffda13087bfd4f9471087927ac5329c0944c Mon Sep 17 00:00:00 2001 From: Dominic Oram Date: Mon, 12 Aug 2024 11:32:14 +0100 Subject: [PATCH 4/6] Hotfixes from 9.5.2 (#1515) * Hotfixes from beamline * Tidy up and fix tests * Fix linting * Add unit test for panda stage/unstage --------- Co-authored-by: Robert Tuck --- src/hyperion/__main__.py | 2 +- .../device_setup_plans/setup_panda.py | 2 + .../flyscan_xray_centre_plan.py | 1 + .../experiment_plans/oav_snapshot_plan.py | 5 +- .../resources/panda/panda-gridscan.yaml | 15 +- tests/conftest.py | 7 + .../test_flyscan_xray_centre_plan.py | 205 ++++++++++++++---- tests/unit_tests/hyperion/test_main_system.py | 6 +- 8 files changed, 179 insertions(+), 64 deletions(-) diff --git a/src/hyperion/__main__.py b/src/hyperion/__main__.py index e18dd84f2..c4a354f66 100755 --- a/src/hyperion/__main__.py +++ b/src/hyperion/__main__.py @@ -228,7 +228,7 @@ def compose_start_args(context: BlueskyContext, plan_name: str, action: Actions) parameters = experiment_internal_param_type(**json.loads(request.data)) except Exception as e: raise ValueError( - "Supplied parameters don't match the plan for this endpoint" + f"Supplied parameters don't match the plan for this endpoint {request.data}" ) from e return plan, parameters, plan_name, callback_type diff --git a/src/hyperion/device_setup_plans/setup_panda.py b/src/hyperion/device_setup_plans/setup_panda.py index 6d24fc2df..1bd344885 100644 --- a/src/hyperion/device_setup_plans/setup_panda.py +++ b/src/hyperion/device_setup_plans/setup_panda.py @@ -151,6 +151,8 @@ def setup_panda_for_flyscan( assert time_between_x_steps_ms * 1000 >= exposure_time_s assert sample_velocity_mm_per_s * exposure_time_s < parameters.x_step_size + yield from bps.stage(panda, group="panda-config") + with resources.as_file( resources.files(hyperion.resources.panda) / "panda-gridscan.yaml" ) as config_yaml_path: diff --git a/src/hyperion/experiment_plans/flyscan_xray_centre_plan.py b/src/hyperion/experiment_plans/flyscan_xray_centre_plan.py index b44d89344..34e5114b1 100755 --- a/src/hyperion/experiment_plans/flyscan_xray_centre_plan.py +++ b/src/hyperion/experiment_plans/flyscan_xray_centre_plan.py @@ -467,6 +467,7 @@ def _panda_tidy(fgs_composite: FlyScanXRayCentreComposite): yield from disarm_panda_for_gridscan(fgs_composite.panda, group) yield from _generic_tidy(fgs_composite, group, False) yield from bps.wait(group, timeout=10) + yield from bps.unstage(fgs_composite.panda) def _zebra_triggering_setup( diff --git a/src/hyperion/experiment_plans/oav_snapshot_plan.py b/src/hyperion/experiment_plans/oav_snapshot_plan.py index b6887a9c2..e92033c55 100644 --- a/src/hyperion/experiment_plans/oav_snapshot_plan.py +++ b/src/hyperion/experiment_plans/oav_snapshot_plan.py @@ -3,7 +3,7 @@ from blueapi.core import MsgGenerator from bluesky import plan_stubs as bps -from dodal.devices.aperturescatterguard import AperturePositions, ApertureScatterguard +from dodal.devices.aperturescatterguard import ApertureScatterguard from dodal.devices.backlight import Backlight, BacklightPosition from dodal.devices.oav.oav_detector import OAV from dodal.devices.oav.oav_parameters import OAVParameters @@ -39,9 +39,10 @@ def setup_oav_snapshot_plan( yield from bps.abs_set( composite.backlight, BacklightPosition.IN, group=OAV_SNAPSHOT_SETUP_GROUP ) + assert composite.aperture_scatterguard.aperture_positions is not None yield from bps.abs_set( composite.aperture_scatterguard, - AperturePositions.ROBOT_LOAD, + composite.aperture_scatterguard.aperture_positions.ROBOT_LOAD, group=OAV_SNAPSHOT_SETUP_GROUP, ) diff --git a/src/hyperion/resources/panda/panda-gridscan.yaml b/src/hyperion/resources/panda/panda-gridscan.yaml index 2166865cf..337dad0e9 100644 --- a/src/hyperion/resources/panda/panda-gridscan.yaml +++ b/src/hyperion/resources/panda/panda-gridscan.yaml @@ -42,7 +42,7 @@ pulse.4.delay_units: s pulse.4.step_units: s pulse.4.width_units: s - seq.1.prescale_units: s + seq.1.prescale_units: us seq.2.prescale_units: s sfp3_sync_in.pos1_units: '' sfp3_sync_in.pos2_units: '' @@ -211,13 +211,6 @@ counter.8.step: 0.0 counter.8.trig: ZERO counter.8.trig_delay: 0 - data.capture: false - data.capture_mode: FIRST_N - data.createdirectory: 0 - data.flush_period: 1.0 - data.hdf_directory: '' - data.hdf_file_name: '' - data.num_capture: 0 div.1.divisor: 0.0 div.1.enable: ZERO div.1.enable_delay: 0 @@ -308,7 +301,7 @@ inenc.2.protocol: Quadrature inenc.2.rst_on_z: '0' inenc.2.setp: 0 - inenc.2.val_capture: 'No' + inenc.2.val_capture: Min Max Mean inenc.2.val_dataset: '' inenc.2.val_offset: 0.0 inenc.2.val_scale: 5.0e-06 @@ -324,7 +317,7 @@ inenc.3.protocol: Quadrature inenc.3.rst_on_z: '0' inenc.3.setp: 0 - inenc.3.val_capture: 'No' + inenc.3.val_capture: Min Max Mean inenc.3.val_dataset: '' inenc.3.val_offset: 0.0 inenc.3.val_scale: 5.0e-06 @@ -676,7 +669,7 @@ seq.1.posa: INENC1.VAL seq.1.posb: ZERO seq.1.posc: ZERO - seq.1.prescale: 0.0 + seq.1.prescale: 1.0 seq.1.repeats: 0.0 seq.1.table: outa1: [0, 1, 0, 0, 1, 0] diff --git a/tests/conftest.py b/tests/conftest.py index 27099cbe3..053145e78 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ from unittest.mock import MagicMock, patch import bluesky.plan_stubs as bps +import numpy as np import pytest from bluesky.run_engine import RunEngine from bluesky.simulators import RunEngineSimulator @@ -53,6 +54,7 @@ from ophyd_async.core.async_status import AsyncStatus from ophyd_async.epics.motion.motor import Motor from ophyd_async.epics.signal import epics_signal_rw +from ophyd_async.panda._common_blocks import DatasetTable from scanspec.core import Path as ScanPath from scanspec.specs import Line @@ -648,6 +650,11 @@ async def create_mock_signals(devices_and_signals: dict[Device, dict[str, Any]]) **{panda.pulse[i]: {"enable": str} for i in panda.pulse.keys()}, } ) + + set_mock_value( + panda.data.datasets, DatasetTable(name=np.array(["name"]), hdf5_type=[]) + ) + return panda diff --git a/tests/unit_tests/experiment_plans/test_flyscan_xray_centre_plan.py b/tests/unit_tests/experiment_plans/test_flyscan_xray_centre_plan.py index 180a3ba23..8d0e9f47e 100644 --- a/tests/unit_tests/experiment_plans/test_flyscan_xray_centre_plan.py +++ b/tests/unit_tests/experiment_plans/test_flyscan_xray_centre_plan.py @@ -20,6 +20,7 @@ from dodal.devices.zocalo import ZocaloStartInfo from ophyd.status import Status from ophyd_async.core import set_mock_value +from ophyd_async.panda._table import DatasetTable from hyperion.device_setup_plans.read_hardware_for_setup import ( read_hardware_during_collection, @@ -81,6 +82,21 @@ ReWithSubs = tuple[RunEngine, Tuple[GridscanNexusFileCallback, GridscanISPyBCallback]] +@pytest.fixture +def fgs_composite_with_panda_pcap(fake_fgs_composite: FlyScanXRayCentreComposite): + capture_table = DatasetTable(name=np.array(["name"]), hdf5_type=[]) + set_mock_value(fake_fgs_composite.panda.data.datasets, capture_table) + + return fake_fgs_composite + + +@pytest.fixture +def fgs_params_use_panda(test_fgs_params: ThreeDGridScan, feature_flags: FeatureFlags): + feature_flags.use_panda_for_gridscan = True + test_fgs_params.features = feature_flags + return test_fgs_params + + @pytest.fixture(params=[True, False], ids=["panda", "zebra"]) def test_fgs_params_panda_zebra( request: pytest.FixtureRequest, @@ -126,11 +142,16 @@ def mock_ispyb(): return MagicMock() +def _custom_msg(command_name: str): + return lambda *args, **kwargs: iter([Msg(command_name)]) + + @patch( "hyperion.external_interaction.callbacks.xray_centre.ispyb_callback.StoreInIspyb", modified_store_grid_scan_mock, ) class TestFlyscanXrayCentrePlan: + td: TestData = TestData() def test_eiger2_x_16_detector_specified( @@ -295,44 +316,53 @@ def test_results_adjusted_and_passed_to_move_xyz( move_x_y_z: MagicMock, run_gridscan: MagicMock, move_aperture: MagicMock, - fake_fgs_composite: FlyScanXRayCentreComposite, + fgs_composite_with_panda_pcap: FlyScanXRayCentreComposite, test_fgs_params_panda_zebra: ThreeDGridScan, RE_with_subs: ReWithSubs, ): feature_controlled = _get_feature_controlled( - fake_fgs_composite, + fgs_composite_with_panda_pcap, test_fgs_params_panda_zebra, ) RE, _ = RE_with_subs RE.subscribe(VerbosePlanExecutionLoggingCallback()) - mock_zocalo_trigger(fake_fgs_composite.zocalo, TEST_RESULT_LARGE) + mock_zocalo_trigger(fgs_composite_with_panda_pcap.zocalo, TEST_RESULT_LARGE) RE( run_gridscan_and_move( - fake_fgs_composite, test_fgs_params_panda_zebra, feature_controlled + fgs_composite_with_panda_pcap, + test_fgs_params_panda_zebra, + feature_controlled, ) ) - mock_zocalo_trigger(fake_fgs_composite.zocalo, TEST_RESULT_MEDIUM) + mock_zocalo_trigger(fgs_composite_with_panda_pcap.zocalo, TEST_RESULT_MEDIUM) RE( run_gridscan_and_move( - fake_fgs_composite, test_fgs_params_panda_zebra, feature_controlled + fgs_composite_with_panda_pcap, + test_fgs_params_panda_zebra, + feature_controlled, ) ) - mock_zocalo_trigger(fake_fgs_composite.zocalo, TEST_RESULT_SMALL) + mock_zocalo_trigger(fgs_composite_with_panda_pcap.zocalo, TEST_RESULT_SMALL) RE( run_gridscan_and_move( - fake_fgs_composite, test_fgs_params_panda_zebra, feature_controlled + fgs_composite_with_panda_pcap, + test_fgs_params_panda_zebra, + feature_controlled, ) ) - assert fake_fgs_composite.aperture_scatterguard.aperture_positions is not None + assert ( + fgs_composite_with_panda_pcap.aperture_scatterguard.aperture_positions + is not None + ) ap_call_large = call( - fake_fgs_composite.aperture_scatterguard.aperture_positions.LARGE.location + fgs_composite_with_panda_pcap.aperture_scatterguard.aperture_positions.LARGE.location ) ap_call_medium = call( - fake_fgs_composite.aperture_scatterguard.aperture_positions.MEDIUM.location + fgs_composite_with_panda_pcap.aperture_scatterguard.aperture_positions.MEDIUM.location ) move_aperture.assert_has_calls( @@ -340,10 +370,18 @@ def test_results_adjusted_and_passed_to_move_xyz( ) mv_call_large = call( - fake_fgs_composite.sample_motors, 0.05, pytest.approx(0.15), 0.25, wait=True + fgs_composite_with_panda_pcap.sample_motors, + 0.05, + pytest.approx(0.15), + 0.25, + wait=True, ) mv_call_medium = call( - fake_fgs_composite.sample_motors, 0.05, pytest.approx(0.15), 0.25, wait=True + fgs_composite_with_panda_pcap.sample_motors, + 0.05, + pytest.approx(0.15), + 0.25, + wait=True, ) move_x_y_z.assert_has_calls( [mv_call_large, mv_call_large, mv_call_medium], any_order=True @@ -404,18 +442,20 @@ def test_individual_plans_triggered_once_and_only_once_in_composite_run( run_gridscan: MagicMock, move_aperture: MagicMock, RE_with_subs: ReWithSubs, - fake_fgs_composite: FlyScanXRayCentreComposite, + fgs_composite_with_panda_pcap: FlyScanXRayCentreComposite, test_fgs_params_panda_zebra: ThreeDGridScan, ): RE, (_, ispyb_cb) = RE_with_subs feature_controlled = _get_feature_controlled( - fake_fgs_composite, test_fgs_params_panda_zebra + fgs_composite_with_panda_pcap, test_fgs_params_panda_zebra ) def wrapped_gridscan_and_move(): run_generic_ispyb_handler_setup(ispyb_cb, test_fgs_params_panda_zebra) yield from run_gridscan_and_move( - fake_fgs_composite, test_fgs_params_panda_zebra, feature_controlled + fgs_composite_with_panda_pcap, + test_fgs_params_panda_zebra, + feature_controlled, ) RE( @@ -443,20 +483,22 @@ async def test_when_gridscan_finished_then_smargon_stub_offsets_are_set_and_dev_ aperture_set: MagicMock, RE_with_subs: ReWithSubs, test_fgs_params_panda_zebra: ThreeDGridScan, - fake_fgs_composite: FlyScanXRayCentreComposite, + fgs_composite_with_panda_pcap: FlyScanXRayCentreComposite, ): feature_controlled = _get_feature_controlled( - fake_fgs_composite, test_fgs_params_panda_zebra + fgs_composite_with_panda_pcap, test_fgs_params_panda_zebra ) RE, (nexus_cb, ispyb_cb) = RE_with_subs test_fgs_params_panda_zebra.features.set_stub_offsets = True - fake_fgs_composite.eiger.odin.fan.dev_shm_enable.sim_put(1) # type: ignore + fgs_composite_with_panda_pcap.eiger.odin.fan.dev_shm_enable.sim_put(1) # type: ignore def wrapped_gridscan_and_move(): run_generic_ispyb_handler_setup(ispyb_cb, test_fgs_params_panda_zebra) yield from run_gridscan_and_move( - fake_fgs_composite, test_fgs_params_panda_zebra, feature_controlled + fgs_composite_with_panda_pcap, + test_fgs_params_panda_zebra, + feature_controlled, ) RE( @@ -465,10 +507,10 @@ def wrapped_gridscan_and_move(): ) ) assert ( - await fake_fgs_composite.smargon.stub_offsets.center_at_current_position.proc.get_value() + await fgs_composite_with_panda_pcap.smargon.stub_offsets.center_at_current_position.proc.get_value() == 1 ) - assert fake_fgs_composite.eiger.odin.fan.dev_shm_enable.get() == 0 + assert fgs_composite_with_panda_pcap.eiger.odin.fan.dev_shm_enable.get() == 0 @patch( "dodal.devices.aperturescatterguard.ApertureScatterguard.set", @@ -487,18 +529,20 @@ def test_when_gridscan_succeeds_ispyb_comment_appended_to( aperture_set: MagicMock, RE_with_subs: ReWithSubs, test_fgs_params_panda_zebra: ThreeDGridScan, - fake_fgs_composite: FlyScanXRayCentreComposite, + fgs_composite_with_panda_pcap: FlyScanXRayCentreComposite, ): RE, (nexus_cb, ispyb_cb) = RE_with_subs feature_controlled = _get_feature_controlled( - fake_fgs_composite, + fgs_composite_with_panda_pcap, test_fgs_params_panda_zebra, ) def _wrapped_gridscan_and_move(): run_generic_ispyb_handler_setup(ispyb_cb, test_fgs_params_panda_zebra) yield from run_gridscan_and_move( - fake_fgs_composite, test_fgs_params_panda_zebra, feature_controlled + fgs_composite_with_panda_pcap, + test_fgs_params_panda_zebra, + feature_controlled, ) RE.subscribe(VerbosePlanExecutionLoggingCallback()) @@ -578,21 +622,23 @@ def test_when_gridscan_fails_ispyb_comment_appended_to( run_gridscan: MagicMock, RE_with_subs: ReWithSubs, test_fgs_params_panda_zebra: ThreeDGridScan, - fake_fgs_composite: FlyScanXRayCentreComposite, + fgs_composite_with_panda_pcap: FlyScanXRayCentreComposite, ): RE, (nexus_cb, ispyb_cb) = RE_with_subs feature_controlled = _get_feature_controlled( - fake_fgs_composite, + fgs_composite_with_panda_pcap, test_fgs_params_panda_zebra, ) def wrapped_gridscan_and_move(): run_generic_ispyb_handler_setup(ispyb_cb, test_fgs_params_panda_zebra) yield from run_gridscan_and_move( - fake_fgs_composite, test_fgs_params_panda_zebra, feature_controlled + fgs_composite_with_panda_pcap, + test_fgs_params_panda_zebra, + feature_controlled, ) - mock_zocalo_trigger(fake_fgs_composite.zocalo, []) + mock_zocalo_trigger(fgs_composite_with_panda_pcap.zocalo, []) RE( ispyb_activation_wrapper( wrapped_gridscan_and_move(), test_fgs_params_panda_zebra @@ -623,15 +669,17 @@ def test_GIVEN_no_results_from_zocalo_WHEN_communicator_wait_for_results_called_ mock_complete: MagicMock, RE_with_subs: ReWithSubs, test_fgs_params_panda_zebra: ThreeDGridScan, - fake_fgs_composite: FlyScanXRayCentreComposite, + fgs_composite_with_panda_pcap: FlyScanXRayCentreComposite, done_status: Status, ): RE, (nexus_cb, ispyb_cb) = RE_with_subs feature_controlled = _get_feature_controlled( - fake_fgs_composite, + fgs_composite_with_panda_pcap, test_fgs_params_panda_zebra, ) - fake_fgs_composite.eiger.unstage = MagicMock(return_value=done_status) + fgs_composite_with_panda_pcap.eiger.unstage = MagicMock( + return_value=done_status + ) initial_x_y_z = np.array( [ random.uniform(-0.5, 0.5), @@ -639,17 +687,25 @@ def test_GIVEN_no_results_from_zocalo_WHEN_communicator_wait_for_results_called_ random.uniform(-0.5, 0.5), ] ) - set_mock_value(fake_fgs_composite.smargon.x.user_readback, initial_x_y_z[0]) - set_mock_value(fake_fgs_composite.smargon.y.user_readback, initial_x_y_z[1]) - set_mock_value(fake_fgs_composite.smargon.z.user_readback, initial_x_y_z[2]) + set_mock_value( + fgs_composite_with_panda_pcap.smargon.x.user_readback, initial_x_y_z[0] + ) + set_mock_value( + fgs_composite_with_panda_pcap.smargon.y.user_readback, initial_x_y_z[1] + ) + set_mock_value( + fgs_composite_with_panda_pcap.smargon.z.user_readback, initial_x_y_z[2] + ) def wrapped_gridscan_and_move(): run_generic_ispyb_handler_setup(ispyb_cb, test_fgs_params_panda_zebra) yield from run_gridscan_and_move( - fake_fgs_composite, test_fgs_params_panda_zebra, feature_controlled + fgs_composite_with_panda_pcap, + test_fgs_params_panda_zebra, + feature_controlled, ) - mock_zocalo_trigger(fake_fgs_composite.zocalo, []) + mock_zocalo_trigger(fgs_composite_with_panda_pcap.zocalo, []) RE( ispyb_activation_wrapper( wrapped_gridscan_and_move(), test_fgs_params_panda_zebra @@ -668,27 +724,29 @@ async def test_given_gridscan_fails_to_centre_then_stub_offsets_not_set( move_xyz: MagicMock, run_gridscan: MagicMock, RE: RunEngine, - fake_fgs_composite: FlyScanXRayCentreComposite, + fgs_composite_with_panda_pcap: FlyScanXRayCentreComposite, test_fgs_params_panda_zebra: ThreeDGridScan, ): class MoveException(Exception): pass feature_controlled = _get_feature_controlled( - fake_fgs_composite, + fgs_composite_with_panda_pcap, test_fgs_params_panda_zebra, ) - mock_zocalo_trigger(fake_fgs_composite.zocalo, []) + mock_zocalo_trigger(fgs_composite_with_panda_pcap.zocalo, []) move_xyz.side_effect = MoveException() with pytest.raises(MoveException): RE( run_gridscan_and_move( - fake_fgs_composite, test_fgs_params_panda_zebra, feature_controlled + fgs_composite_with_panda_pcap, + test_fgs_params_panda_zebra, + feature_controlled, ) ) assert ( - await fake_fgs_composite.smargon.stub_offsets.center_at_current_position.proc.get_value() + await fgs_composite_with_panda_pcap.smargon.stub_offsets.center_at_current_position.proc.get_value() == 0 ) @@ -702,17 +760,17 @@ async def test_given_setting_stub_offsets_disabled_then_stub_offsets_not_set( self, move_xyz: MagicMock, run_gridscan: MagicMock, - fake_fgs_composite: FlyScanXRayCentreComposite, + fgs_composite_with_panda_pcap: FlyScanXRayCentreComposite, test_fgs_params_panda_zebra: ThreeDGridScan, RE_with_subs: ReWithSubs, done_status: Status, ): RE, (nexus_cb, ispyb_cb) = RE_with_subs - fake_fgs_composite.aperture_scatterguard.set = MagicMock( + fgs_composite_with_panda_pcap.aperture_scatterguard.set = MagicMock( return_value=done_status ) feature_controlled = _get_feature_controlled( - fake_fgs_composite, + fgs_composite_with_panda_pcap, test_fgs_params_panda_zebra, ) test_fgs_params_panda_zebra.features.set_stub_offsets = False @@ -720,7 +778,9 @@ async def test_given_setting_stub_offsets_disabled_then_stub_offsets_not_set( def wrapped_gridscan_and_move(): run_generic_ispyb_handler_setup(ispyb_cb, test_fgs_params_panda_zebra) yield from run_gridscan_and_move( - fake_fgs_composite, test_fgs_params_panda_zebra, feature_controlled + fgs_composite_with_panda_pcap, + test_fgs_params_panda_zebra, + feature_controlled, ) RE.subscribe(VerbosePlanExecutionLoggingCallback()) @@ -731,7 +791,7 @@ def wrapped_gridscan_and_move(): ) ) assert ( - await fake_fgs_composite.smargon.stub_offsets.center_at_current_position.proc.get_value() + await fgs_composite_with_panda_pcap.smargon.stub_offsets.center_at_current_position.proc.get_value() == 0 ) @@ -833,6 +893,57 @@ 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( + "hyperion.experiment_plans.flyscan_xray_centre_plan.set_and_create_panda_directory", + new=MagicMock(side_effect=_custom_msg("set_panda_directory")), + ) + @patch( + "hyperion.device_setup_plans.setup_panda.arm_panda_for_gridscan", + new=MagicMock(side_effect=_custom_msg("arm_panda")), + ) + @patch( + "hyperion.experiment_plans.flyscan_xray_centre_plan.disarm_panda_for_gridscan", + new=MagicMock(side_effect=_custom_msg("disarm_panda")), + ) + @patch( + "hyperion.experiment_plans.flyscan_xray_centre_plan.run_gridscan", + new=MagicMock(side_effect=_custom_msg("do_gridscan")), + ) + def test_flyscan_xray_centre_sets_directory_stages_arms_disarms_unstages_the_panda( + self, + done_status: Status, + fgs_composite_with_panda_pcap: FlyScanXRayCentreComposite, + fgs_params_use_panda: ThreeDGridScan, + sim_run_engine: RunEngineSimulator, + ): + sim_run_engine.add_handler("unstage", lambda _: done_status) + sim_run_engine.add_read_handler_for( + fgs_composite_with_panda_pcap.smargon.x.max_velocity, 10 + ) + + msgs = sim_run_engine.simulate_plan( + flyscan_xray_centre(fgs_composite_with_panda_pcap, fgs_params_use_panda) + ) + + msgs = assert_message_and_return_remaining( + msgs, lambda msg: msg.command == "set_panda_directory" + ) + msgs = assert_message_and_return_remaining( + msgs, lambda msg: msg.command == "stage" and msg.obj.name == "panda" + ) + msgs = assert_message_and_return_remaining( + msgs, lambda msg: msg.command == "arm_panda" + ) + msgs = assert_message_and_return_remaining( + msgs, lambda msg: msg.command == "do_gridscan" + ) + msgs = assert_message_and_return_remaining( + msgs, lambda msg: msg.command == "disarm_panda" + ) + msgs = assert_message_and_return_remaining( + msgs, lambda msg: msg.command == "unstage" and msg.obj.name == "panda" + ) + @patch("hyperion.experiment_plans.flyscan_xray_centre_plan.bps.wait", autospec=True) @patch( "hyperion.experiment_plans.flyscan_xray_centre_plan.bps.complete", autospec=True diff --git a/tests/unit_tests/hyperion/test_main_system.py b/tests/unit_tests/hyperion/test_main_system.py index 5baf303a0..00f23e01a 100644 --- a/tests/unit_tests/hyperion/test_main_system.py +++ b/tests/unit_tests/hyperion/test_main_system.py @@ -516,9 +516,9 @@ def test_log_on_invalid_json_params(test_env: ClientAndRunEngine): response = test_env.client.put(TEST_BAD_PARAM_ENDPOINT, data='{"bad":1}').json assert isinstance(response, dict) assert response.get("status") == Status.FAILED.value - assert ( - response.get("message") - == 'ValueError("Supplied parameters don\'t match the plan for this endpoint")' + assert (message := response.get("message")) is not None + assert message.startswith( + "ValueError('Supplied parameters don\\'t match the plan for this endpoint" ) assert response.get("exception_type") == "ValueError" From dbae8a0b516ed374b812a1df011bf0f731fe141f Mon Sep 17 00:00:00 2001 From: rtuck99 Date: Tue, 13 Aug 2024 12:01:45 +0100 Subject: [PATCH 5/6] 1517 fix panda issues discovered during testing (#1520) * (#1517) Fix surperfluous panda subdirectory being added * (#1517) Ensure unique pcap file names are created * (#1517) Make output pulse width equal to exposure time to ensure pcap capture correctly triggered * Bump dodal dependency commit hash * Unpin dodal --- .../device_setup_plans/setup_panda.py | 21 +++------- .../flyscan_xray_centre_plan.py | 7 ++-- .../device_setup_plans/test_setup_panda.py | 39 ++++++++++++------- .../test_flyscan_xray_centre_plan.py | 10 ++++- 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/hyperion/device_setup_plans/setup_panda.py b/src/hyperion/device_setup_plans/setup_panda.py index 1bd344885..1bf7aaf4b 100644 --- a/src/hyperion/device_setup_plans/setup_panda.py +++ b/src/hyperion/device_setup_plans/setup_panda.py @@ -1,4 +1,4 @@ -import os +from datetime import datetime from enum import Enum from importlib import resources from pathlib import Path @@ -21,7 +21,6 @@ MM_TO_ENCODER_COUNTS = 200000 GENERAL_TIMEOUT = 60 -DETECTOR_TRIGGER_WIDTH = 1e-4 TICKS_PER_MS = 1000 # Panda sequencer prescaler will be set to us @@ -165,9 +164,7 @@ def setup_panda_for_flyscan( wait=True, ) - yield from bps.abs_set( - panda.pulse[1].width, DETECTOR_TRIGGER_WIDTH, group="panda-config" - ) + 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 @@ -209,18 +206,12 @@ def disarm_panda_for_gridscan(panda, group="disarm_panda_gridscan") -> MsgGenera yield from bps.wait(group=group, timeout=GENERAL_TIMEOUT) -def set_and_create_panda_directory(panda_directory: Path) -> MsgGenerator: - """Updates and creates the panda subdirectory which is used by the PandA's PCAP. - See https://github.com/DiamondLightSource/hyperion/issues/1385 for a better long - term solution. - """ +def set_panda_directory(panda_directory: Path) -> MsgGenerator: + """Updates the root folder which is used by the PandA's PCAP.""" - if not os.path.isdir(panda_directory): - LOGGER.debug(f"Creating PandA PCAP subdirectory at {panda_directory}") - # Assumes we have permissions, which should be true on Hyperion for now - os.makedirs(panda_directory) + suffix = datetime.now().strftime("_%Y%m%d%H%M%S") async def set_panda_dir(): - await get_directory_provider().update(directory=panda_directory) + await get_directory_provider().update(directory=panda_directory, suffix=suffix) yield from bps.wait_for([set_panda_dir]) diff --git a/src/hyperion/experiment_plans/flyscan_xray_centre_plan.py b/src/hyperion/experiment_plans/flyscan_xray_centre_plan.py index 34e5114b1..5250049de 100755 --- a/src/hyperion/experiment_plans/flyscan_xray_centre_plan.py +++ b/src/hyperion/experiment_plans/flyscan_xray_centre_plan.py @@ -56,7 +56,7 @@ ) from hyperion.device_setup_plans.setup_panda import ( disarm_panda_for_gridscan, - set_and_create_panda_directory, + set_panda_directory, setup_panda_for_flyscan, ) from hyperion.device_setup_plans.setup_zebra import ( @@ -520,9 +520,8 @@ def _panda_triggering_setup( time_between_x_steps_ms, ) - panda_directory = Path(parameters.storage_directory, "panda") - - yield from set_and_create_panda_directory(panda_directory) + directory_provider_root = Path(parameters.storage_directory) + yield from set_panda_directory(directory_provider_root) yield from setup_panda_for_flyscan( fgs_composite.panda, diff --git a/tests/unit_tests/device_setup_plans/test_setup_panda.py b/tests/unit_tests/device_setup_plans/test_setup_panda.py index 9d65efe24..d15381661 100644 --- a/tests/unit_tests/device_setup_plans/test_setup_panda.py +++ b/tests/unit_tests/device_setup_plans/test_setup_panda.py @@ -1,4 +1,4 @@ -from pathlib import Path +from datetime import datetime from typing import NamedTuple from unittest.mock import MagicMock, patch @@ -6,14 +6,15 @@ import pytest from bluesky.plan_stubs import null from bluesky.run_engine import RunEngine -from bluesky.simulators import RunEngineSimulator +from bluesky.simulators import RunEngineSimulator, assert_message_and_return_remaining +from dodal.common.types import UpdatingDirectoryProvider from dodal.devices.fast_grid_scan import PandAGridScanParams from ophyd_async.panda import SeqTrigger from hyperion.device_setup_plans.setup_panda import ( MM_TO_ENCODER_COUNTS, disarm_panda_for_gridscan, - set_and_create_panda_directory, + set_panda_directory, setup_panda_for_flyscan, ) @@ -124,6 +125,13 @@ def test_setup_panda_correctly_configures_table( msg for msg in msgs if not msg.kwargs.get("group", "").startswith("load-phase") ] + assert_message_and_return_remaining( + msgs, + lambda msg: msg.command == "set" + and msg.obj.name == "panda-pulse-1-width" + and msg.args[0] == exposure_time_s, + ) + table_msg = [ msg for msg in msgs @@ -220,15 +228,18 @@ def test_disarm_panda_disables_correct_blocks(sim_run_engine): assert num_of_waits == 1 -def test_set_and_create_panda_directory(tmp_path, RE): - with patch( - "hyperion.device_setup_plans.setup_panda.os.path.isdir", return_value=False - ), patch("hyperion.device_setup_plans.setup_panda.os.makedirs") as mock_makedir: - RE(set_and_create_panda_directory(Path(tmp_path))) - mock_makedir.assert_called_once() +@patch("hyperion.device_setup_plans.setup_panda.get_directory_provider") +@patch("hyperion.device_setup_plans.setup_panda.datetime", spec=datetime) +def test_set_panda_directory( + mock_datetime, mock_get_directory_provider: MagicMock, tmp_path, RE +): + mock_directory_provider = MagicMock(spec=UpdatingDirectoryProvider) + mock_datetime.now = MagicMock( + return_value=datetime.fromisoformat("2024-08-11T15:59:23") + ) + mock_get_directory_provider.return_value = mock_directory_provider - with patch( - "hyperion.device_setup_plans.setup_panda.os.path.isdir", return_value=True - ), patch("hyperion.device_setup_plans.setup_panda.os.makedirs") as mock_makedir: - RE(set_and_create_panda_directory(Path(tmp_path))) - mock_makedir.assert_not_called() + RE(set_panda_directory(tmp_path)) + mock_directory_provider.update.assert_called_with( + directory=tmp_path, suffix="_20240811155923" + ) diff --git a/tests/unit_tests/experiment_plans/test_flyscan_xray_centre_plan.py b/tests/unit_tests/experiment_plans/test_flyscan_xray_centre_plan.py index 8d0e9f47e..a75a508c4 100644 --- a/tests/unit_tests/experiment_plans/test_flyscan_xray_centre_plan.py +++ b/tests/unit_tests/experiment_plans/test_flyscan_xray_centre_plan.py @@ -1,5 +1,6 @@ import random import types +from pathlib import Path from typing import Tuple from unittest.mock import DEFAULT, MagicMock, call, patch @@ -894,8 +895,8 @@ 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( - "hyperion.experiment_plans.flyscan_xray_centre_plan.set_and_create_panda_directory", - new=MagicMock(side_effect=_custom_msg("set_panda_directory")), + "hyperion.experiment_plans.flyscan_xray_centre_plan.set_panda_directory", + side_effect=_custom_msg("set_panda_directory"), ) @patch( "hyperion.device_setup_plans.setup_panda.arm_panda_for_gridscan", @@ -911,6 +912,7 @@ def test_when_grid_scan_ran_then_eiger_disarmed_before_zocalo_end( ) def test_flyscan_xray_centre_sets_directory_stages_arms_disarms_unstages_the_panda( self, + mock_set_panda_directory: MagicMock, done_status: Status, fgs_composite_with_panda_pcap: FlyScanXRayCentreComposite, fgs_params_use_panda: ThreeDGridScan, @@ -925,6 +927,10 @@ def test_flyscan_xray_centre_sets_directory_stages_arms_disarms_unstages_the_pan flyscan_xray_centre(fgs_composite_with_panda_pcap, fgs_params_use_panda) ) + mock_set_panda_directory.assert_called_with( + Path("/tmp/dls/i03/data/2024/cm31105-4/xraycentring/123456") + ) + msgs = assert_message_and_return_remaining( msgs, lambda msg: msg.command == "set_panda_directory" ) From 2384be216520368b71e7d2c8952cd962341770a3 Mon Sep 17 00:00:00 2001 From: David Perl <115003895+dperl-dls@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:54:03 +0100 Subject: [PATCH 6/6] use daq config service from pypi (#1524) --- setup.cfg | 2 +- src/hyperion/external_interaction/config_server.py | 3 --- src/hyperion/parameters/constants.py | 6 +++++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 52aef0717..4d1bd2782 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ install_requires = # These dependencies may be issued as pre-release versions and should have a pin constraint # as by default pip-install will not upgrade to a pre-release. # - daq-config-server @ git+https://github.com/DiamondLightSource/daq-config-server.git + daq-config-server >= 0.1.1 ophyd == 1.9.0 ophyd-async >= 0.3a5 bluesky >= 1.13.0a4 diff --git a/src/hyperion/external_interaction/config_server.py b/src/hyperion/external_interaction/config_server.py index 72888e98d..adcf86fb8 100644 --- a/src/hyperion/external_interaction/config_server.py +++ b/src/hyperion/external_interaction/config_server.py @@ -1,5 +1,3 @@ -from typing import TypeVar - from daq_config_server.client import ConfigServer from pydantic import BaseModel @@ -7,7 +5,6 @@ from hyperion.parameters.constants import CONST _CONFIG_SERVER: ConfigServer | None = None -T = TypeVar("T") def config_server() -> ConfigServer: diff --git a/src/hyperion/parameters/constants.py b/src/hyperion/parameters/constants.py index e3d0b9d74..f029aaea7 100644 --- a/src/hyperion/parameters/constants.py +++ b/src/hyperion/parameters/constants.py @@ -118,7 +118,11 @@ class HyperionConstants: TRIGGER = TriggerConstants() CALLBACK_0MQ_PROXY_PORTS = (5577, 5578) DESCRIPTORS = DocDescriptorNames() - CONFIG_SERVER_URL = "https://daq-config.diamond.ac.uk/api" + CONFIG_SERVER_URL = ( + "http://fake-url-not-real" + if TEST_MODE + else "https://daq-config.diamond.ac.uk/api" + ) GRAYLOG_PORT = 12232 PARAMETER_SCHEMA_DIRECTORY = "src/hyperion/parameters/schemas/" ZOCALO_ENV = "dev_artemis" if TEST_MODE else "artemis"