mx-bluesky 0.0.2__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. mx_bluesky/__main__.py +1 -2
  2. mx_bluesky/_version.py +14 -2
  3. mx_bluesky/beamlines/i04/__init__.py +3 -0
  4. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +45 -0
  5. mx_bluesky/beamlines/i04/thawing_plan.py +85 -0
  6. mx_bluesky/beamlines/i24/serial/__init__.py +49 -0
  7. mx_bluesky/beamlines/i24/serial/blueapi_config.yaml +12 -0
  8. mx_bluesky/{I24 → beamlines/i24}/serial/dcid.py +53 -41
  9. mx_bluesky/{I24 → beamlines/i24}/serial/extruder/EX-gui-edm/DetStage.edl +3 -4
  10. mx_bluesky/{I24 → beamlines/i24}/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +28 -32
  11. mx_bluesky/{I24 → beamlines/i24}/serial/extruder/EX-gui-edm/microdrop_alignment.edl +0 -1
  12. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +516 -0
  13. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/CustomChip_py3v1.edl +3 -4
  14. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/DetStage.edl +3 -4
  15. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +273 -223
  16. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/ME14E-GeneralPurpose.edl +0 -1
  17. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/MappingLite-oxford_py3v1.edl +12 -13
  18. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/PMAC_Command.edl +0 -1
  19. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/Shutter_Control.edl +0 -1
  20. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/microdrop_alignment.edl +0 -1
  21. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/nudgechip.edl +0 -1
  22. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/pumpprobe-py3v1.edl +273 -143
  23. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/short1-laser.png +0 -0
  24. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/short2-laser.png +0 -0
  25. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/ft_utils.py +24 -1
  26. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +808 -0
  27. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +377 -416
  28. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/i24ssx_Chip_Mapping_py3v1.py +34 -40
  29. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +328 -0
  30. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/i24ssx_moveonclick.py +66 -48
  31. mx_bluesky/{I24 → beamlines/i24}/serial/log.py +66 -19
  32. mx_bluesky/beamlines/i24/serial/parameters/__init__.py +15 -0
  33. mx_bluesky/beamlines/i24/serial/parameters/constants.py +47 -0
  34. mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py +103 -0
  35. mx_bluesky/beamlines/i24/serial/parameters/fixed_target/cs/cs_maker.json +9 -0
  36. mx_bluesky/{I24 → beamlines/i24}/serial/parameters/fixed_target/cs/motor_direction.txt +1 -1
  37. mx_bluesky/{I24 → beamlines/i24}/serial/parameters/fixed_target/pvar_files/minichip-oxford.pvar +1 -1
  38. mx_bluesky/beamlines/i24/serial/parameters/utils.py +42 -0
  39. mx_bluesky/beamlines/i24/serial/run_extruder.sh +19 -0
  40. mx_bluesky/beamlines/i24/serial/run_fixed_target.sh +22 -0
  41. mx_bluesky/beamlines/i24/serial/run_serial.py +36 -0
  42. mx_bluesky/{I24 → beamlines/i24}/serial/set_visit_directory.sh +6 -1
  43. mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/pv.py +1 -62
  44. mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/pv_abstract.py +6 -7
  45. mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/setup_beamline.py +90 -269
  46. mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/setup_detector.py +47 -40
  47. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +459 -0
  48. mx_bluesky/beamlines/i24/serial/start_blueapi.sh +28 -0
  49. mx_bluesky/beamlines/i24/serial/write_nexus.py +105 -0
  50. mx_bluesky/example.py +4 -4
  51. mx_bluesky/hyperion/__init__.py +1 -0
  52. mx_bluesky/hyperion/__main__.py +374 -0
  53. mx_bluesky/hyperion/device_setup_plans/__init__.py +0 -0
  54. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +134 -0
  55. mx_bluesky/hyperion/device_setup_plans/manipulate_sample.py +110 -0
  56. mx_bluesky/hyperion/device_setup_plans/position_detector.py +16 -0
  57. mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py +60 -0
  58. mx_bluesky/hyperion/device_setup_plans/setup_oav.py +87 -0
  59. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +210 -0
  60. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +214 -0
  61. mx_bluesky/hyperion/device_setup_plans/smargon.py +25 -0
  62. mx_bluesky/hyperion/device_setup_plans/utils.py +44 -0
  63. mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +93 -0
  64. mx_bluesky/hyperion/exceptions.py +47 -0
  65. mx_bluesky/hyperion/experiment_plans/__init__.py +30 -0
  66. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +84 -0
  67. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +528 -0
  68. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +209 -0
  69. mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +173 -0
  70. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +81 -0
  71. mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +463 -0
  72. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +119 -0
  73. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +164 -0
  74. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +322 -0
  75. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +436 -0
  76. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +68 -0
  77. mx_bluesky/hyperion/external_interaction/__init__.py +9 -0
  78. mx_bluesky/hyperion/external_interaction/callbacks/__init__.py +10 -0
  79. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +148 -0
  80. mx_bluesky/hyperion/external_interaction/callbacks/aperture_change_callback.py +22 -0
  81. mx_bluesky/hyperion/external_interaction/callbacks/common/__init__.py +0 -0
  82. mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py +46 -0
  83. mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py +70 -0
  84. mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py +88 -0
  85. mx_bluesky/hyperion/external_interaction/callbacks/ispyb_callback_base.py +203 -0
  86. mx_bluesky/hyperion/external_interaction/callbacks/log_uid_tag_callback.py +20 -0
  87. mx_bluesky/hyperion/external_interaction/callbacks/logging_callback.py +29 -0
  88. mx_bluesky/hyperion/external_interaction/callbacks/plan_reactive_callback.py +101 -0
  89. mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +88 -0
  90. mx_bluesky/hyperion/external_interaction/callbacks/rotation/__init__.py +0 -0
  91. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +174 -0
  92. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +17 -0
  93. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +102 -0
  94. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/__init__.py +0 -0
  95. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +269 -0
  96. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_mapping.py +53 -0
  97. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py +95 -0
  98. mx_bluesky/hyperion/external_interaction/callbacks/zocalo_callback.py +92 -0
  99. mx_bluesky/hyperion/external_interaction/config_server.py +35 -0
  100. mx_bluesky/hyperion/external_interaction/exceptions.py +13 -0
  101. mx_bluesky/hyperion/external_interaction/ispyb/__init__.py +0 -0
  102. mx_bluesky/hyperion/external_interaction/ispyb/data_model.py +95 -0
  103. mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +125 -0
  104. mx_bluesky/hyperion/external_interaction/ispyb/ispyb_store.py +276 -0
  105. mx_bluesky/hyperion/external_interaction/ispyb/ispyb_utils.py +29 -0
  106. mx_bluesky/hyperion/external_interaction/nexus/__init__.py +0 -0
  107. mx_bluesky/hyperion/external_interaction/nexus/nexus_utils.py +148 -0
  108. mx_bluesky/hyperion/external_interaction/nexus/write_nexus.py +114 -0
  109. mx_bluesky/hyperion/log.py +99 -0
  110. mx_bluesky/hyperion/parameters/__init__.py +2 -0
  111. mx_bluesky/hyperion/parameters/cli.py +68 -0
  112. mx_bluesky/hyperion/parameters/components.py +253 -0
  113. mx_bluesky/hyperion/parameters/constants.py +158 -0
  114. mx_bluesky/hyperion/parameters/gridscan.py +216 -0
  115. mx_bluesky/hyperion/parameters/rotation.py +160 -0
  116. mx_bluesky/hyperion/resources/panda/panda-gridscan.yaml +964 -0
  117. mx_bluesky/hyperion/tracing.py +28 -0
  118. mx_bluesky/hyperion/utils/context.py +84 -0
  119. mx_bluesky/hyperion/utils/utils.py +25 -0
  120. mx_bluesky/hyperion/utils/validation.py +196 -0
  121. mx_bluesky/jupyter_example.ipynb +3 -2
  122. {mx_bluesky-0.0.2.dist-info → mx_bluesky-1.1.0.dist-info}/METADATA +53 -32
  123. mx_bluesky-1.1.0.dist-info/RECORD +136 -0
  124. {mx_bluesky-0.0.2.dist-info → mx_bluesky-1.1.0.dist-info}/WHEEL +1 -1
  125. mx_bluesky-1.1.0.dist-info/entry_points.txt +8 -0
  126. mx_bluesky/I24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +0 -476
  127. mx_bluesky/I24/serial/fixed_target/FT-gui-edm/ME14E-motors.edl +0 -1874
  128. mx_bluesky/I24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +0 -706
  129. mx_bluesky/I24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +0 -463
  130. mx_bluesky/I24/serial/parameters/__init__.py +0 -5
  131. mx_bluesky/I24/serial/parameters/constants.py +0 -39
  132. mx_bluesky/I24/serial/parameters/fixed_target/cs/cs_maker.json +0 -9
  133. mx_bluesky/I24/serial/parameters/fixed_target/cs/fiducial_1.txt +0 -4
  134. mx_bluesky/I24/serial/parameters/fixed_target/cs/fiducial_2.txt +0 -4
  135. mx_bluesky/I24/serial/parameters/fixed_target/litemaps/currentchip.map +0 -81
  136. mx_bluesky/I24/serial/parameters/fixed_target/parameters.txt +0 -13
  137. mx_bluesky/I24/serial/run_serial.py +0 -52
  138. mx_bluesky/I24/serial/write_nexus.py +0 -113
  139. mx_bluesky-0.0.2.dist-info/RECORD +0 -58
  140. mx_bluesky-0.0.2.dist-info/entry_points.txt +0 -4
  141. /mx_bluesky/{I24 → beamlines}/__init__.py +0 -0
  142. /mx_bluesky/{I24/serial → beamlines/i24}/__init__.py +0 -0
  143. /mx_bluesky/{I24 → beamlines/i24}/serial/extruder/__init__.py +0 -0
  144. /mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/__init__.py +0 -0
  145. /mx_bluesky/{I24 → beamlines/i24}/serial/parameters/fixed_target/pvar_files/oxford.pvar +0 -0
  146. /mx_bluesky/{I24 → beamlines/i24}/serial/run_ssx.sh +0 -0
  147. /mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/__init__.py +0 -0
  148. /mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/ca.py +0 -0
  149. {mx_bluesky-0.0.2.dist-info → mx_bluesky-1.1.0.dist-info}/LICENSE +0 -0
  150. {mx_bluesky-0.0.2.dist-info → mx_bluesky-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,87 @@
1
+ from functools import partial
2
+
3
+ import bluesky.plan_stubs as bps
4
+ from dodal.devices.oav.oav_detector import OAV
5
+ from dodal.devices.oav.oav_errors import OAVError_ZoomLevelNotFound
6
+ from dodal.devices.oav.oav_parameters import OAVParameters
7
+ from dodal.devices.oav.pin_image_recognition import PinTipDetection
8
+ from dodal.devices.oav.utils import ColorMode
9
+
10
+ from mx_bluesky.hyperion.parameters.constants import CONST
11
+
12
+ # Helper function to make sure we set the waiting groups correctly
13
+ set_using_group = partial(bps.abs_set, group=CONST.WAIT.READY_FOR_OAV)
14
+
15
+
16
+ def setup_pin_tip_detection_params(
17
+ pin_tip_detect_device: PinTipDetection, parameters: OAVParameters
18
+ ):
19
+ # select which blur to apply to image
20
+ yield from set_using_group(
21
+ pin_tip_detect_device.preprocess_operation, parameters.preprocess
22
+ )
23
+
24
+ # sets length scale for blurring
25
+ yield from set_using_group(
26
+ pin_tip_detect_device.preprocess_ksize, parameters.preprocess_K_size
27
+ )
28
+
29
+ # Canny edge detect - lower
30
+ yield from set_using_group(
31
+ pin_tip_detect_device.canny_lower_threshold,
32
+ parameters.canny_edge_lower_threshold,
33
+ )
34
+
35
+ # Canny edge detect - upper
36
+ yield from set_using_group(
37
+ pin_tip_detect_device.canny_upper_threshold,
38
+ parameters.canny_edge_upper_threshold,
39
+ )
40
+
41
+ # "Close" morphological operation
42
+ yield from set_using_group(
43
+ pin_tip_detect_device.close_ksize, parameters.close_ksize
44
+ )
45
+
46
+ # Sample detection direction
47
+ yield from set_using_group(
48
+ pin_tip_detect_device.scan_direction, parameters.direction
49
+ )
50
+
51
+ # Minimum height
52
+ yield from set_using_group(
53
+ pin_tip_detect_device.min_tip_height,
54
+ parameters.minimum_height,
55
+ )
56
+
57
+
58
+ def setup_general_oav_params(oav: OAV, parameters: OAVParameters):
59
+ yield from set_using_group(oav.cam.color_mode, ColorMode.RGB1)
60
+ yield from set_using_group(oav.cam.acquire_period, parameters.acquire_period)
61
+ yield from set_using_group(oav.cam.acquire_time, parameters.exposure)
62
+ yield from set_using_group(oav.cam.gain, parameters.gain)
63
+
64
+ zoom_level_str = f"{float(parameters.zoom)}x"
65
+ if zoom_level_str not in oav.zoom_controller.allowed_zoom_levels:
66
+ raise OAVError_ZoomLevelNotFound(
67
+ f"Found {zoom_level_str} as a zoom level but expected one of {oav.zoom_controller.allowed_zoom_levels}"
68
+ )
69
+
70
+ yield from bps.abs_set(
71
+ oav.zoom_controller,
72
+ zoom_level_str,
73
+ wait=True,
74
+ )
75
+
76
+
77
+ def pre_centring_setup_oav(
78
+ oav: OAV,
79
+ parameters: OAVParameters,
80
+ pin_tip_detection_device: PinTipDetection,
81
+ ):
82
+ """
83
+ Setup OAV PVs with required values.
84
+ """
85
+ yield from setup_general_oav_params(oav, parameters)
86
+ yield from setup_pin_tip_detection_params(pin_tip_detection_device, parameters)
87
+ yield from bps.wait(CONST.WAIT.READY_FOR_OAV)
@@ -0,0 +1,210 @@
1
+ from datetime import datetime
2
+ from enum import Enum
3
+ from importlib import resources
4
+ from pathlib import Path
5
+
6
+ import bluesky.plan_stubs as bps
7
+ from blueapi.core import MsgGenerator
8
+ from dodal.common.beamlines.beamline_utils import get_path_provider
9
+ from dodal.devices.fast_grid_scan import PandAGridScanParams
10
+ from ophyd_async.core import load_device
11
+ from ophyd_async.fastcs.panda import (
12
+ HDFPanda,
13
+ SeqTable,
14
+ SeqTrigger,
15
+ )
16
+
17
+ import mx_bluesky.hyperion.resources.panda as panda_resource
18
+ from mx_bluesky.hyperion.log import LOGGER
19
+
20
+ MM_TO_ENCODER_COUNTS = 200000
21
+ GENERAL_TIMEOUT = 60
22
+ TICKS_PER_MS = 1000 # Panda sequencer prescaler will be set to us
23
+
24
+
25
+ class Enabled(Enum):
26
+ ENABLED = "ONE"
27
+ DISABLED = "ZERO"
28
+
29
+
30
+ class PcapArm(Enum):
31
+ ARMED = "Arm"
32
+ DISARMED = "Disarm"
33
+
34
+
35
+ def _get_seq_table(
36
+ parameters: PandAGridScanParams, exposure_distance_mm, time_between_steps_ms
37
+ ) -> SeqTable:
38
+ """
39
+ Generate the sequencer table for the panda.
40
+
41
+ - Sending a 'trigger' means trigger PCAP internally and send signal to Eiger via physical panda output
42
+
43
+ SEQUENCER TABLE:
44
+
45
+ 1. Wait for physical trigger from motion script to mark start of scan / change of direction
46
+ 2. Wait for POSA (X2) to be greater than X_START and send x_steps triggers every time_between_steps_ms
47
+ 3. Wait for physical trigger from motion script to mark change of direction
48
+ 4. Wait for POSA (X2) to be less than X_START + X_STEP_SIZE * x_steps + exposure distance, then
49
+ send x_steps triggers every time_between_steps_ms
50
+ 5. Go back to step one.
51
+
52
+ For a more detailed explanation and a diagram, see https://github.com/DiamondLightSource/hyperion/wiki/PandA-constant%E2%80%90motion-scanning
53
+
54
+ For documentation on Panda itself, see https://pandablocks.github.io/PandABlocks-FPGA/master/index.html
55
+
56
+ Args:
57
+ exposure_distance_mm: The distance travelled by the sample each time the detector is exposed: exposure time * sample velocity
58
+ time_between_steps_ms: The time taken to traverse between each grid step.
59
+ parameters: Parameters for the panda gridscan
60
+
61
+ Returns:
62
+ An instance of SeqTable describing the panda sequencer table
63
+ """
64
+
65
+ start_of_grid_x_counts = int(parameters.x_start * MM_TO_ENCODER_COUNTS)
66
+
67
+ # x_start is the first trigger point, so we need to travel to x_steps-1 for the final trigger point
68
+ end_of_grid_x_counts = int(
69
+ start_of_grid_x_counts
70
+ + (parameters.x_step_size * (parameters.x_steps - 1) * MM_TO_ENCODER_COUNTS)
71
+ )
72
+
73
+ exposure_distance_x_counts = int(exposure_distance_mm * MM_TO_ENCODER_COUNTS)
74
+
75
+ num_pulses = parameters.x_steps
76
+
77
+ delay_between_pulses = time_between_steps_ms * TICKS_PER_MS
78
+
79
+ PULSE_WIDTH_US = 1
80
+
81
+ assert delay_between_pulses > PULSE_WIDTH_US
82
+
83
+ # BITA_1 trigger wired from TTLIN1, this is the trigger input
84
+
85
+ # +ve direction scan
86
+
87
+ table = (
88
+ SeqTable.row(trigger=SeqTrigger.BITA_1, time2=1)
89
+ + SeqTable.row(
90
+ repeats=num_pulses,
91
+ trigger=SeqTrigger.POSA_GT,
92
+ position=start_of_grid_x_counts,
93
+ time1=PULSE_WIDTH_US,
94
+ outa1=True,
95
+ time2=delay_between_pulses - PULSE_WIDTH_US,
96
+ outa2=False,
97
+ )
98
+ +
99
+ # -ve direction scan
100
+ SeqTable.row(trigger=SeqTrigger.BITA_1, time2=1)
101
+ + SeqTable.row(
102
+ repeats=num_pulses,
103
+ trigger=SeqTrigger.POSA_LT,
104
+ position=end_of_grid_x_counts + exposure_distance_x_counts,
105
+ time1=PULSE_WIDTH_US,
106
+ outa1=True,
107
+ time2=delay_between_pulses - PULSE_WIDTH_US,
108
+ outa2=False,
109
+ )
110
+ )
111
+
112
+ return table
113
+
114
+
115
+ def setup_panda_for_flyscan(
116
+ panda: HDFPanda,
117
+ parameters: PandAGridScanParams,
118
+ initial_x: float,
119
+ exposure_time_s: float,
120
+ time_between_x_steps_ms: float,
121
+ sample_velocity_mm_per_s: float,
122
+ ) -> MsgGenerator:
123
+ """Configures the PandA device for a flyscan.
124
+ Sets PVs from a yaml file, calibrates the encoder, and
125
+ adjusts the sequencer table based off the grid parameters. Yaml file can be
126
+ created using ophyd_async.core.save_device()
127
+
128
+ Args:
129
+ panda (HDFPanda): The PandA Ophyd device
130
+ parameters (PandAGridScanParams): Grid parameters
131
+ initial_x (float): Motor positions at time of PandA setup
132
+ exposure_time_s (float): Detector exposure time per trigger
133
+ time_between_x_steps_ms (float): Time, in ms, between each trigger. Equal to deadtime + exposure time
134
+ sample_velocity_mm_per_s (float): Velocity of the sample in mm/s = x_step_size_mm * 1000 /
135
+ time_between_x_steps_ms
136
+ Returns:
137
+ MsgGenerator
138
+
139
+ Yields:
140
+ Iterator[MsgGenerator]
141
+ """
142
+ assert parameters.x_steps > 0
143
+ assert time_between_x_steps_ms * 1000 >= exposure_time_s
144
+ assert sample_velocity_mm_per_s * exposure_time_s < parameters.x_step_size
145
+
146
+ yield from bps.stage(panda, group="panda-config")
147
+
148
+ with resources.as_file(
149
+ resources.files(panda_resource) / "panda-gridscan.yaml"
150
+ ) as config_yaml_path:
151
+ yield from load_device(panda, str(config_yaml_path))
152
+
153
+ # Home the PandA X encoder using current motor position
154
+ yield from bps.abs_set(
155
+ panda.inenc[1].setp, # type: ignore
156
+ initial_x * MM_TO_ENCODER_COUNTS,
157
+ wait=True,
158
+ )
159
+
160
+ yield from bps.abs_set(panda.pulse[1].width, exposure_time_s, group="panda-config")
161
+
162
+ exposure_distance_mm = sample_velocity_mm_per_s * exposure_time_s
163
+
164
+ table = _get_seq_table(parameters, exposure_distance_mm, time_between_x_steps_ms)
165
+
166
+ yield from bps.abs_set(panda.seq[1].table, table, group="panda-config")
167
+
168
+ yield from bps.abs_set(
169
+ panda.pcap.enable, # type: ignore
170
+ Enabled.ENABLED.value,
171
+ group="panda-config",
172
+ )
173
+
174
+ # Values need to be set before blocks are enabled, so wait here
175
+ yield from bps.wait(group="panda-config", timeout=GENERAL_TIMEOUT)
176
+
177
+ LOGGER.info(f"PandA sequencer table has been set to: {str(table)}")
178
+ table_readback = yield from bps.rd(panda.seq[1].table)
179
+ LOGGER.debug(f"PandA sequencer table readback is: {str(table_readback)}")
180
+
181
+ yield from arm_panda_for_gridscan(panda)
182
+
183
+
184
+ def arm_panda_for_gridscan(panda: HDFPanda, group="arm_panda_gridscan"):
185
+ yield from bps.abs_set(panda.seq[1].enable, Enabled.ENABLED.value, group=group) # type: ignore
186
+ yield from bps.abs_set(panda.pulse[1].enable, Enabled.ENABLED.value, group=group) # type: ignore
187
+ yield from bps.abs_set(panda.counter[1].enable, Enabled.ENABLED.value, group=group) # type: ignore
188
+ yield from bps.abs_set(panda.pcap.arm, PcapArm.ARMED.value, group=group) # type: ignore
189
+ yield from bps.wait(group=group, timeout=GENERAL_TIMEOUT)
190
+ LOGGER.info("PandA has been armed")
191
+
192
+
193
+ def disarm_panda_for_gridscan(panda, group="disarm_panda_gridscan") -> MsgGenerator:
194
+ yield from bps.abs_set(panda.pcap.arm, PcapArm.DISARMED.value, group=group) # type: ignore
195
+ yield from bps.abs_set(panda.counter[1].enable, Enabled.DISABLED.value, group=group) # type: ignore
196
+ yield from bps.abs_set(panda.seq[1].enable, Enabled.DISABLED.value, group=group)
197
+ yield from bps.abs_set(panda.pulse[1].enable, Enabled.DISABLED.value, group=group)
198
+ yield from bps.abs_set(panda.pcap.enable, Enabled.DISABLED.value, group=group) # type: ignore
199
+ yield from bps.wait(group=group, timeout=GENERAL_TIMEOUT)
200
+
201
+
202
+ def set_panda_directory(panda_directory: Path) -> MsgGenerator:
203
+ """Updates the root folder which is used by the PandA's PCAP."""
204
+
205
+ suffix = datetime.now().strftime("_%Y%m%d%H%M%S")
206
+
207
+ async def set_panda_dir():
208
+ await get_path_provider().update(directory=panda_directory, suffix=suffix)
209
+
210
+ yield from bps.wait_for([set_panda_dir])
@@ -0,0 +1,214 @@
1
+ from collections.abc import Callable
2
+ from functools import wraps
3
+
4
+ import bluesky.plan_stubs as bps
5
+ import bluesky.preprocessors as bpp
6
+ from blueapi.core import MsgGenerator
7
+ from dodal.devices.zebra import (
8
+ AUTO_SHUTTER_GATE,
9
+ AUTO_SHUTTER_INPUT,
10
+ DISCONNECT,
11
+ IN1_TTL,
12
+ IN3_TTL,
13
+ IN4_TTL,
14
+ PC_GATE,
15
+ PC_PULSE,
16
+ TTL_DETECTOR,
17
+ TTL_PANDA,
18
+ TTL_XSPRESS3,
19
+ ArmDemand,
20
+ EncEnum,
21
+ I03Axes,
22
+ RotationDirection,
23
+ Zebra,
24
+ )
25
+ from dodal.devices.zebra_controlled_shutter import ZebraShutter, ZebraShutterControl
26
+
27
+ from mx_bluesky.hyperion.log import LOGGER
28
+
29
+ ZEBRA_STATUS_TIMEOUT = 30
30
+
31
+
32
+ def bluesky_retry(func: Callable):
33
+ """Decorator that will retry the decorated plan if it fails.
34
+
35
+ Use this with care as it knows nothing about the state of the world when things fail.
36
+ If it is possible that your plan fails when the beamline is in a transient state that
37
+ the plan could not act on do not use this decorator without doing some more intelligent
38
+ clean up.
39
+
40
+ You should avoid using this decorator often in general production as it hides errors,
41
+ instead it should be used only for debugging these underlying errors.
42
+ """
43
+
44
+ @wraps(func)
45
+ def newfunc(*args, **kwargs):
46
+ def log_and_retry(exception):
47
+ LOGGER.error(f"Function {func.__name__} failed with {exception}, retrying")
48
+ yield from func(*args, **kwargs)
49
+
50
+ yield from bpp.contingency_wrapper(
51
+ func(*args, **kwargs), except_plan=log_and_retry, auto_raise=False
52
+ )
53
+
54
+ return newfunc
55
+
56
+
57
+ def arm_zebra(zebra: Zebra):
58
+ yield from bps.abs_set(zebra.pc.arm, ArmDemand.ARM, wait=True)
59
+
60
+
61
+ def tidy_up_zebra_after_rotation_scan(
62
+ zebra: Zebra,
63
+ zebra_shutter: ZebraShutter,
64
+ group="tidy_up_zebra_after_rotation",
65
+ wait=True,
66
+ ):
67
+ yield from bps.abs_set(zebra.pc.arm, ArmDemand.DISARM, group=group)
68
+ yield from bps.abs_set(
69
+ zebra_shutter.control_mode, ZebraShutterControl.MANUAL, group=group
70
+ )
71
+ if wait:
72
+ yield from bps.wait(group, timeout=ZEBRA_STATUS_TIMEOUT)
73
+
74
+
75
+ def set_shutter_auto_input(zebra: Zebra, input: int, group="set_shutter_trigger"):
76
+ """Set the input that the shutter uses when set to auto.
77
+
78
+ For more details see the ZebraShutter device."""
79
+ auto_shutter_control = zebra.logic_gates.and_gates[AUTO_SHUTTER_GATE]
80
+ yield from bps.abs_set(
81
+ auto_shutter_control.sources[AUTO_SHUTTER_INPUT], input, group
82
+ )
83
+
84
+
85
+ @bluesky_retry
86
+ def setup_zebra_for_rotation(
87
+ zebra: Zebra,
88
+ zebra_shutter: ZebraShutter,
89
+ axis: EncEnum = I03Axes.OMEGA,
90
+ start_angle: float = 0,
91
+ scan_width: float = 360,
92
+ shutter_opening_deg: float = 2.5,
93
+ shutter_opening_s: float = 0.04,
94
+ direction: RotationDirection = RotationDirection.POSITIVE,
95
+ group: str = "setup_zebra_for_rotation",
96
+ wait: bool = True,
97
+ ):
98
+ """Set up the Zebra to collect a rotation dataset. Any plan using this is
99
+ responsible for setting the smargon velocity appropriately so that the desired
100
+ image width is achieved with the exposure time given here.
101
+
102
+ Parameters:
103
+ zebra: The zebra device to use
104
+ axis: I03 axes enum representing which axis to use for position
105
+ compare. Currently always omega.
106
+ start_angle: Position at which the scan should begin, in degrees.
107
+ scan_width: Total angle through which to collect, in degrees.
108
+ shutter_opening_deg:How many degrees of rotation it takes for the fast shutter
109
+ to open. Increases the gate width.
110
+ shutter_opening_s: How many seconds it takes for the fast shutter to open. The
111
+ detector pulse is delayed after the shutter signal by this
112
+ amount.
113
+ direction: RotationDirection enum for positive or negative.
114
+ Defaults to Positive.
115
+ group: A name for the group of statuses generated
116
+ wait: Block until all the settings have completed
117
+ """
118
+ if not isinstance(direction, RotationDirection):
119
+ raise ValueError(
120
+ "Disallowed rotation direction provided to Zebra setup plan. "
121
+ "Use RotationDirection.POSITIVE or RotationDirection.NEGATIVE."
122
+ )
123
+ yield from bps.abs_set(zebra.pc.dir, direction.value, group=group)
124
+ LOGGER.info("ZEBRA SETUP: START")
125
+ # Set gate start, adjust for shutter opening time if necessary
126
+ LOGGER.info(f"ZEBRA SETUP: degrees to adjust for shutter = {shutter_opening_deg}")
127
+ LOGGER.info(f"ZEBRA SETUP: start angle start: {start_angle}")
128
+ LOGGER.info(f"ZEBRA SETUP: start angle adjusted, gate start set to: {start_angle}")
129
+ yield from bps.abs_set(zebra.pc.gate_start, start_angle, group=group)
130
+ # set gate width to total width
131
+ yield from bps.abs_set(
132
+ zebra.pc.gate_width, scan_width + shutter_opening_deg, group=group
133
+ )
134
+ LOGGER.info(
135
+ f"Pulse start set to shutter open time, set to: {abs(shutter_opening_s)}"
136
+ )
137
+ yield from bps.abs_set(zebra.pc.pulse_start, abs(shutter_opening_s), group=group)
138
+ # Set gate position to be angle of interest
139
+ yield from bps.abs_set(zebra.pc.gate_trigger, axis.value, group=group)
140
+ # Trigger the shutter with the gate
141
+ yield from bps.abs_set(
142
+ zebra_shutter.control_mode, ZebraShutterControl.AUTO, group=group
143
+ )
144
+ yield from set_shutter_auto_input(zebra, PC_GATE, group=group)
145
+ # Trigger the detector with a pulse
146
+ yield from bps.abs_set(zebra.output.out_pvs[TTL_DETECTOR], PC_PULSE, group=group)
147
+ # Don't use the fluorescence detector
148
+ yield from bps.abs_set(zebra.output.out_pvs[TTL_XSPRESS3], DISCONNECT, group=group)
149
+ yield from bps.abs_set(zebra.output.pulse_1.input, DISCONNECT, group=group)
150
+ LOGGER.info(f"ZEBRA SETUP: END - {'' if wait else 'not'} waiting for completion")
151
+ if wait:
152
+ yield from bps.wait(group, timeout=ZEBRA_STATUS_TIMEOUT)
153
+
154
+
155
+ @bluesky_retry
156
+ def setup_zebra_for_gridscan(
157
+ zebra: Zebra,
158
+ zebra_shutter: ZebraShutter,
159
+ group="setup_zebra_for_gridscan",
160
+ wait=True,
161
+ ):
162
+ yield from bps.abs_set(zebra.output.out_pvs[TTL_DETECTOR], IN3_TTL, group=group)
163
+ yield from bps.abs_set(
164
+ zebra_shutter.control_mode, ZebraShutterControl.AUTO, group=group
165
+ )
166
+ yield from set_shutter_auto_input(zebra, IN4_TTL, group=group)
167
+ yield from bps.abs_set(zebra.output.out_pvs[TTL_XSPRESS3], DISCONNECT, group=group)
168
+ yield from bps.abs_set(zebra.output.pulse_1.input, DISCONNECT, group=group)
169
+
170
+ if wait:
171
+ yield from bps.wait(group, timeout=ZEBRA_STATUS_TIMEOUT)
172
+
173
+
174
+ @bluesky_retry
175
+ def tidy_up_zebra_after_gridscan(
176
+ zebra: Zebra,
177
+ zebra_shutter: ZebraShutter,
178
+ group="tidy_up_zebra_after_gridscan",
179
+ wait=True,
180
+ ) -> MsgGenerator:
181
+ yield from bps.abs_set(zebra.output.out_pvs[TTL_DETECTOR], PC_PULSE, group=group)
182
+ yield from bps.abs_set(
183
+ zebra_shutter.control_mode, ZebraShutterControl.MANUAL, group=group
184
+ )
185
+ yield from set_shutter_auto_input(zebra, PC_GATE, group=group)
186
+
187
+ if wait:
188
+ yield from bps.wait(group, timeout=ZEBRA_STATUS_TIMEOUT)
189
+
190
+
191
+ @bluesky_retry
192
+ def setup_zebra_for_panda_flyscan(
193
+ zebra: Zebra,
194
+ zebra_shutter: ZebraShutter,
195
+ group="setup_zebra_for_panda_flyscan",
196
+ wait=True,
197
+ ):
198
+ # Forwards eiger trigger signal from panda
199
+ yield from bps.abs_set(zebra.output.out_pvs[TTL_DETECTOR], IN1_TTL, group=group)
200
+
201
+ # Forwards signal from PPMAC to fast shutter. High while panda PLC is running
202
+ yield from bps.abs_set(
203
+ zebra_shutter.control_mode, ZebraShutterControl.AUTO, group=group
204
+ )
205
+ yield from set_shutter_auto_input(zebra, IN4_TTL, group=group)
206
+
207
+ yield from bps.abs_set(zebra.output.out_pvs[TTL_XSPRESS3], DISCONNECT, group=group)
208
+
209
+ yield from bps.abs_set(
210
+ zebra.output.out_pvs[TTL_PANDA], IN3_TTL, group=group
211
+ ) # Tells panda that motion is beginning/changing direction
212
+
213
+ if wait:
214
+ yield from bps.wait(group, timeout=ZEBRA_STATUS_TIMEOUT)
@@ -0,0 +1,25 @@
1
+ import numpy as np
2
+ from bluesky import plan_stubs as bps
3
+ from dodal.devices.smargon import Smargon
4
+
5
+ from mx_bluesky.hyperion.exceptions import WarningException
6
+
7
+
8
+ def move_smargon_warn_on_out_of_range(
9
+ smargon: Smargon, position: np.ndarray | list[float] | tuple[float, float, float]
10
+ ):
11
+ """Throws a WarningException if the specified position is out of range for the
12
+ smargon. Otherwise moves to that position."""
13
+ limits = yield from smargon.get_xyz_limits()
14
+ if not limits.position_valid(position):
15
+ raise WarningException(
16
+ "Pin tip centring failed - pin too long/short/bent and out of range"
17
+ )
18
+ yield from bps.mv(
19
+ smargon.x,
20
+ position[0],
21
+ smargon.y,
22
+ position[1],
23
+ smargon.z,
24
+ position[2],
25
+ )
@@ -0,0 +1,44 @@
1
+ from collections.abc import Generator
2
+
3
+ from bluesky import plan_stubs as bps
4
+ from bluesky import preprocessors as bpp
5
+ from bluesky.utils import Msg
6
+ from dodal.devices.detector.detector_motion import DetectorMotion, ShutterState
7
+ from dodal.devices.eiger import EigerDetector
8
+
9
+ from mx_bluesky.hyperion.device_setup_plans.position_detector import (
10
+ set_detector_z_position,
11
+ set_shutter,
12
+ )
13
+
14
+
15
+ def start_preparing_data_collection_then_do_plan(
16
+ eiger: EigerDetector,
17
+ detector_motion: DetectorMotion,
18
+ detector_distance_mm: float | None,
19
+ plan_to_run: Generator[Msg, None, None],
20
+ group="ready_for_data_collection",
21
+ ) -> Generator[Msg, None, None]:
22
+ """Starts preparing for the next data collection and then runs the
23
+ given plan.
24
+
25
+ Preparation consists of:
26
+ * Arming the Eiger
27
+ * Moving the detector to the specified position
28
+ * Opening the detect shutter
29
+ If the plan fails it will disarm the eiger.
30
+ """
31
+
32
+ def wrapped_plan():
33
+ yield from bps.abs_set(eiger.do_arm, 1, group=group)
34
+ if detector_distance_mm:
35
+ yield from set_detector_z_position(
36
+ detector_motion, detector_distance_mm, group
37
+ )
38
+ yield from set_shutter(detector_motion, ShutterState.OPEN, group)
39
+ yield from plan_to_run
40
+
41
+ yield from bpp.contingency_wrapper(
42
+ wrapped_plan(),
43
+ except_plan=lambda e: (yield from bps.stop(eiger)),
44
+ )
@@ -0,0 +1,93 @@
1
+ from bluesky import plan_stubs as bps
2
+ from bluesky.preprocessors import finalize_wrapper
3
+ from bluesky.utils import make_decorator
4
+ from dodal.devices.attenuator import Attenuator
5
+ from dodal.devices.xbpm_feedback import Pause, XBPMFeedback
6
+
7
+ from mx_bluesky.hyperion.log import LOGGER
8
+
9
+
10
+ def _check_and_pause_feedback(
11
+ xbpm_feedback: XBPMFeedback,
12
+ attenuator: Attenuator,
13
+ desired_transmission_fraction: float,
14
+ ):
15
+ """Checks that the xbpm is in position before then turning it off and setting a new
16
+ transmission.
17
+
18
+ Args:
19
+ xbpm_feedback (XBPMFeedback): The XBPM device that is responsible for keeping
20
+ the beam in position
21
+ attenuator (Attenuator): The attenuator used to set transmission
22
+ desired_transmission_fraction (float): The desired transmission to set after
23
+ turning XBPM feedback off.
24
+
25
+ """
26
+ yield from bps.mv(attenuator, 1.0)
27
+ LOGGER.info("Waiting for XBPM feedback to be stable")
28
+ yield from bps.trigger(xbpm_feedback, wait=True)
29
+ LOGGER.info(
30
+ f"XPBM feedback in position, pausing and setting transmission to {desired_transmission_fraction}"
31
+ )
32
+ yield from bps.mv(xbpm_feedback.pause_feedback, Pause.PAUSE)
33
+ yield from bps.mv(attenuator, desired_transmission_fraction)
34
+
35
+
36
+ def _unpause_xbpm_feedback_and_set_transmission_to_1(
37
+ xbpm_feedback: XBPMFeedback, attenuator: Attenuator
38
+ ):
39
+ """Turns the XBPM feedback back on and sets transmission to 1 so that it keeps the
40
+ beam aligned whilst not collecting.
41
+
42
+ Args:
43
+ xbpm_feedback (XBPMFeedback): The XBPM device that is responsible for keeping
44
+ the beam in position
45
+ attenuator (Attenuator): The attenuator used to set transmission
46
+ """
47
+ yield from bps.mv(xbpm_feedback.pause_feedback, Pause.RUN, attenuator, 1.0)
48
+
49
+
50
+ def transmission_and_xbpm_feedback_for_collection_wrapper(
51
+ plan,
52
+ xbpm_feedback: XBPMFeedback,
53
+ attenuator: Attenuator,
54
+ desired_transmission_fraction: float,
55
+ ):
56
+ """Sets the transmission for the data collection, ensuring the xbpm feedback is valid
57
+ this wrapper should be run around every data collection or movement that may disrupt
58
+ the XBPM feedback.
59
+
60
+ XBPM feedback isn't reliable during collections due to:
61
+ * Objects (e.g. attenuator) crossing the beam can cause large (incorrect) feedback movements
62
+ * Lower transmissions/higher energies are less reliable for the xbpm
63
+
64
+ So we need to keep the transmission at 100% and the feedback on when not collecting
65
+ and then turn it off and set the correct transmission for collection. The feedback
66
+ mostly accounts for slow thermal drift so it is safe to assume that the beam is
67
+ stable during a collection.
68
+
69
+ Args:
70
+ plan: The plan performing the data collection
71
+ xbpm_feedback (XBPMFeedback): The XBPM device that is responsible for keeping
72
+ the beam in position
73
+ attenuator (Attenuator): The attenuator used to set transmission
74
+ desired_transmission_fraction (float): The desired transmission for the collection
75
+ """
76
+
77
+ def _inner_plan():
78
+ yield from _check_and_pause_feedback(
79
+ xbpm_feedback, attenuator, desired_transmission_fraction
80
+ )
81
+ return (yield from plan)
82
+
83
+ return (
84
+ yield from finalize_wrapper(
85
+ _inner_plan(),
86
+ _unpause_xbpm_feedback_and_set_transmission_to_1(xbpm_feedback, attenuator),
87
+ )
88
+ )
89
+
90
+
91
+ transmission_and_xbpm_feedback_for_collection_decorator = make_decorator(
92
+ transmission_and_xbpm_feedback_for_collection_wrapper
93
+ )