diff --git a/docs/panda-gridscan-layout.png b/docs/panda-gridscan-layout.png new file mode 100644 index 000000000..a3bd55c01 Binary files /dev/null and b/docs/panda-gridscan-layout.png differ diff --git a/setup.cfg b/setup.cfg index 673f584eb..47eac787b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,12 +35,12 @@ 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 blueapi >= 0.4.3-rc1 - dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@be6e51593e78613e68292fd4af630d138ffbbb01 + dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@ce3193e0d6e0400752673b8a843cd4530ce71adb [options.entry_points] console_scripts = 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 e3099b390..1bf7aaf4b 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 datetime import datetime from enum import Enum +from importlib import resources from pathlib import Path import bluesky.plan_stubs as bps @@ -15,11 +16,12 @@ 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 +34,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 +74,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 +121,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 +134,28 @@ 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 + + yield from bps.stage(panda, group="panda-config") + + 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 +164,11 @@ 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" - ) + 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 - 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,27 +201,17 @@ 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) -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 dfa819dfc..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 ( @@ -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 @@ -471,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( @@ -523,13 +520,11 @@ 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, - PANDA_SETUP_PATH, parameters.panda_FGS_params, initial_xyz[0], parameters.exposure_time_s, 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/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/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/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" 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..337dad0e9 --- a/tests/test_data/flyscan_pcap_ignore_seq.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: '' @@ -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,13 @@ 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 div.1.divisor: 0.0 div.1.enable: ZERO div.1.enable_delay: 0 @@ -227,6 +231,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 +242,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 +286,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 @@ -294,7 +301,8 @@ 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 inenc.3.bits: 0.0 @@ -309,7 +317,8 @@ 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 inenc.4.bits: 0.0 @@ -325,6 +334,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 +545,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 +597,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 +609,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 +623,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 @@ -649,9 +669,32 @@ 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: 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 +826,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 +935,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 +962,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 9be2b3179..e92f850ab 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 @@ -602,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] = {} @@ -646,6 +648,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/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/device_setup_plans/test_setup_panda.py b/tests/unit_tests/device_setup_plans/test_setup_panda.py index 1a5fa0dc1..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,19 +1,20 @@ -from pathlib import Path +from datetime import datetime +from typing import NamedTuple from unittest.mock import MagicMock, patch import numpy as np 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, - get_seq_table, - set_and_create_panda_directory, + set_panda_directory, setup_panda_for_flyscan, ) @@ -45,11 +46,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 +65,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 +95,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 +107,80 @@ def test_setup_panda_correctly_configures_table( transmission_fraction=0.01, ) - exposure_distance_mm = int(sample_velocity_mm_per_s * exposure_time_s) + exposure_distance_mm = 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)) + 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, + ) + ) - safe_distance = int((params.x_step_size * MM_TO_ENCODER_COUNTS) / 2) + # 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") + ] - exposure_distance_counts = exposure_distance_mm * MM_TO_ENCODER_COUNTS + 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, + ) - 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, + 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 +209,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,19 +224,22 @@ 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 -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 aecf97970..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 @@ -20,6 +21,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 +83,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,15 +143,16 @@ def mock_ispyb(): return MagicMock() -@patch( - "hyperion.experiment_plans.flyscan_xray_centre_plan.PANDA_SETUP_PATH", - "tests/test_data/flyscan_pcap_ignore_seq.yaml", -) +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( @@ -299,44 +317,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( @@ -344,10 +371,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 @@ -408,18 +443,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( @@ -447,20 +484,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( @@ -469,10 +508,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", @@ -491,18 +530,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()) @@ -582,21 +623,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 @@ -627,15 +670,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), @@ -643,17 +688,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 @@ -672,27 +725,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 ) @@ -706,17 +761,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 @@ -724,7 +779,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()) @@ -735,7 +792,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 ) @@ -837,6 +894,62 @@ 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_panda_directory", + 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, + mock_set_panda_directory: MagicMock, + 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) + ) + + 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" + ) + 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/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 e3011d7d5..9577cdeb0 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, 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"