diff --git a/setup.cfg b/setup.cfg index f46e1336b..6a8381e19 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ install_requires = xarray doct databroker - dodal @ git+https://github.com/DiamondLightSource/python-dodal.git + dodal @ git+https://github.com/DiamondLightSource/python-dodal.git@4b4bf94f1d8bd107ad432ae1269e07e179c5c26d [options.extras_require] dev = diff --git a/src/artemis/device_setup_plans/oav_centring_plan.py b/src/artemis/device_setup_plans/oav_centring_plan.py deleted file mode 100644 index 158661b00..000000000 --- a/src/artemis/device_setup_plans/oav_centring_plan.py +++ /dev/null @@ -1,390 +0,0 @@ -import bluesky.plan_stubs as bps -import numpy as np -from bluesky.run_engine import RunEngine -from dodal.devices.backlight import Backlight -from dodal.devices.oav.oav_calculations import ( - camera_coordinates_to_xyz, - check_i_within_bounds, - extract_pixel_centre_values_from_rotation_data, - find_midpoint, - get_rotation_increment, - keep_inside_bounds, -) -from dodal.devices.oav.oav_detector import OAV, ColorMode, EdgeOutputArrayImageType -from dodal.devices.oav.oav_errors import ( - OAVError_WaveformAllZero, - OAVError_ZoomLevelNotFound, -) -from dodal.devices.oav.oav_parameters import OAVParameters -from dodal.devices.smargon import Smargon - -from artemis.log import LOGGER, set_up_logging_handlers - -# Z and Y bounds are hardcoded into GDA (we don't want to exceed them). We should look -# at streamlining this -_Y_LOWER_BOUND = _Z_LOWER_BOUND = -1500 -_Y_UPPER_BOUND = _Z_UPPER_BOUND = 1500 - -# The smargon can rotate indefinitely, so the [high/low]_limit_travel is set as 0 to -# reflect this. Despite this, Neil would like to have omega to oscillate so we will -# hard code limits so gridscans will switch rotation directions and |omega| will stay pretty low. -_DESIRED_HIGH_LIMIT = 181 - - -def start_mxsc(oav: OAV, input_plugin, min_callback_time, filename): - """ - Sets PVs relevant to edge detection plugin. - - Args: - input_plugin: link to the camera stream - min_callback_time: the value to set the minimum callback time to - filename: filename of the python script to detect edge waveforms from camera stream. - Returns: None - """ - yield from bps.abs_set(oav.mxsc.input_plugin_pv, input_plugin) - - # Turns the area detector plugin on - yield from bps.abs_set(oav.mxsc.enable_callbacks_pv, 1) - - # Set the minimum time between updates of the plugin - yield from bps.abs_set(oav.mxsc.min_callback_time_pv, min_callback_time) - - # Stop the plugin from blocking the IOC and hogging all the CPU - yield from bps.abs_set(oav.mxsc.blocking_callbacks_pv, 0) - - # Set the python file to use for calculating the edge waveforms - current_filename = yield from bps.rd(oav.mxsc.py_filename) - if current_filename != filename: - LOGGER.info(f"Current python file is {current_filename}, setting to {filename}") - yield from bps.abs_set(oav.mxsc.py_filename, filename) - yield from bps.abs_set(oav.mxsc.read_file, 1) - - # Image annotations - yield from bps.abs_set(oav.mxsc.draw_tip, True) - yield from bps.abs_set(oav.mxsc.draw_edges, True) - - # Use the original image type for the edge output array - yield from bps.abs_set(oav.mxsc.output_array, EdgeOutputArrayImageType.ORIGINAL) - - -def pre_centring_setup_oav(oav: OAV, parameters: OAVParameters): - """Setup OAV PVs with required values.""" - - parameters.load_parameters_from_json() - - yield from bps.abs_set(oav.cam.color_mode, ColorMode.RGB1) - yield from bps.abs_set(oav.cam.acquire_period, parameters.acquire_period) - yield from bps.abs_set(oav.cam.acquire_time, parameters.exposure) - yield from bps.abs_set(oav.cam.gain, parameters.gain) - - # select which blur to apply to image - yield from bps.abs_set(oav.mxsc.preprocess_operation, parameters.preprocess) - - # sets length scale for blurring - yield from bps.abs_set(oav.mxsc.preprocess_ksize, parameters.preprocess_K_size) - - # Canny edge detect - yield from bps.abs_set( - oav.mxsc.canny_lower_threshold, - parameters.canny_edge_lower_threshold, - ) - yield from bps.abs_set( - oav.mxsc.canny_upper_threshold, - parameters.canny_edge_upper_threshold, - ) - # "Close" morphological operation - yield from bps.abs_set(oav.mxsc.close_ksize, parameters.close_ksize) - - # Sample detection - yield from bps.abs_set( - oav.mxsc.sample_detection_scan_direction, parameters.direction - ) - yield from bps.abs_set( - oav.mxsc.sample_detection_min_tip_height, - parameters.minimum_height, - ) - - # Connect MXSC output to MJPG input - yield from start_mxsc( - oav, - parameters.input_plugin + "." + parameters.mxsc_input, - parameters.min_callback_time, - parameters.filename, - ) - - yield from bps.abs_set(oav.snapshot.input_pv, parameters.input_plugin + ".CAM") - - zoom_level_str = f"{float(parameters.zoom)}x" - if zoom_level_str not in oav.zoom_controller.allowed_zoom_levels: - raise OAVError_ZoomLevelNotFound( - f"Found {zoom_level_str} as a zoom level but expected one of {oav.zoom_controller.allowed_zoom_levels}" - ) - - yield from bps.abs_set( - oav.zoom_controller.level, - zoom_level_str, - wait=True, - ) - yield from bps.wait() - - """ - TODO: We require setting the backlight brightness to that in the json, we can't do this currently without a PV. - """ - - -def rotate_pin_and_collect_positional_data( - oav: OAV, smargon: Smargon, rotations: int, omega_high_limit: float -): - """ - Calculate relevant spacial values (waveforms, and pixel positions) at each rotation and save them in lists. - - Args: - oav (OAV): The oav device to rotate and sample MXSC data from. - smargon (Smargon): The smargon controller device. - rotations (int): The number of rotations to sample. - omega_high_limit (float): The motor limit that shouldn't be exceeded. - Returns: - Relevant lists for each rotation, where index n corresponds to data at rotation n: - i_positions: the i positions of centres (x in camera coordinates) - j_positions: the j positions of centres (y in camera coordinates) - widths: the widths between the top and bottom waveforms at the centre point - omega_angles: the angle of the goniometer at which the measurement was taken - tip_i_positions: the measured i tip at a given rotation - tip_j_positions: the measured j tip at a given rotation - """ - smargon.wait_for_connection() - current_omega = yield from bps.rd(smargon.omega) - - # The angle to rotate by on each iteration. - increment = get_rotation_increment(rotations, current_omega, omega_high_limit) - - # Arrays to hold positions data of the pin at each rotation, - # these need to be np arrays for their use in centring. - i_positions = np.array([], dtype=np.int32) - j_positions = np.array([], dtype=np.int32) - widths = np.array([], dtype=np.int32) - omega_angles = np.array([], dtype=np.int32) - tip_i_positions = np.array([], dtype=np.int32) - tip_j_positions = np.array([], dtype=np.int32) - - for n in range(rotations): - current_omega = yield from bps.rd(smargon.omega) - top = np.array((yield from bps.rd(oav.mxsc.top))) - - bottom = np.array((yield from bps.rd(oav.mxsc.bottom))) - tip_i = yield from bps.rd(oav.mxsc.tip_x) - tip_j = yield from bps.rd(oav.mxsc.tip_y) - - for waveform in (top, bottom): - if np.all(waveform == 0): - raise OAVError_WaveformAllZero( - f"Error at rotation {current_omega}, one of the waveforms is all 0" - ) - - (i, j, width) = find_midpoint(top, bottom) - i_positions = np.append(i_positions, i) - j_positions = np.append(j_positions, j) - widths = np.append(widths, width) - omega_angles = np.append(omega_angles, current_omega) - tip_i_positions = np.append(tip_i_positions, tip_i) - tip_j_positions = np.append(tip_j_positions, tip_j) - - # rotate the pin to take next measurement, unless it's the last measurement - if n < rotations - 1: - yield from bps.mv( - smargon.omega, - current_omega + increment, - ) - - return ( - i_positions, - j_positions, - widths, - omega_angles, - tip_i_positions, - tip_j_positions, - ) - - -def get_waveforms_to_image_scale(oav: OAV): - """ - Returns the scale of the image. The standard calculation for the image is based - on a size of (1024, 768) so we require these scaling factors. - Args: - oav (OAV): The OAV device in use. - Returns: - The (i_dimensions,j_dimensions) where n_dimensions is the scale of the camera image to the - waveform values on the n axis. - """ - image_size_i = yield from bps.rd(oav.cam.array_size.array_size_x) - image_size_j = yield from bps.rd(oav.cam.array_size.array_size_y) - waveform_size_i = yield from bps.rd(oav.mxsc.waveform_size_x) - waveform_size_j = yield from bps.rd(oav.mxsc.waveform_size_y) - return image_size_i / waveform_size_i, image_size_j / waveform_size_j - - -def centring_plan( - oav: OAV, - parameters: OAVParameters, - smargon: Smargon, - max_run_num=3, - rotation_points=6, -): - """ - Attempts to find the centre of the pin on the oav by rotating and sampling elements. - I03 gets the number of rotation points from gda.mx.loop.centring.omega.steps which defaults to 6. - - Args: - oav (OAV): The OAV device in use. - parameters (OAVParamaters): Object containing values loaded in from various parameter files in use. - max_run_num (int): Maximum number of times to run. - rotation_points (int): Test to see if the pin is widest `rotation_points` number of times on a full 180 degree rotation. - """ - - LOGGER.info("OAV Centring: Starting loop centring") - yield from bps.wait() - - # Set relevant PVs to whatever the config dictates. - yield from pre_centring_setup_oav(oav, parameters) - - LOGGER.info("OAV Centring: Camera set up") - - # If omega can rotate indefinitely (indicated by high_limit_travel=0), we set the hard coded limit. - omega_high_limit = yield from bps.rd(smargon.omega.high_limit_travel) - if not omega_high_limit: - omega_high_limit = _DESIRED_HIGH_LIMIT - - # The image resolution may not correspond to the (1024, 768) of the waveform, then we have to scale - # waveform pixels to get the camera pixels. - i_scale, j_scale = yield from get_waveforms_to_image_scale(oav) - - # array for holding the current xyz position of the motor. - motor_xyz = np.array( - [ - (yield from bps.rd(smargon.x)), - (yield from bps.rd(smargon.y)), - (yield from bps.rd(smargon.z)), - ], - dtype=np.float64, - ) - - LOGGER.info(f"OAV Centring: Starting xyz, {motor_xyz}") - - # We attempt to find the centre `max_run_num` times... - for run_num in range(max_run_num): - # Spin the goniometer and capture data from the camera at each rotation_point. - ( - i_positions, - j_positions, - widths, - omega_angles, - tip_i_positions, - tip_j_positions, - ) = yield from rotate_pin_and_collect_positional_data( - oav, smargon, rotation_points, omega_high_limit - ) - - LOGGER.info( - f"Run {run_num}, mid_points: {(i_positions, j_positions)}, widths {widths}, angles {omega_angles}, tips {(tip_i_positions, tip_j_positions)}" - ) - - # Filters the data captured at rotation and formats it in terms of i, j, k and angles. - # (i_pixels,j_pixels) correspond to the (x,y) midpoint at the widest rotation, in the camera coordinate system, - # k_pixels correspond to the distance between the midpoint and the tip of the camera at the angle orthogonal to the - # widest rotation. - ( - i_pixels, - j_pixels, - k_pixels, - best_omega_angle, - best_omega_angle_orthogonal, - ) = extract_pixel_centre_values_from_rotation_data( - i_positions, j_positions, widths, omega_angles - ) - - LOGGER.info(f"Run {run_num} centre in pixels {(i_pixels, j_pixels, k_pixels)}") - LOGGER.info( - f"Run {run_num} best angles {(best_omega_angle, best_omega_angle_orthogonal)}" - ) - - # Adjust waveform values to match the camera pixels. - - i_pixels *= i_scale - j_pixels *= j_scale - k_pixels *= j_scale - LOGGER.info( - f"Run {run_num} centre in pixels after scaling {(i_pixels, j_pixels, k_pixels)}" - ) - - # Adjust i_pixels if it is too far away from the pin. - tip_i = np.median(tip_i_positions) - - i_pixels = check_i_within_bounds( - parameters.max_tip_distance_pixels, tip_i, i_pixels - ) - # Get the beam distance from the centre (in pixels). - ( - beam_distance_i_pixels, - beam_distance_j_pixels, - ) = parameters.calculate_beam_distance(i_pixels, j_pixels) - - # Add the beam distance to the current motor position (adjusting for the changes in coordinate system - # and the from the angle). - motor_xyz += camera_coordinates_to_xyz( - beam_distance_i_pixels, - beam_distance_j_pixels, - best_omega_angle, - parameters.micronsPerXPixel, - parameters.micronsPerYPixel, - ) - - LOGGER.info(f"Run {run_num} move for x, y {motor_xyz}") - - if run_num == max_run_num - 1: - # If it's the last run we adjust the z value of the motors. - beam_distance_k_pixels = parameters.calculate_beam_distance( - i_pixels, k_pixels - )[1] - - motor_xyz += camera_coordinates_to_xyz( - 0, - beam_distance_k_pixels, - best_omega_angle_orthogonal, - parameters.micronsPerXPixel, - parameters.micronsPerYPixel, - ) - - # If the x value exceeds the stub offsets, reset it to the stub offsets - motor_xyz[1] = keep_inside_bounds(motor_xyz[1], _Y_LOWER_BOUND, _Y_UPPER_BOUND) - motor_xyz[2] = keep_inside_bounds(motor_xyz[2], _Z_LOWER_BOUND, _Z_UPPER_BOUND) - - run_num += 1 - LOGGER.info(f"Run {run_num} move for x, y, z: {motor_xyz}") - - yield from bps.mv( - smargon.x, motor_xyz[0], smargon.y, motor_xyz[1], smargon.z, motor_xyz[2] - ) - - # We've moved to the best x,y,z already. Now rotate to the widest pin angle. - yield from bps.mv(smargon.omega, best_omega_angle) - - yield from bps.sleep(1) - LOGGER.info("Finished loop centring") - - -if __name__ == "__main__": - beamline = "BL03I" - set_up_logging_handlers("INFO") - oav = OAV(name="oav", prefix=beamline) - - smargon: Smargon = Smargon(name="smargon", prefix=beamline) - backlight: Backlight = Backlight(name="backlight", prefix=beamline) - parameters = OAVParameters( - "src/artemis/unit_tests/test_OAVCentring.json", - "src/artemis/unit_tests/test_jCameraManZoomLevels.xml", - "src/artemis/unit_tests/test_display.configuration", - ) - oav.wait_for_connection() - smargon.wait_for_connection() - RE = RunEngine() - RE(centring_plan(oav, parameters, smargon, backlight)) diff --git a/src/artemis/device_setup_plans/setup_oav.py b/src/artemis/device_setup_plans/setup_oav.py new file mode 100644 index 000000000..5b76b5295 --- /dev/null +++ b/src/artemis/device_setup_plans/setup_oav.py @@ -0,0 +1,107 @@ +import bluesky.plan_stubs as bps +from dodal.devices.oav.oav_detector import OAV +from dodal.devices.oav.oav_errors import OAVError_ZoomLevelNotFound +from dodal.devices.oav.oav_parameters import OAVParameters +from dodal.devices.oav.utils import ColorMode, EdgeOutputArrayImageType + +from artemis.log import LOGGER + + +def start_mxsc(oav: OAV, input_plugin, min_callback_time, filename): + """ + Sets PVs relevant to edge detection plugin. + + Args: + input_plugin: link to the camera stream + min_callback_time: the value to set the minimum callback time to + filename: filename of the python script to detect edge waveforms from camera stream. + Returns: None + """ + yield from bps.abs_set(oav.mxsc.input_plugin, input_plugin) + + # Turns the area detector plugin on + yield from bps.abs_set(oav.mxsc.enable_callbacks, 1) + + # Set the minimum time between updates of the plugin + yield from bps.abs_set(oav.mxsc.min_callback_time, min_callback_time) + + # Stop the plugin from blocking the IOC and hogging all the CPU + yield from bps.abs_set(oav.mxsc.blocking_callbacks, 0) + + # Set the python file to use for calculating the edge waveforms + current_filename = yield from bps.rd(oav.mxsc.filename) + if current_filename != filename: + LOGGER.info( + f"Current OAV MXSC plugin python file is {current_filename}, setting to {filename}" + ) + yield from bps.abs_set(oav.mxsc.filename, filename) + yield from bps.abs_set(oav.mxsc.read_file, 1) + + # Image annotations + yield from bps.abs_set(oav.mxsc.draw_tip, True) + yield from bps.abs_set(oav.mxsc.draw_edges, True) + + # Use the original image type for the edge output array + yield from bps.abs_set(oav.mxsc.output_array, EdgeOutputArrayImageType.ORIGINAL) + + +def pre_centring_setup_oav(oav: OAV, parameters: OAVParameters): + """Setup OAV PVs with required values.""" + yield from bps.abs_set(oav.cam.color_mode, ColorMode.RGB1) + yield from bps.abs_set(oav.cam.acquire_period, parameters.acquire_period) + yield from bps.abs_set(oav.cam.acquire_time, parameters.exposure) + yield from bps.abs_set(oav.cam.gain, parameters.gain) + + # select which blur to apply to image + yield from bps.abs_set(oav.mxsc.preprocess_operation, parameters.preprocess) + + # sets length scale for blurring + yield from bps.abs_set(oav.mxsc.preprocess_ksize, parameters.preprocess_K_size) + + # Canny edge detect + yield from bps.abs_set( + oav.mxsc.canny_lower_threshold, + parameters.canny_edge_lower_threshold, + ) + yield from bps.abs_set( + oav.mxsc.canny_upper_threshold, + parameters.canny_edge_upper_threshold, + ) + # "Close" morphological operation + yield from bps.abs_set(oav.mxsc.close_ksize, parameters.close_ksize) + + # Sample detection + yield from bps.abs_set( + oav.mxsc.sample_detection_scan_direction, parameters.direction + ) + yield from bps.abs_set( + oav.mxsc.sample_detection_min_tip_height, + parameters.minimum_height, + ) + + # Connect MXSC output to MJPG input + yield from start_mxsc( + oav, + parameters.input_plugin + "." + parameters.mxsc_input, + parameters.min_callback_time, + parameters.detection_script_filename, + ) + + yield from bps.abs_set(oav.snapshot.input_plugin, parameters.input_plugin + ".CAM") + + zoom_level_str = f"{float(parameters.zoom)}x" + if zoom_level_str not in oav.zoom_controller.allowed_zoom_levels: + raise OAVError_ZoomLevelNotFound( + f"Found {zoom_level_str} as a zoom level but expected one of {oav.zoom_controller.allowed_zoom_levels}" + ) + + yield from bps.abs_set( + oav.zoom_controller.level, + zoom_level_str, + wait=True, + ) + yield from bps.wait() + + """ + TODO: We require setting the backlight brightness to that in the json, we can't do this currently without a PV. + """ diff --git a/src/artemis/experiment_plans/experiment_registry.py b/src/artemis/experiment_plans/experiment_registry.py index dd9e574ce..e9602c807 100644 --- a/src/artemis/experiment_plans/experiment_registry.py +++ b/src/artemis/experiment_plans/experiment_registry.py @@ -4,7 +4,7 @@ from dodal.devices.fast_grid_scan import GridScanParams -from artemis.experiment_plans import fast_grid_scan_plan +from artemis.experiment_plans import fast_grid_scan_plan, full_grid_scan from artemis.parameters.internal_parameters.plan_specific.fgs_internal_params import ( FGSInternalParameters, ) @@ -30,6 +30,12 @@ def do_nothing(): "internal_param_type": FGSInternalParameters, "experiment_param_type": GridScanParams, }, + "full_grid_scan": { + "setup": full_grid_scan.create_devices, + "run": full_grid_scan.get_plan, + "internal_param_type": FGSInternalParameters, + "experiment_param_type": GridScanParams, + }, "rotation_scan": { "setup": do_nothing, "run": not_implemented, diff --git a/src/artemis/experiment_plans/fast_grid_scan_plan.py b/src/artemis/experiment_plans/fast_grid_scan_plan.py index 4829dbcf1..ff2da0ad2 100755 --- a/src/artemis/experiment_plans/fast_grid_scan_plan.py +++ b/src/artemis/experiment_plans/fast_grid_scan_plan.py @@ -40,7 +40,7 @@ SIM_BEAMLINE, ) from artemis.tracing import TRACER -from artemis.utils import Point3D +from artemis.utils.utils import Point3D if TYPE_CHECKING: from artemis.external_interaction.callbacks.fgs.fgs_callback_collection import ( diff --git a/src/artemis/experiment_plans/full_grid_scan.py b/src/artemis/experiment_plans/full_grid_scan.py new file mode 100644 index 000000000..dd6f69241 --- /dev/null +++ b/src/artemis/experiment_plans/full_grid_scan.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import os +from typing import TYPE_CHECKING, Callable + +from bluesky import plan_stubs as bps +from dodal import i03 +from dodal.devices.aperturescatterguard import ApertureScatterguard +from dodal.devices.backlight import Backlight +from dodal.devices.detector_motion import DetectorMotion +from dodal.devices.oav.oav_parameters import OAV_CONFIG_FILE_DEFAULTS, OAVParameters + +from artemis.experiment_plans.fast_grid_scan_plan import ( + create_devices as fgs_create_devices, +) +from artemis.experiment_plans.fast_grid_scan_plan import get_plan as fgs_get_plan +from artemis.experiment_plans.oav_grid_detection_plan import ( + create_devices as oav_create_devices, +) +from artemis.experiment_plans.oav_grid_detection_plan import grid_detection_plan +from artemis.log import LOGGER + +if TYPE_CHECKING: + from artemis.external_interaction.callbacks.fgs.fgs_callback_collection import ( + FGSCallbackCollection, + ) + from artemis.parameters.internal_parameters.plan_specific.fgs_internal_params import ( + FGSInternalParameters, + ) + + +def create_devices(): + fgs_create_devices() + oav_create_devices() + + i03.detector_motion().wait_for_connection() + i03.backlight().wait_for_connection() + i03.aperture_scatterguard().wait_for_connection() + + +def wait_for_det_to_finish_moving(detector: DetectorMotion, timeout=2): + LOGGER.info("Waiting for detector to finish moving") + SLEEP_PER_CHECK = 0.1 + times_to_check = int(timeout / SLEEP_PER_CHECK) + for _ in range(times_to_check): + detector_state = yield from bps.rd(detector.shutter) + detector_z_dmov = yield from bps.rd(detector.z.motor_done_move) + LOGGER.info(f"Shutter state is {'open' if detector_state==1 else 'closed'}") + LOGGER.info(f"Detector z DMOV is {detector_z_dmov}") + if detector_state == 1 and detector_z_dmov == 1: + return + yield from bps.sleep(SLEEP_PER_CHECK) + raise TimeoutError("Detector not finished moving") + + +def get_plan( + parameters: FGSInternalParameters, + subscriptions: FGSCallbackCollection, + oav_param_files: dict = OAV_CONFIG_FILE_DEFAULTS, +) -> Callable: + """ + A plan which combines the collection of snapshots from the OAV and the determination + of the grid dimensions to use for the following grid scan. + """ + backlight: Backlight = i03.backlight() + aperture_scatterguard: ApertureScatterguard = i03.aperture_scatterguard() + detector_motion: DetectorMotion = i03.detector_motion() + + gda_snap_1 = parameters.artemis_params.ispyb_params.xtal_snapshots_omega_start[0] + gda_snap_2 = parameters.artemis_params.ispyb_params.xtal_snapshots_omega_end[0] + + snapshot_paths = { + "snapshot_dir": os.path.dirname(os.path.abspath(gda_snap_1)), + "snap_1_filename": os.path.basename(os.path.abspath(gda_snap_1)), + "snap_2_filename": os.path.basename(os.path.abspath(gda_snap_2)), + } + + oav_params = OAVParameters("loopCentring", **oav_param_files) + + LOGGER.info( + f"microns_per_pixel: GDA: {parameters.artemis_params.ispyb_params.microns_per_pixel_x, parameters.artemis_params.ispyb_params.microns_per_pixel_y} Artemis {oav_params.micronsPerXPixel, oav_params.micronsPerYPixel}" + ) + + def detect_grid_and_do_gridscan(): + yield from grid_detection_plan( + oav_params, parameters.experiment_params, snapshot_paths + ) + + yield from bps.abs_set(backlight.pos, Backlight.OUT) + yield from bps.abs_set( + aperture_scatterguard, aperture_scatterguard.aperture_positions.SMALL + ) + yield from wait_for_det_to_finish_moving(detector_motion) + + yield from fgs_get_plan(parameters, subscriptions) + + return detect_grid_and_do_gridscan() diff --git a/src/artemis/experiment_plans/oav_grid_detection_plan.py b/src/artemis/experiment_plans/oav_grid_detection_plan.py new file mode 100644 index 000000000..2e6e40567 --- /dev/null +++ b/src/artemis/experiment_plans/oav_grid_detection_plan.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +import math +from typing import TYPE_CHECKING + +import bluesky.plan_stubs as bps +import numpy as np +from bluesky.preprocessors import finalize_wrapper +from dodal import i03 +from dodal.devices.fast_grid_scan import GridScanParams +from dodal.devices.oav.oav_calculations import camera_coordinates_to_xyz +from dodal.devices.oav.oav_detector import OAV +from dodal.devices.smargon import Smargon + +from artemis.device_setup_plans.setup_oav import pre_centring_setup_oav +from artemis.log import LOGGER + +if TYPE_CHECKING: + from dodal.devices.oav.oav_parameters import OAVParameters + + +def create_devices(): + i03.oav().wait_for_connection() + i03.smargon().wait_for_connection() + i03.backlight().wait_for_connection() + + +def grid_detection_plan( + parameters: OAVParameters, + out_parameters: GridScanParams, + filenames: dict[str, str], + width=600, + box_size_microns=20, +): + yield from finalize_wrapper( + grid_detection_main_plan( + parameters, out_parameters, filenames, width, box_size_microns + ), + reset_oav(parameters), + ) + + +def grid_detection_main_plan( + parameters: OAVParameters, + out_parameters: GridScanParams, + filenames: dict[str, str], + grid_width_px: int, + box_size_um: int, +): + """ + Attempts to find the centre of the pin on the oav by rotating and sampling elements. + I03 gets the number of rotation points from gda.mx.loop.centring.omega.steps which defaults to 6. + + Args: + oav (OAV): The OAV device in use. + parameters (OAVParamaters): Object containing values loaded in from various parameter files in use. + max_run_num (int): Maximum number of times to run. + rotation_points (int): Test to see if the pin is widest `rotation_points` number of times on a full 180 degree rotation. + """ + oav: OAV = i03.oav() + smargon: Smargon = i03.smargon() + LOGGER.info("OAV Centring: Starting loop centring") + + yield from bps.wait() + + # Set relevant PVs to whatever the config dictates. + yield from pre_centring_setup_oav(oav, parameters) + + LOGGER.info("OAV Centring: Camera set up") + + start_positions = [] + box_numbers = [] + + box_size_x_pixels = box_size_um / parameters.micronsPerXPixel + box_size_y_pixels = box_size_um / parameters.micronsPerYPixel + + for angle in [0, 90]: + yield from bps.mv(smargon.omega, angle) + # need to wait for the OAV image to update + # TODO improve this from just waiting some random time + yield from bps.sleep(0.5) + + top_edge = np.array((yield from bps.rd(oav.mxsc.top))) + bottom_edge = np.array((yield from bps.rd(oav.mxsc.bottom))) + + tip_x_px = yield from bps.rd(oav.mxsc.tip_x) + tip_y_px = yield from bps.rd(oav.mxsc.tip_y) + + LOGGER.info(f"Tip is at x,y: {tip_x_px},{tip_y_px}") + + full_image_height_px = yield from bps.rd(oav.cam.array_size.array_size_y) + + # only use the area from the start of the pin onwards + top_edge = top_edge[tip_x_px : tip_x_px + grid_width_px] + bottom_edge = bottom_edge[tip_x_px : tip_x_px + grid_width_px] + + # the edge detection line can jump to the edge of the image sometimes, filter + # those points out, and if empty after filter use the whole image + filtered_top = list(top_edge[top_edge != 0]) or [full_image_height_px] + filtered_bottom = list(bottom_edge[bottom_edge != full_image_height_px]) or [0] + min_y = min(filtered_top) + max_y = max(filtered_bottom) + LOGGER.info(f"Min/max {min_y, max_y}") + grid_height_px = max_y - min_y + + LOGGER.info(f"Drawing snapshot {grid_width_px} by {grid_height_px}") + + boxes = ( + math.ceil(grid_width_px / box_size_x_pixels), + math.ceil(grid_height_px / box_size_y_pixels), + ) + box_numbers.append(boxes) + + upper_left = (tip_x_px, min_y) + + yield from bps.abs_set(oav.snapshot.top_left_x, upper_left[0]) + yield from bps.abs_set(oav.snapshot.top_left_y, upper_left[1]) + yield from bps.abs_set(oav.snapshot.box_width, box_size_x_pixels) + yield from bps.abs_set(oav.snapshot.num_boxes_x, boxes[0]) + yield from bps.abs_set(oav.snapshot.num_boxes_y, boxes[1]) + + LOGGER.info("Triggering snapshot") + + snapshot_filename = ( + filenames["snap_1_filename"] if angle == 0 else filenames["snap_2_filename"] + ) + + yield from bps.abs_set(oav.snapshot.filename, snapshot_filename) + yield from bps.abs_set(oav.snapshot.directory, filenames["snapshot_dir"]) + yield from bps.trigger(oav.snapshot, wait=True) + + # Get the beam distance from the centre (in pixels). + ( + beam_distance_i_pixels, + beam_distance_j_pixels, + ) = parameters.calculate_beam_distance(upper_left[0], upper_left[1]) + + current_motor_xyz = np.array( + [ + (yield from bps.rd(smargon.x)), + (yield from bps.rd(smargon.y)), + (yield from bps.rd(smargon.z)), + ], + dtype=np.float64, + ) + + # Add the beam distance to the current motor position (adjusting for the changes in coordinate system + # and the from the angle). + start_position = current_motor_xyz + camera_coordinates_to_xyz( + beam_distance_i_pixels, + beam_distance_j_pixels, + angle, + parameters.micronsPerXPixel, + parameters.micronsPerYPixel, + ) + start_positions.append(start_position) + + LOGGER.info( + f"x_start: GDA: {out_parameters.x_start}, Artemis {start_positions[0][0]}" + ) + + LOGGER.info( + f"y1_start: GDA: {out_parameters.y1_start}, Artemis {start_positions[0][1]}" + ) + + LOGGER.info( + f"z1_start: GDA: {out_parameters.z1_start}, Artemis {start_positions[1][1]}" + ) + + LOGGER.info( + f"x_step_size: GDA: {out_parameters.x_step_size}, Artemis {box_size_um}" + ) + LOGGER.info( + f"y_step_size: GDA: {out_parameters.y_step_size}, Artemis {box_size_um}" + ) + LOGGER.info( + f"z_step_size: GDA: {out_parameters.z_step_size}, Artemis {box_size_um}" + ) + + LOGGER.info(f"x_steps: GDA: {out_parameters.x_steps}, Artemis {box_numbers[0][0]}") + LOGGER.info(f"y_steps: GDA: {out_parameters.y_steps}, Artemis {box_numbers[0][1]}") + LOGGER.info(f"z_steps: GDA: {out_parameters.z_steps}, Artemis {box_numbers[1][1]}") + + +def reset_oav(parameters: OAVParameters): + oav = i03.oav() + yield from bps.abs_set(oav.snapshot.input_plugin, parameters.input_plugin + ".CAM") + yield from bps.abs_set(oav.mxsc.enable_callbacks, 0) diff --git a/src/artemis/experiment_plans/tests/conftest.py b/src/artemis/experiment_plans/tests/conftest.py new file mode 100644 index 000000000..8836bfa56 --- /dev/null +++ b/src/artemis/experiment_plans/tests/conftest.py @@ -0,0 +1,84 @@ +from unittest.mock import MagicMock + +import pytest +from bluesky.run_engine import RunEngine +from dodal.devices.aperturescatterguard import AperturePositions + +from artemis.experiment_plans.fast_grid_scan_plan import FGSComposite +from artemis.external_interaction.callbacks.fgs.fgs_callback_collection import ( + FGSCallbackCollection, +) +from artemis.external_interaction.system_tests.conftest import TEST_RESULT_LARGE +from artemis.parameters.external_parameters import from_file as default_raw_params +from artemis.parameters.internal_parameters.internal_parameters import ( + InternalParameters, +) +from artemis.parameters.internal_parameters.plan_specific.fgs_internal_params import ( + FGSInternalParameters, +) + + +@pytest.fixture() +def test_config_files(): + return { + "zoom_params_file": "src/artemis/experiment_plans/tests/test_data/jCameraManZoomLevels.xml", + "oav_config_json": "src/artemis/experiment_plans/tests/test_data/OAVCentring.json", + "display_config": "src/artemis/experiment_plans/tests/test_data/display.configuration", + } + + +@pytest.fixture +def RE(): + return RunEngine({}) + + +@pytest.fixture +def test_params(): + return FGSInternalParameters(default_raw_params()) + + +@pytest.fixture +def fake_fgs_composite(test_params: InternalParameters): + fake_composite = FGSComposite( + aperture_positions=AperturePositions( + LARGE=(1, 2, 3, 4, 5), + MEDIUM=(2, 3, 3, 5, 6), + SMALL=(3, 4, 3, 6, 7), + ROBOT_LOAD=(0, 0, 3, 0, 0), + ), + detector_params=test_params.artemis_params.detector_params, + fake=True, + ) + fake_composite.aperture_scatterguard.aperture.x.user_setpoint._use_limits = False + fake_composite.aperture_scatterguard.aperture.y.user_setpoint._use_limits = False + fake_composite.aperture_scatterguard.aperture.z.user_setpoint._use_limits = False + fake_composite.aperture_scatterguard.scatterguard.x.user_setpoint._use_limits = ( + False + ) + fake_composite.aperture_scatterguard.scatterguard.y.user_setpoint._use_limits = ( + False + ) + + fake_composite.fast_grid_scan.scan_invalid.sim_put(False) + fake_composite.fast_grid_scan.position_counter.sim_put(0) + + return fake_composite + + +@pytest.fixture +def mock_subscriptions(test_params): + subscriptions = FGSCallbackCollection.from_params(test_params) + subscriptions.zocalo_handler.zocalo_interactor.wait_for_result = MagicMock() + subscriptions.zocalo_handler.zocalo_interactor.run_end = MagicMock() + subscriptions.zocalo_handler.zocalo_interactor.run_start = MagicMock() + subscriptions.zocalo_handler.zocalo_interactor.wait_for_result.return_value = ( + TEST_RESULT_LARGE + ) + + subscriptions.nexus_handler.nxs_writer_1 = MagicMock() + subscriptions.nexus_handler.nxs_writer_2 = MagicMock() + + subscriptions.ispyb_handler.ispyb = MagicMock() + subscriptions.ispyb_handler.ispyb_ids = [[0, 0], 0, 0] + + return subscriptions diff --git a/src/artemis/experiment_plans/tests/test_data/OAVCentring.json b/src/artemis/experiment_plans/tests/test_data/OAVCentring.json new file mode 100644 index 000000000..cdf68b7e9 --- /dev/null +++ b/src/artemis/experiment_plans/tests/test_data/OAVCentring.json @@ -0,0 +1,75 @@ +{ + "exposure": 0.075, + "acqPeriod": 0.05, + "gain": 1.0, + "minheight": 70, + "oav": "OAV", + "mxsc_input": "CAM", + "min_callback_time": 0.080, + "close_ksize": 11, + "direction": 0, + + "pinTipCentring": { + "zoom": 1.0, + "preprocess": 8, + "preProcessKSize": 21, + "CannyEdgeUpperThreshold": 20.0, + "CannyEdgeLowerThreshold": 5.0, + "brightness": 20, + "max_tip_distance": 300, + "mxsc_input": "proc", + "minheight": 10, + "min_callback_time": 0.15, + "filename": "/dls_sw/prod/R3.14.12.7/support/adPython/2-1-11/adPythonApp/scripts/adPythonMxSampleDetect.py" + }, + + "loopCentring": { + "zoom": 5.0, + "preprocess": 8, + "preProcessKSize": 21, + "CannyEdgeUpperThreshold": 20.0, + "CannyEdgeLowerThreshold": 5.0, + "brightness": 20, + "filename": "/dls_sw/prod/R3.14.12.7/support/adPython/2-1-11/adPythonApp/scripts/adPythonMxSampleDetect.py", + "max_tip_distance": 300 + }, + + "xrayCentring": { + "zoom": 7.5, + "preprocess": 8, + "preProcessKSize": 31, + "CannyEdgeUpperThreshold": 30.0, + "CannyEdgeLowerThreshold": 5.0, + "close_ksize": 3, + "filename": "/dls_sw/prod/R3.14.12.7/support/adPython/2-1-11/adPythonApp/scripts/adPythonMxSampleDetect.py", + "brightness": 80 + }, + + "rotationAxisAlign": { + "zoom": 10.0, + "preprocess": 8, + "preProcessKSize": 21, + "CannyEdgeUpperThreshold": 20.0, + "CannyEdgeLowerThreshold": 5.0, + "filename": "/dls_sw/prod/R3.14.12.7/support/adPython/2-1-11/adPythonApp/scripts/adPythonMxSampleDetect.py", + "brightness": 100 + }, + + "SmargonOffsets1": { + "zoom": 1.0, + "preprocess": 8, + "preProcessKSize": 21, + "CannyEdgeUpperThreshold": 50.0, + "CannyEdgeLowerThreshold": 5.0, + "brightness": 80 + }, + + "SmargonOffsets2": { + "zoom": 5.0, + "preprocess": 8, + "preProcessKSize": 11, + "CannyEdgeUpperThreshold": 50.0, + "CannyEdgeLowerThreshold": 5.0, + "brightness": 90 + } +} diff --git a/src/artemis/experiment_plans/tests/test_data/display.configuration b/src/artemis/experiment_plans/tests/test_data/display.configuration new file mode 100755 index 000000000..dfb01954a --- /dev/null +++ b/src/artemis/experiment_plans/tests/test_data/display.configuration @@ -0,0 +1,42 @@ +zoomLevel = 1.0 +crosshairX = 477 +crosshairY = 359 +topLeftX = 383 +topLeftY = 253 +bottomRightX = 410 +bottomRightY = 278 +zoomLevel = 2.5 +crosshairX = 493 +crosshairY = 355 +topLeftX = 340 +topLeftY = 283 +bottomRightX = 388 +bottomRightY = 322 +zoomLevel = 5.0 +crosshairX = 517 +crosshairY = 350 +topLeftX = 268 +topLeftY = 326 +bottomRightX = 354 +bottomRightY = 387 +zoomLevel = 7.5 +crosshairX = 549 +crosshairY = 347 +topLeftX = 248 +topLeftY = 394 +bottomRightX = 377 +bottomRightY = 507 +zoomLevel = 10.0 +crosshairX = 613 +crosshairY = 344 +topLeftX = 2 +topLeftY = 489 +bottomRightX = 206 +bottomRightY = 630 +zoomLevel = 15.0 +crosshairX = 693 +crosshairY = 339 +topLeftX = 1 +topLeftY = 601 +bottomRightX = 65 +bottomRightY = 767 diff --git a/src/artemis/experiment_plans/tests/test_data/jCameraManZoomLevels.xml b/src/artemis/experiment_plans/tests/test_data/jCameraManZoomLevels.xml new file mode 100644 index 000000000..d751fd697 --- /dev/null +++ b/src/artemis/experiment_plans/tests/test_data/jCameraManZoomLevels.xml @@ -0,0 +1,42 @@ + + + + + 1.0 + 0 + 2.87 + 2.87 + + + 2.5 + 10 + 2.31 + 2.31 + + + 5.0 + 25 + 1.58 + 1.58 + + + 7.5 + 50 + 0.806 + 0.806 + + + 10.0 + 75 + 0.438 + 0.438 + + + 15.0 + 90 + 0.302 + 0.302 + + +1.0 + diff --git a/src/artemis/unit_tests/test_fast_grid_scan_plan.py b/src/artemis/experiment_plans/tests/test_fast_grid_scan_plan.py similarity index 82% rename from src/artemis/unit_tests/test_fast_grid_scan_plan.py rename to src/artemis/experiment_plans/tests/test_fast_grid_scan_plan.py index 30db2bc2b..a0966276c 100644 --- a/src/artemis/unit_tests/test_fast_grid_scan_plan.py +++ b/src/artemis/experiment_plans/tests/test_fast_grid_scan_plan.py @@ -4,7 +4,6 @@ import bluesky.plan_stubs as bps import pytest from bluesky.run_engine import RunEngine -from dodal.devices.aperturescatterguard import AperturePositions from dodal.devices.det_dim_constants import ( EIGER2_X_4M_DIMENSION, EIGER_TYPE_EIGER2_X_4M, @@ -38,66 +37,13 @@ ) from artemis.log import set_up_logging_handlers from artemis.parameters import external_parameters -from artemis.parameters.external_parameters import from_file as default_raw_params from artemis.parameters.internal_parameters.internal_parameters import ( InternalParameters, ) from artemis.parameters.internal_parameters.plan_specific.fgs_internal_params import ( FGSInternalParameters, ) -from artemis.utils import Point3D - - -@pytest.fixture -def test_params(): - return FGSInternalParameters(default_raw_params()) - - -@pytest.fixture -def fake_fgs_composite(test_params: InternalParameters): - fake_composite = FGSComposite( - aperture_positions=AperturePositions( - LARGE=(1, 2, 3, 4, 5), - MEDIUM=(2, 3, 3, 5, 6), - SMALL=(3, 4, 3, 6, 7), - ROBOT_LOAD=(0, 0, 3, 0, 0), - ), - detector_params=test_params.artemis_params.detector_params, - fake=True, - ) - fake_composite.aperture_scatterguard.aperture.x.user_setpoint._use_limits = False - fake_composite.aperture_scatterguard.aperture.y.user_setpoint._use_limits = False - fake_composite.aperture_scatterguard.aperture.z.user_setpoint._use_limits = False - fake_composite.aperture_scatterguard.scatterguard.x.user_setpoint._use_limits = ( - False - ) - fake_composite.aperture_scatterguard.scatterguard.y.user_setpoint._use_limits = ( - False - ) - - fake_composite.fast_grid_scan.scan_invalid.sim_put(False) - fake_composite.fast_grid_scan.position_counter.sim_put(0) - - return fake_composite - - -@pytest.fixture -def mock_subscriptions(test_params): - subscriptions = FGSCallbackCollection.from_params(test_params) - subscriptions.zocalo_handler.zocalo_interactor.wait_for_result = MagicMock() - subscriptions.zocalo_handler.zocalo_interactor.run_end = MagicMock() - subscriptions.zocalo_handler.zocalo_interactor.run_start = MagicMock() - subscriptions.zocalo_handler.zocalo_interactor.wait_for_result.return_value = ( - TEST_RESULT_LARGE - ) - - subscriptions.nexus_handler.nxs_writer_1 = MagicMock() - subscriptions.nexus_handler.nxs_writer_2 = MagicMock() - - subscriptions.ispyb_handler.ispyb = MagicMock() - subscriptions.ispyb_handler.ispyb_ids = [[0, 0], 0, 0] - - return subscriptions +from artemis.utils.utils import Point3D def test_given_full_parameters_dict_when_detector_name_used_and_converted_then_detector_constants_correct( @@ -124,10 +70,8 @@ def test_when_run_gridscan_called_then_generator_returned(): def test_read_hardware_for_ispyb_updates_from_ophyd_devices( - fake_fgs_composite: FGSComposite, test_params: FGSInternalParameters + fake_fgs_composite: FGSComposite, test_params: FGSInternalParameters, RE: RunEngine ): - RE = RunEngine({}) - undulator_test_value = 1.234 fake_fgs_composite.undulator.gap.user_readback.sim_put(undulator_test_value) @@ -178,8 +122,8 @@ def test_results_adjusted_and_passed_to_move_xyz( fake_fgs_composite: FGSComposite, mock_subscriptions: FGSCallbackCollection, test_params: InternalParameters, + RE: RunEngine, ): - RE = RunEngine({}) set_up_logging_handlers(logging_level="INFO", dev_mode=True) RE.subscribe(VerbosePlanExecutionLoggingCallback()) @@ -231,10 +175,10 @@ def test_results_passed_to_move_motors( bps_mv: MagicMock, test_params: InternalParameters, fake_fgs_composite: FGSComposite, + RE: RunEngine, ): from artemis.experiment_plans.fast_grid_scan_plan import move_xyz - RE = RunEngine({}) set_up_logging_handlers(logging_level="INFO", dev_mode=True) RE.subscribe(VerbosePlanExecutionLoggingCallback()) motor_position = test_params.experiment_params.grid_position_to_motor_position( @@ -262,8 +206,8 @@ def test_individual_plans_triggered_once_and_only_once_in_composite_run( mock_subscriptions: FGSCallbackCollection, fake_fgs_composite: FGSComposite, test_params: FGSInternalParameters, + RE: RunEngine, ): - RE = RunEngine({}) set_up_logging_handlers(logging_level="INFO", dev_mode=True) RE.subscribe(VerbosePlanExecutionLoggingCallback()) @@ -295,8 +239,8 @@ def test_logging_within_plan( mock_subscriptions: FGSCallbackCollection, fake_fgs_composite: FGSComposite, test_params: InternalParameters, + RE: RunEngine, ): - RE = RunEngine({}) set_up_logging_handlers(logging_level="INFO", dev_mode=True) RE.subscribe(VerbosePlanExecutionLoggingCallback()) @@ -314,15 +258,13 @@ def test_logging_within_plan( @patch("artemis.experiment_plans.fast_grid_scan_plan.bps.sleep") def test_GIVEN_scan_already_valid_THEN_wait_for_FGS_returns_immediately( - patch_sleep: MagicMock, + patch_sleep: MagicMock, RE: RunEngine ): test_fgs: FastGridScan = make_fake_device(FastGridScan)("prefix", name="fake_fgs") test_fgs.scan_invalid.sim_put(False) test_fgs.position_counter.sim_put(0) - RE = RunEngine({}) - RE(wait_for_fgs_valid(test_fgs)) patch_sleep.assert_not_called() @@ -330,14 +272,12 @@ def test_GIVEN_scan_already_valid_THEN_wait_for_FGS_returns_immediately( @patch("artemis.experiment_plans.fast_grid_scan_plan.bps.sleep") def test_GIVEN_scan_not_valid_THEN_wait_for_FGS_raises_and_sleeps_called( - patch_sleep: MagicMock, + patch_sleep: MagicMock, RE: RunEngine ): test_fgs: FastGridScan = make_fake_device(FastGridScan)("prefix", name="fake_fgs") test_fgs.scan_invalid.sim_put(True) test_fgs.position_counter.sim_put(0) - - RE = RunEngine({}) with pytest.raises(WarningException): RE(wait_for_fgs_valid(test_fgs)) @@ -358,9 +298,8 @@ def test_when_grid_scan_ran_then_eiger_disarmed_before_zocalo_end( fake_fgs_composite: FGSComposite, test_params: InternalParameters, mock_subscriptions: FGSCallbackCollection, + RE: RunEngine, ): - RE = RunEngine({}) - # Put both mocks in a parent to easily capture order mock_parent = MagicMock() diff --git a/src/artemis/experiment_plans/tests/test_full_grid_scan_plan.py b/src/artemis/experiment_plans/tests/test_full_grid_scan_plan.py new file mode 100644 index 000000000..602f263ee --- /dev/null +++ b/src/artemis/experiment_plans/tests/test_full_grid_scan_plan.py @@ -0,0 +1,44 @@ +from typing import Generator +from unittest.mock import patch + +import pytest +from dodal.i03 import detector_motion + +from artemis.experiment_plans.full_grid_scan import ( + create_devices, + get_plan, + wait_for_det_to_finish_moving, +) + + +def test_create_devices(): + with ( + patch("artemis.experiment_plans.full_grid_scan.i03") as i03, + patch( + "artemis.experiment_plans.full_grid_scan.fgs_create_devices" + ) as fgs_create_devices, + patch( + "artemis.experiment_plans.full_grid_scan.oav_create_devices" + ) as oav_create_devices, + ): + create_devices() + fgs_create_devices.assert_called() + oav_create_devices.assert_called() + i03.detector_motion.return_value.wait_for_connection.assert_called() + i03.backlight.return_value.wait_for_connection.assert_called() + + +def test_wait_for_detector(RE): + d_m = detector_motion(fake_with_ophyd_sim=True) + with pytest.raises(TimeoutError): + RE(wait_for_det_to_finish_moving(d_m, 0.2)) + d_m.shutter.sim_put(1) + d_m.z.motor_done_move.sim_put(1) + RE(wait_for_det_to_finish_moving(d_m, 0.5)) + + +def test_get_plan(test_params, mock_subscriptions, test_config_files): + with patch("artemis.experiment_plans.full_grid_scan.i03"): + plan = get_plan(test_params, mock_subscriptions, test_config_files) + + assert isinstance(plan, Generator) diff --git a/src/artemis/experiment_plans/tests/test_grid_detection_plan.py b/src/artemis/experiment_plans/tests/test_grid_detection_plan.py new file mode 100644 index 000000000..d4169ca50 --- /dev/null +++ b/src/artemis/experiment_plans/tests/test_grid_detection_plan.py @@ -0,0 +1,63 @@ +from unittest.mock import MagicMock, patch + +from dodal import i03 +from dodal.devices.fast_grid_scan import GridScanParams +from dodal.devices.oav.oav_parameters import OAVParameters + +from artemis.experiment_plans.oav_grid_detection_plan import grid_detection_plan + + +def fake_create_devices(): + oav = i03.oav(fake_with_ophyd_sim=True) + oav.wait_for_connection() + smargon = i03.smargon(fake_with_ophyd_sim=True) + smargon.wait_for_connection() + bl = i03.backlight(fake_with_ophyd_sim=True) + bl.wait_for_connection() + + oav.zoom_controller.zrst.set("1.0x") + oav.zoom_controller.onst.set("2.0x") + oav.zoom_controller.twst.set("3.0x") + oav.zoom_controller.thst.set("5.0x") + oav.zoom_controller.frst.set("7.0x") + oav.zoom_controller.fvst.set("9.0x") + + # fmt: off + oav.mxsc.bottom.set([0,0,0,0,0,0,0,0,1,1,1,1,1,2,2,2,2,3,3,3,3,33,3,4,4,4]) # noqa: E231 + oav.mxsc.top.set([7,7,7,7,7,7,6,6,6,6,6,6,2,2,2,2,3,3,3,3,33,3,4,4,4]) # noqa: E231 + # fmt: on + + smargon.x.user_setpoint._use_limits = False + smargon.y.user_setpoint._use_limits = False + smargon.z.user_setpoint._use_limits = False + smargon.omega.user_setpoint._use_limits = False + + return oav, smargon, bl + + +@patch("dodal.i03.active_device_is_same_type", lambda a, b: True) +@patch("bluesky.plan_stubs.wait") +@patch("bluesky.plan_stubs.mv") +@patch("bluesky.plan_stubs.trigger") +def test_grid_detection_plan( + bps_trigger: MagicMock, + bps_mv: MagicMock, + bps_wait: MagicMock, + RE, + test_config_files, +): + oav, smargon, bl = fake_create_devices() + params = OAVParameters(context="loopCentring", **test_config_files) + gridscan_params = GridScanParams() + RE( + grid_detection_plan( + parameters=params, + out_parameters=gridscan_params, + filenames={ + "snapshot_dir": "tmp", + "snap_1_filename": "1.jpg", + "snap_2_filename": "2.jpg", + }, + ) + ) + bps_trigger.assert_called_with(oav.snapshot, wait=True) diff --git a/src/artemis/external_interaction/callbacks/fgs/tests/test_fgs_callback_collection.py b/src/artemis/external_interaction/callbacks/fgs/tests/test_fgs_callback_collection.py index c88419edd..925db3f23 100644 --- a/src/artemis/external_interaction/callbacks/fgs/tests/test_fgs_callback_collection.py +++ b/src/artemis/external_interaction/callbacks/fgs/tests/test_fgs_callback_collection.py @@ -16,7 +16,7 @@ from artemis.parameters.internal_parameters.plan_specific.fgs_internal_params import ( FGSInternalParameters, ) -from artemis.utils import Point3D +from artemis.utils.utils import Point3D def test_callback_collection_init(): diff --git a/src/artemis/external_interaction/callbacks/fgs/tests/test_zocalo_handler.py b/src/artemis/external_interaction/callbacks/fgs/tests/test_zocalo_handler.py index 71155c51a..001d0edfc 100644 --- a/src/artemis/external_interaction/callbacks/fgs/tests/test_zocalo_handler.py +++ b/src/artemis/external_interaction/callbacks/fgs/tests/test_zocalo_handler.py @@ -9,13 +9,11 @@ from artemis.external_interaction.callbacks.fgs.tests.conftest import TestData from artemis.external_interaction.exceptions import ISPyBDepositionNotMade from artemis.external_interaction.zocalo.zocalo_interaction import NoDiffractionFound +from artemis.parameters.external_parameters import from_file as default_raw_params from artemis.parameters.internal_parameters.plan_specific.fgs_internal_params import ( FGSInternalParameters, ) -from artemis.utils import Point3D - -from artemis.parameters.external_parameters import from_file as default_raw_params - +from artemis.utils.utils import Point3D EXPECTED_DCID = 100 EXPECTED_RUN_START_MESSAGE = {"event": "start", "ispyb_dcid": EXPECTED_DCID} diff --git a/src/artemis/external_interaction/callbacks/fgs/zocalo_callback.py b/src/artemis/external_interaction/callbacks/fgs/zocalo_callback.py index 0fa70681b..4a488e9f6 100644 --- a/src/artemis/external_interaction/callbacks/fgs/zocalo_callback.py +++ b/src/artemis/external_interaction/callbacks/fgs/zocalo_callback.py @@ -18,7 +18,7 @@ from artemis.parameters.internal_parameters.plan_specific.fgs_internal_params import ( FGSInternalParameters, ) -from artemis.utils import Point3D +from artemis.utils.utils import Point3D class FGSZocaloCallback(CallbackBase): diff --git a/src/artemis/external_interaction/ispyb/ispyb_dataclass.py b/src/artemis/external_interaction/ispyb/ispyb_dataclass.py index 61848f2db..f4305de07 100644 --- a/src/artemis/external_interaction/ispyb/ispyb_dataclass.py +++ b/src/artemis/external_interaction/ispyb/ispyb_dataclass.py @@ -4,7 +4,7 @@ from dataclasses_json import config, dataclass_json -from artemis.utils import Point3D +from artemis.utils.utils import Point3D ISPYB_PARAM_DEFAULTS = { "sample_id": None, diff --git a/src/artemis/external_interaction/ispyb/store_in_ispyb.py b/src/artemis/external_interaction/ispyb/store_in_ispyb.py index bc28c9e11..43c8ea9df 100755 --- a/src/artemis/external_interaction/ispyb/store_in_ispyb.py +++ b/src/artemis/external_interaction/ispyb/store_in_ispyb.py @@ -13,7 +13,7 @@ from artemis.external_interaction.ispyb.ispyb_dataclass import Orientation from artemis.log import LOGGER from artemis.tracing import TRACER -from artemis.utils import Point2D +from artemis.utils.utils import Point2D if TYPE_CHECKING: from artemis.parameters.internal_parameters import InternalParameters diff --git a/src/artemis/external_interaction/system_tests/conftest.py b/src/artemis/external_interaction/system_tests/conftest.py index 5ed2508d1..5eeee4f5c 100644 --- a/src/artemis/external_interaction/system_tests/conftest.py +++ b/src/artemis/external_interaction/system_tests/conftest.py @@ -13,11 +13,11 @@ StoreInIspyb2D, StoreInIspyb3D, ) +from artemis.parameters.external_parameters import from_file as default_raw_params from artemis.parameters.internal_parameters.plan_specific.fgs_internal_params import ( FGSInternalParameters, ) -from artemis.utils import Point3D -from artemis.parameters.external_parameters import from_file as default_raw_params +from artemis.utils.utils import Point3D ISPYB_CONFIG = "/dls_sw/dasc/mariadb/credentials/ispyb-dev.cfg" diff --git a/src/artemis/external_interaction/system_tests/test_zocalo_system.py b/src/artemis/external_interaction/system_tests/test_zocalo_system.py index 0b0a0e503..b5fb723ea 100644 --- a/src/artemis/external_interaction/system_tests/test_zocalo_system.py +++ b/src/artemis/external_interaction/system_tests/test_zocalo_system.py @@ -8,12 +8,11 @@ TEST_RESULT_LARGE, TEST_RESULT_SMALL, ) +from artemis.parameters.external_parameters import from_file as default_raw_params from artemis.parameters.internal_parameters.plan_specific.fgs_internal_params import ( FGSInternalParameters, ) -from artemis.utils import Point3D - -from artemis.parameters.external_parameters import from_file as default_raw_params +from artemis.utils.utils import Point3D @pytest.mark.s03 diff --git a/src/artemis/external_interaction/unit_tests/test_store_in_ispyb.py b/src/artemis/external_interaction/unit_tests/test_store_in_ispyb.py index e1a31883b..cba49d196 100644 --- a/src/artemis/external_interaction/unit_tests/test_store_in_ispyb.py +++ b/src/artemis/external_interaction/unit_tests/test_store_in_ispyb.py @@ -10,11 +10,11 @@ StoreInIspyb3D, ) from artemis.parameters.constants import SIM_ISPYB_CONFIG +from artemis.parameters.external_parameters import from_file as default_raw_params from artemis.parameters.internal_parameters.plan_specific.fgs_internal_params import ( FGSInternalParameters, ) -from artemis.parameters.external_parameters import from_file as default_raw_params -from artemis.utils import Point3D +from artemis.utils.utils import Point3D TEST_DATA_COLLECTION_IDS = [12, 13] TEST_DATA_COLLECTION_GROUP_ID = 34 diff --git a/src/artemis/external_interaction/unit_tests/test_zocalo_interaction.py b/src/artemis/external_interaction/unit_tests/test_zocalo_interaction.py index 0893d6546..84873ad0f 100644 --- a/src/artemis/external_interaction/unit_tests/test_zocalo_interaction.py +++ b/src/artemis/external_interaction/unit_tests/test_zocalo_interaction.py @@ -15,7 +15,7 @@ ZocaloInteractor, ) from artemis.parameters.constants import SIM_ZOCALO_ENV -from artemis.utils import Point3D +from artemis.utils.utils import Point3D EXPECTED_DCID = 100 EXPECTED_RUN_START_MESSAGE = {"event": "start", "ispyb_dcid": EXPECTED_DCID} diff --git a/src/artemis/external_interaction/zocalo/zocalo_interaction.py b/src/artemis/external_interaction/zocalo/zocalo_interaction.py index b4968aaf9..7f098572a 100644 --- a/src/artemis/external_interaction/zocalo/zocalo_interaction.py +++ b/src/artemis/external_interaction/zocalo/zocalo_interaction.py @@ -12,7 +12,7 @@ import artemis.log from artemis.exceptions import WarningException -from artemis.utils import Point3D +from artemis.utils.utils import Point3D TIMEOUT = 90 @@ -81,7 +81,7 @@ def run_end(self, data_collection_id: int): ) def wait_for_result( - self, data_collection_group_id: int, timeout: int = None + self, data_collection_group_id: int, timeout: int | None = None ) -> Point3D: """Block until a result is received from Zocalo. Args: diff --git a/src/artemis/parameters/internal_parameters/internal_parameters.py b/src/artemis/parameters/internal_parameters/internal_parameters.py index 6a6169a2a..f90d0b922 100644 --- a/src/artemis/parameters/internal_parameters/internal_parameters.py +++ b/src/artemis/parameters/internal_parameters/internal_parameters.py @@ -16,7 +16,7 @@ SIM_INSERTION_PREFIX, SIM_ZOCALO_ENV, ) -from artemis.utils import Point3D +from artemis.utils.utils import Point3D class ArtemisParameters: diff --git a/src/artemis/parameters/internal_parameters/plan_specific/tests/test_fgs_internal_parameters.py b/src/artemis/parameters/internal_parameters/plan_specific/tests/test_fgs_internal_parameters.py index d70859ddb..61fbcfb00 100644 --- a/src/artemis/parameters/internal_parameters/plan_specific/tests/test_fgs_internal_parameters.py +++ b/src/artemis/parameters/internal_parameters/plan_specific/tests/test_fgs_internal_parameters.py @@ -5,7 +5,7 @@ from artemis.parameters.internal_parameters.plan_specific.fgs_internal_params import ( FGSInternalParameters, ) -from artemis.utils import Point3D +from artemis.utils.utils import Point3D def test_FGS_parameters_load_from_file(): diff --git a/src/artemis/parameters/internal_parameters/plan_specific/tests/test_rotation_internal_parameters.py b/src/artemis/parameters/internal_parameters/plan_specific/tests/test_rotation_internal_parameters.py index f64e1f7d1..3c85d820a 100644 --- a/src/artemis/parameters/internal_parameters/plan_specific/tests/test_rotation_internal_parameters.py +++ b/src/artemis/parameters/internal_parameters/plan_specific/tests/test_rotation_internal_parameters.py @@ -8,7 +8,7 @@ RotationInternalParameters, RotationScanParams, ) -from artemis.utils import Point3D +from artemis.utils.utils import Point3D def test_rotation_scan_param_validity(): diff --git a/src/artemis/system_tests/test_main_system.py b/src/artemis/system_tests/test_main_system.py index 5fcb6dad4..4c2b7446d 100644 --- a/src/artemis/system_tests/test_main_system.py +++ b/src/artemis/system_tests/test_main_system.py @@ -225,7 +225,7 @@ def test_given_started_when_RE_stops_on_its_own_with_error_then_error_reported( assert response_json["message"] == 'Exception("D\'Oh")' -def test_given_started_and_return_status_interrupted_when_RE_aborted_then_error_reported( +def test_when_started_n_returnstatus_interrupted_bc_RE_aborted_thn_error_reptd( test_env: ClientAndRunEngine, ): test_env.mock_run_engine.aborting_takes_time = True @@ -273,6 +273,7 @@ def test_cli_args_parse(): assert test_args == ("DEBUG", True, True, True) +@pytest.mark.skip(reason="fixed in #621") @patch("dodal.i03.ApertureScatterguard") @patch("dodal.i03.Backlight") @patch("dodal.i03.EigerDetector") @@ -369,6 +370,7 @@ def test_when_blueskyrunner_initiated_and_skip_flag_is_not_set_then_all_plans_se "param_type": MagicMock(), }, }, + clear=True, ): BlueskyRunner(MagicMock(), skip_startup_connection=False) assert mock_setup.call_count == 3 diff --git a/src/artemis/utils/oav_utils.py b/src/artemis/utils/oav_utils.py new file mode 100644 index 000000000..570ea1296 --- /dev/null +++ b/src/artemis/utils/oav_utils.py @@ -0,0 +1,18 @@ +import bluesky.plan_stubs as bps +from dodal.devices.oav.oav_detector import OAV + + +def get_waveforms_to_image_scale(oav: OAV): + """ + Returns the scale of the image. + Args: + oav (OAV): The OAV device in use. + Returns: + The (i_dimensions,j_dimensions) where n_dimensions is the scale of the camera image to the + waveform values on the n axis. + """ + image_size_i = yield from bps.rd(oav.cam.array_size.array_size_x) + image_size_j = yield from bps.rd(oav.cam.array_size.array_size_y) + waveform_size_i = yield from bps.rd(oav.mxsc.waveform_size_x) + waveform_size_j = yield from bps.rd(oav.mxsc.waveform_size_y) + return image_size_i / waveform_size_i, image_size_j / waveform_size_j diff --git a/src/artemis/utils.py b/src/artemis/utils/utils.py similarity index 100% rename from src/artemis/utils.py rename to src/artemis/utils/utils.py