mx-bluesky 1.2.0__py3-none-any.whl → 1.4.1__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 (105) hide show
  1. mx_bluesky/__init__.py +8 -3
  2. mx_bluesky/__main__.py +12 -7
  3. mx_bluesky/_version.py +2 -2
  4. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +14 -4
  5. mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +178 -0
  6. mx_bluesky/beamlines/i04/thawing_plan.py +49 -11
  7. mx_bluesky/beamlines/i24/serial/__init__.py +3 -0
  8. mx_bluesky/beamlines/i24/serial/dcid.py +143 -171
  9. mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +1 -1
  10. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +121 -110
  11. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +3 -6
  12. mx_bluesky/beamlines/i24/serial/fixed_target/ft_utils.py +0 -1
  13. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +164 -169
  14. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +149 -225
  15. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +7 -216
  16. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +18 -17
  17. mx_bluesky/beamlines/i24/serial/log.py +58 -49
  18. mx_bluesky/beamlines/i24/serial/parameters/__init__.py +4 -0
  19. mx_bluesky/beamlines/i24/serial/parameters/constants.py +6 -1
  20. mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py +42 -15
  21. mx_bluesky/beamlines/i24/serial/parameters/fixed_target/cs/cs_maker.json +3 -3
  22. mx_bluesky/beamlines/i24/serial/run_extruder.sh +30 -5
  23. mx_bluesky/beamlines/i24/serial/run_fixed_target.sh +30 -5
  24. mx_bluesky/beamlines/i24/serial/run_serial.py +24 -8
  25. mx_bluesky/beamlines/i24/serial/setup_beamline/ca.py +0 -2
  26. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +2 -0
  27. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +104 -82
  28. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +9 -20
  29. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +26 -28
  30. mx_bluesky/beamlines/i24/serial/write_nexus.py +74 -72
  31. mx_bluesky/common/__init__.py +0 -0
  32. mx_bluesky/common/device_setup_plans/read_hardware_for_setup.py +14 -0
  33. mx_bluesky/common/external_interaction/config_server.py +46 -0
  34. mx_bluesky/common/parameters/components.py +258 -0
  35. mx_bluesky/common/parameters/constants.py +143 -0
  36. mx_bluesky/common/parameters/gridscan.py +94 -0
  37. mx_bluesky/common/parameters/robot_load.py +16 -0
  38. mx_bluesky/common/plans/__init__.py +1 -0
  39. mx_bluesky/common/plans/do_fgs.py +121 -0
  40. mx_bluesky/common/utils/log.py +118 -0
  41. mx_bluesky/{hyperion → common/utils}/tracing.py +2 -2
  42. mx_bluesky/hyperion/__main__.py +13 -10
  43. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +47 -52
  44. mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py +6 -12
  45. mx_bluesky/hyperion/device_setup_plans/setup_oav.py +6 -12
  46. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +5 -6
  47. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +49 -18
  48. mx_bluesky/hyperion/device_setup_plans/smargon.py +9 -9
  49. mx_bluesky/hyperion/device_setup_plans/utils.py +2 -2
  50. mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +4 -4
  51. mx_bluesky/hyperion/exceptions.py +13 -1
  52. mx_bluesky/hyperion/experiment_plans/__init__.py +4 -0
  53. mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py +83 -0
  54. mx_bluesky/hyperion/experiment_plans/common/xrc_result.py +47 -0
  55. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +9 -9
  56. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +147 -169
  57. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +48 -22
  58. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +75 -9
  59. mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +21 -20
  60. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +9 -6
  61. mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +2 -2
  62. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +40 -21
  63. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +22 -22
  64. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +43 -39
  65. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +69 -18
  66. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +17 -7
  67. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +13 -13
  68. mx_bluesky/hyperion/external_interaction/callbacks/__init__.py +0 -4
  69. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +5 -2
  70. mx_bluesky/hyperion/external_interaction/callbacks/common/abstract_event.py +66 -0
  71. mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py +5 -0
  72. mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py +1 -1
  73. mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py +30 -25
  74. mx_bluesky/hyperion/external_interaction/callbacks/ispyb_callback_base.py +29 -12
  75. mx_bluesky/hyperion/external_interaction/callbacks/log_uid_tag_callback.py +1 -1
  76. mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +19 -11
  77. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +7 -4
  78. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +5 -3
  79. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/__init__.py +0 -0
  80. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/sample_handling_callback.py +84 -0
  81. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +38 -27
  82. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py +5 -4
  83. mx_bluesky/hyperion/external_interaction/config_server.py +11 -28
  84. mx_bluesky/hyperion/external_interaction/exceptions.py +0 -9
  85. mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +65 -15
  86. mx_bluesky/hyperion/external_interaction/ispyb/ispyb_store.py +1 -1
  87. mx_bluesky/hyperion/external_interaction/nexus/nexus_utils.py +2 -2
  88. mx_bluesky/hyperion/external_interaction/nexus/write_nexus.py +1 -1
  89. mx_bluesky/hyperion/log.py +0 -84
  90. mx_bluesky/hyperion/parameters/components.py +4 -251
  91. mx_bluesky/hyperion/parameters/constants.py +22 -119
  92. mx_bluesky/hyperion/parameters/gridscan.py +35 -74
  93. mx_bluesky/hyperion/parameters/load_centre_collect.py +16 -11
  94. mx_bluesky/hyperion/parameters/rotation.py +23 -10
  95. mx_bluesky/hyperion/utils/utils.py +17 -0
  96. mx_bluesky/hyperion/utils/validation.py +5 -6
  97. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/METADATA +36 -33
  98. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/RECORD +102 -89
  99. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/WHEEL +1 -1
  100. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Mapping_py3v1.py +0 -161
  101. mx_bluesky/example.py +0 -19
  102. mx_bluesky/hyperion/parameters/robot_load.py +0 -16
  103. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/LICENSE +0 -0
  104. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/entry_points.txt +0 -0
  105. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import dataclasses
4
- from collections.abc import Callable
4
+ from collections.abc import Callable, Sequence
5
5
  from functools import partial
6
6
  from pathlib import Path
7
- from time import time
8
7
  from typing import Protocol
9
8
 
10
9
  import bluesky.plan_stubs as bps
11
10
  import bluesky.preprocessors as bpp
12
11
  import numpy as np
13
- from blueapi.core import BlueskyContext, MsgGenerator
12
+ import pydantic
13
+ from blueapi.core import BlueskyContext
14
+ from bluesky.callbacks import CallbackBase
15
+ from bluesky.utils import MsgGenerator
14
16
  from dodal.devices.aperturescatterguard import (
15
17
  ApertureScatterguard,
16
- ApertureValue,
17
18
  )
18
19
  from dodal.devices.attenuator import Attenuator
19
20
  from dodal.devices.backlight import Backlight
@@ -30,7 +31,7 @@ from dodal.devices.fast_grid_scan import (
30
31
  from dodal.devices.flux import Flux
31
32
  from dodal.devices.robot import BartRobot
32
33
  from dodal.devices.s4_slit_gaps import S4SlitGaps
33
- from dodal.devices.smargon import Smargon, StubPosition
34
+ from dodal.devices.smargon import Smargon
34
35
  from dodal.devices.synchrotron import Synchrotron
35
36
  from dodal.devices.undulator import Undulator
36
37
  from dodal.devices.xbpm_feedback import XBPMFeedback
@@ -39,17 +40,17 @@ from dodal.devices.zebra_controlled_shutter import ZebraShutter
39
40
  from dodal.devices.zocalo.zocalo_results import (
40
41
  ZOCALO_READING_PLAN_NAME,
41
42
  ZOCALO_STAGE_GROUP,
43
+ XrcResult,
42
44
  ZocaloResults,
43
- get_processing_result,
45
+ get_full_processing_results,
44
46
  )
45
- from dodal.plans.check_topup import check_topup_and_wait_if_necessary
47
+ from event_model import RunStart
46
48
  from ophyd_async.fastcs.panda import HDFPanda
47
- from scanspec.core import AxesPoints, Axis
48
49
 
49
- from mx_bluesky.hyperion.device_setup_plans.manipulate_sample import move_x_y_z
50
+ from mx_bluesky.common.plans.do_fgs import kickoff_and_complete_gridscan
51
+ from mx_bluesky.common.utils.tracing import TRACER
50
52
  from mx_bluesky.hyperion.device_setup_plans.read_hardware_for_setup import (
51
53
  read_hardware_during_collection,
52
- read_hardware_for_zocalo,
53
54
  read_hardware_pre_collection,
54
55
  )
55
56
  from mx_bluesky.hyperion.device_setup_plans.setup_panda import (
@@ -65,11 +66,17 @@ from mx_bluesky.hyperion.device_setup_plans.setup_zebra import (
65
66
  from mx_bluesky.hyperion.device_setup_plans.xbpm_feedback import (
66
67
  transmission_and_xbpm_feedback_for_collection_decorator,
67
68
  )
68
- from mx_bluesky.hyperion.exceptions import WarningException
69
+ from mx_bluesky.hyperion.exceptions import CrystalNotFoundException, SampleException
70
+ from mx_bluesky.hyperion.experiment_plans.change_aperture_then_move_plan import (
71
+ change_aperture_then_move_to_xtal,
72
+ )
73
+ from mx_bluesky.hyperion.experiment_plans.common.xrc_result import XRayCentreResult
74
+ from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import (
75
+ ispyb_activation_wrapper,
76
+ )
69
77
  from mx_bluesky.hyperion.log import LOGGER
70
78
  from mx_bluesky.hyperion.parameters.constants import CONST
71
- from mx_bluesky.hyperion.parameters.gridscan import ThreeDGridScan
72
- from mx_bluesky.hyperion.tracing import TRACER
79
+ from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan
73
80
  from mx_bluesky.hyperion.utils.context import device_composite_from_context
74
81
 
75
82
 
@@ -77,13 +84,7 @@ class SmargonSpeedException(Exception):
77
84
  pass
78
85
 
79
86
 
80
- class CrystalNotFoundException(WarningException):
81
- """Raised if grid detection completed normally but no crystal was found."""
82
-
83
- pass
84
-
85
-
86
- @dataclasses.dataclass
87
+ @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
87
88
  class FlyScanXRayCentreComposite:
88
89
  """All devices which are directly or indirectly required by this plan"""
89
90
 
@@ -112,30 +113,33 @@ class FlyScanXRayCentreComposite:
112
113
  return self.smargon
113
114
 
114
115
 
116
+ class XRayCentreEventHandler(CallbackBase):
117
+ def __init__(self):
118
+ super().__init__()
119
+ self.xray_centre_results: Sequence[XRayCentreResult] | None = None
120
+
121
+ def start(self, doc: RunStart) -> RunStart | None:
122
+ if "xray_centre_results" in doc:
123
+ self.xray_centre_results = [
124
+ XRayCentreResult(**result_dict)
125
+ for result_dict in doc["xray_centre_results"] # type: ignore
126
+ ]
127
+ return doc
128
+
129
+
115
130
  def create_devices(context: BlueskyContext) -> FlyScanXRayCentreComposite:
116
131
  """Creates the devices required for the plan and connect to them"""
117
132
  return device_composite_from_context(context, FlyScanXRayCentreComposite)
118
133
 
119
134
 
120
- def flyscan_xray_centre(
121
- composite: FlyScanXRayCentreComposite,
122
- parameters: ThreeDGridScan,
135
+ def flyscan_xray_centre_no_move(
136
+ composite: FlyScanXRayCentreComposite, parameters: HyperionThreeDGridScan
123
137
  ) -> MsgGenerator:
124
- """Create the plan to run the grid scan based on provided parameters.
125
-
126
- The ispyb handler should be added to the whole gridscan as we want to capture errors
127
- at any point in it.
128
-
129
- Args:
130
- parameters (ThreeDGridScan): The parameters to run the scan.
131
-
132
- Returns:
133
- Generator: The plan for the gridscan
134
- """
138
+ """Perform a flyscan and determine the centres of interest"""
135
139
  parameters.features.update_self_from_server()
136
140
  composite.eiger.set_detector_parameters(parameters.detector_params)
137
- composite.zocalo.zocalo_environment = parameters.zocalo_environment
138
- composite.zocalo.use_cpu_and_gpu = parameters.use_cpu_and_gpu_zocalo
141
+ composite.zocalo.zocalo_environment = CONST.ZOCALO_ENV
142
+ composite.zocalo.use_cpu_and_gpu = parameters.features.compare_cpu_and_gpu_zocalo
139
143
 
140
144
  feature_controlled = _get_feature_controlled(composite, parameters)
141
145
 
@@ -143,8 +147,6 @@ def flyscan_xray_centre(
143
147
  @bpp.run_decorator( # attach experiment metadata to the start document
144
148
  md={
145
149
  "subplan_name": CONST.PLAN.GRIDSCAN_OUTER,
146
- CONST.TRIGGER.ZOCALO: CONST.PLAN.DO_FGS,
147
- "zocalo_environment": parameters.zocalo_environment,
148
150
  "hyperion_parameters": parameters.model_dump_json(),
149
151
  "activate_callbacks": [
150
152
  "GridscanNexusFileCallback",
@@ -157,25 +159,66 @@ def flyscan_xray_centre(
157
159
  composite.attenuator,
158
160
  parameters.transmission_frac,
159
161
  )
160
- def run_gridscan_and_move_and_tidy(
162
+ def run_gridscan_and_fetch_and_tidy(
161
163
  fgs_composite: FlyScanXRayCentreComposite,
162
- params: ThreeDGridScan,
164
+ params: HyperionThreeDGridScan,
163
165
  feature_controlled: _FeatureControlled,
164
- ):
165
- yield from run_gridscan_and_move(fgs_composite, params, feature_controlled)
166
+ ) -> MsgGenerator:
167
+ yield from run_gridscan_and_fetch_results(
168
+ fgs_composite, params, feature_controlled
169
+ )
170
+
171
+ yield from run_gridscan_and_fetch_and_tidy(
172
+ composite, parameters, feature_controlled
173
+ )
174
+
175
+
176
+ def flyscan_xray_centre(
177
+ composite: FlyScanXRayCentreComposite,
178
+ parameters: HyperionThreeDGridScan,
179
+ ) -> MsgGenerator:
180
+ """Create the plan to run the grid scan based on provided parameters.
181
+
182
+ The ispyb handler should be added to the whole gridscan as we want to capture errors
183
+ at any point in it.
184
+
185
+ Args:
186
+ parameters (ThreeDGridScan): The parameters to run the scan.
187
+
188
+ Returns:
189
+ Generator: The plan for the gridscan
190
+ """
191
+ xrc_event_handler = XRayCentreEventHandler()
192
+
193
+ @bpp.subs_decorator(xrc_event_handler)
194
+ def flyscan_and_fetch_results() -> MsgGenerator:
195
+ yield from ispyb_activation_wrapper(
196
+ flyscan_xray_centre_no_move(composite, parameters), parameters
197
+ )
166
198
 
167
- return run_gridscan_and_move_and_tidy(composite, parameters, feature_controlled)
199
+ yield from flyscan_and_fetch_results()
200
+
201
+ xray_centre_results = xrc_event_handler.xray_centre_results
202
+ assert (
203
+ xray_centre_results
204
+ ), "Flyscan result event not received or no crystal found and exception not raised"
205
+ yield from change_aperture_then_move_to_xtal(
206
+ xray_centre_results[0],
207
+ composite.smargon,
208
+ composite.aperture_scatterguard,
209
+ parameters,
210
+ )
168
211
 
169
212
 
170
213
  @bpp.set_run_key_decorator(CONST.PLAN.GRIDSCAN_AND_MOVE)
171
214
  @bpp.run_decorator(md={"subplan_name": CONST.PLAN.GRIDSCAN_AND_MOVE})
172
- def run_gridscan_and_move(
215
+ def run_gridscan_and_fetch_results(
173
216
  fgs_composite: FlyScanXRayCentreComposite,
174
- parameters: ThreeDGridScan,
217
+ parameters: HyperionThreeDGridScan,
175
218
  feature_controlled: _FeatureControlled,
176
219
  ) -> MsgGenerator:
177
220
  """A multi-run plan which runs a gridscan, gets the results from zocalo
178
- and moves to the centre of mass determined by zocalo"""
221
+ and fires an event with the centres of mass determined by zocalo"""
179
222
 
180
223
  # We get the initial motor positions so we can return to them on zocalo failure
181
224
  initial_xyz = np.array(
@@ -202,51 +245,66 @@ def run_gridscan_and_move(
202
245
  [fgs_composite.zocalo], name=ZOCALO_READING_PLAN_NAME
203
246
  )
204
247
  LOGGER.info("Zocalo triggered and read, interpreting results.")
205
- xray_centre, bbox_size = yield from get_processing_result(
206
- fgs_composite.zocalo
207
- )
208
- LOGGER.info(f"Got xray centre: {xray_centre}, bbox size: {bbox_size}")
209
- if xray_centre is not None:
210
- xray_centre = parameters.FGS_params.grid_position_to_motor_position(
211
- xray_centre
212
- )
248
+ xrc_results = yield from get_full_processing_results(fgs_composite.zocalo)
249
+ LOGGER.info(f"Got xray centres, top 5: {xrc_results[:5]}")
250
+ if xrc_results:
251
+ flyscan_results = [
252
+ _xrc_result_in_boxes_to_result_in_mm(xr, parameters)
253
+ for xr in xrc_results
254
+ ]
213
255
  else:
214
256
  LOGGER.warning("No X-ray centre received")
215
257
  raise CrystalNotFoundException()
216
- if bbox_size is not None:
217
- with TRACER.start_span("change_aperture"):
218
- yield from set_aperture_for_bbox_size(
219
- fgs_composite.aperture_scatterguard, bbox_size
220
- )
221
- else:
222
- LOGGER.warning("No bounding box size received")
223
-
224
- # once we have the results, go to the appropriate position
225
- LOGGER.info("Moving to centre of mass.")
226
- with TRACER.start_span("move_to_result"):
227
- x, y, z = xray_centre
228
- yield from move_x_y_z(fgs_composite.sample_motors, x, y, z, wait=True)
229
-
230
- if parameters.FGS_params.set_stub_offsets:
231
- LOGGER.info("Recentring smargon co-ordinate system to this point.")
232
- yield from bps.mv(
233
- fgs_composite.sample_motors.stub_offsets, StubPosition.CURRENT_AS_CENTER
234
- )
258
+ yield from _fire_xray_centre_result_event(flyscan_results)
259
+
235
260
  finally:
236
261
  # Turn off dev/shm streaming to avoid filling disk, see https://github.com/DiamondLightSource/hyperion/issues/1395
237
262
  LOGGER.info("Turning off Eiger dev/shm streaming")
238
- yield from bps.abs_set(fgs_composite.eiger.odin.fan.dev_shm_enable, 0)
263
+ yield from bps.abs_set(fgs_composite.eiger.odin.fan.dev_shm_enable, 0) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
239
264
 
240
265
  # Wait on everything before returning to GDA (particularly apertures), can be removed
241
266
  # when we do not return to GDA here
242
267
  yield from bps.wait()
243
268
 
244
269
 
270
+ def _xrc_result_in_boxes_to_result_in_mm(
271
+ xrc_result: XrcResult, parameters: HyperionThreeDGridScan
272
+ ) -> XRayCentreResult:
273
+ fgs_params = parameters.FGS_params
274
+ xray_centre = fgs_params.grid_position_to_motor_position(
275
+ np.array(xrc_result["centre_of_mass"])
276
+ )
277
+ return XRayCentreResult(
278
+ centre_of_mass_mm=xray_centre,
279
+ bounding_box_mm=(
280
+ fgs_params.grid_position_to_motor_position(
281
+ np.array(xrc_result["bounding_box"][0])
282
+ ),
283
+ fgs_params.grid_position_to_motor_position(
284
+ np.array(xrc_result["bounding_box"][1])
285
+ ),
286
+ ),
287
+ max_count=xrc_result["max_count"],
288
+ total_count=xrc_result["total_count"],
289
+ )
290
+
291
+
292
+ @bpp.set_run_key_decorator(CONST.PLAN.FLYSCAN_RESULTS)
293
+ def _fire_xray_centre_result_event(results: Sequence[XRayCentreResult]):
294
+ def empty_plan():
295
+ return iter([])
296
+
297
+ yield from bpp.run_wrapper(
298
+ empty_plan(),
299
+ md={"xray_centre_results": [dataclasses.asdict(r) for r in results]},
300
+ )
301
+
302
+
245
303
  @bpp.set_run_key_decorator(CONST.PLAN.GRIDSCAN_MAIN)
246
304
  @bpp.run_decorator(md={"subplan_name": CONST.PLAN.GRIDSCAN_MAIN})
247
305
  def run_gridscan(
248
306
  fgs_composite: FlyScanXRayCentreComposite,
249
- parameters: ThreeDGridScan,
307
+ parameters: HyperionThreeDGridScan,
250
308
  feature_controlled: _FeatureControlled,
251
309
  md={ # noqa
252
310
  "plan_name": CONST.PLAN.GRIDSCAN_MAIN,
@@ -266,7 +324,7 @@ def run_gridscan(
266
324
  fgs_composite.undulator,
267
325
  fgs_composite.synchrotron,
268
326
  fgs_composite.s4_slit_gaps,
269
- fgs_composite.robot,
327
+ fgs_composite.dcm,
270
328
  fgs_composite.smargon,
271
329
  )
272
330
 
@@ -287,7 +345,7 @@ def run_gridscan(
287
345
 
288
346
  LOGGER.info("Waiting for arming to finish")
289
347
  yield from bps.wait(CONST.WAIT.GRID_READY_FOR_DC)
290
- yield from bps.stage(fgs_composite.eiger)
348
+ yield from bps.stage(fgs_composite.eiger) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
291
349
 
292
350
  yield from kickoff_and_complete_gridscan(
293
351
  feature_controlled.fgs_motors,
@@ -295,66 +353,11 @@ def run_gridscan(
295
353
  fgs_composite.synchrotron,
296
354
  [parameters.scan_points_first_grid, parameters.scan_points_second_grid],
297
355
  parameters.scan_indices,
298
- do_during_run=read_during_collection,
356
+ plan_during_collection=read_during_collection,
299
357
  )
300
358
  yield from bps.abs_set(feature_controlled.fgs_motors.z_steps, 0, wait=False)
301
359
 
302
360
 
303
- def kickoff_and_complete_gridscan(
304
- gridscan: FastGridScanCommon,
305
- eiger: EigerDetector,
306
- synchrotron: Synchrotron,
307
- scan_points: list[AxesPoints[Axis]],
308
- scan_start_indices: list[int],
309
- do_during_run: Callable[[], MsgGenerator] | None = None,
310
- ):
311
- @TRACER.start_as_current_span(CONST.PLAN.DO_FGS)
312
- @bpp.set_run_key_decorator(CONST.PLAN.DO_FGS)
313
- @bpp.run_decorator(
314
- md={
315
- "subplan_name": CONST.PLAN.DO_FGS,
316
- "scan_points": scan_points,
317
- "scan_start_indices": scan_start_indices,
318
- }
319
- )
320
- @bpp.contingency_decorator(
321
- except_plan=lambda e: (yield from bps.stop(eiger)),
322
- else_plan=lambda: (yield from bps.unstage(eiger)),
323
- )
324
- def do_fgs():
325
- # Check topup gate
326
- expected_images = yield from bps.rd(gridscan.expected_images)
327
- exposure_sec_per_image = yield from bps.rd(eiger.cam.acquire_time)
328
- LOGGER.info("waiting for topup if necessary...")
329
- yield from check_topup_and_wait_if_necessary(
330
- synchrotron,
331
- expected_images * exposure_sec_per_image,
332
- 30.0,
333
- )
334
- yield from read_hardware_for_zocalo(eiger)
335
- LOGGER.info("Wait for all moves with no assigned group")
336
- yield from bps.wait()
337
- LOGGER.info("kicking off FGS")
338
- yield from bps.kickoff(gridscan, wait=True)
339
- gridscan_start_time = time()
340
- LOGGER.info("Waiting for Zocalo device queue to have been cleared...")
341
- yield from bps.wait(
342
- ZOCALO_STAGE_GROUP
343
- ) # Make sure ZocaloResults queue is clear and ready to accept our new data
344
- if do_during_run:
345
- LOGGER.info(f"Running {do_during_run} during FGS")
346
- yield from do_during_run()
347
- LOGGER.info("completing FGS")
348
- yield from bps.complete(gridscan, wait=True)
349
-
350
- # Remove this logging statement once metrics have been added
351
- LOGGER.info(
352
- f"Gridscan motion program took {round(time()-gridscan_start_time,2)} to complete"
353
- )
354
-
355
- yield from do_fgs()
356
-
357
-
358
361
  def wait_for_gridscan_valid(fgs_motors: FastGridScanCommon, timeout=0.5):
359
362
  LOGGER.info("Waiting for valid fgs_params")
360
363
  SLEEP_PER_CHECK = 0.1
@@ -369,32 +372,7 @@ def wait_for_gridscan_valid(fgs_motors: FastGridScanCommon, timeout=0.5):
369
372
  LOGGER.info("Gridscan scan valid and position counter reset")
370
373
  return
371
374
  yield from bps.sleep(SLEEP_PER_CHECK)
372
- raise WarningException("Scan invalid - pin too long/short/bent and out of range")
373
-
374
-
375
- def set_aperture_for_bbox_size(
376
- aperture_device: ApertureScatterguard,
377
- bbox_size: list[int] | np.ndarray,
378
- ):
379
- # bbox_size is [x,y,z], for i03 we only care about x
380
- new_selected_aperture = (
381
- ApertureValue.MEDIUM if bbox_size[0] < 2 else ApertureValue.LARGE
382
- )
383
- LOGGER.info(
384
- f"Setting aperture to {new_selected_aperture} based on bounding box size {bbox_size}."
385
- )
386
-
387
- @bpp.set_run_key_decorator("change_aperture")
388
- @bpp.run_decorator(
389
- md={
390
- "subplan_name": "change_aperture",
391
- "aperture_size": new_selected_aperture.value,
392
- }
393
- )
394
- def set_aperture():
395
- yield from bps.abs_set(aperture_device, new_selected_aperture)
396
-
397
- yield from set_aperture()
375
+ raise SampleException("Scan invalid - pin too long/short/bent and out of range")
398
376
 
399
377
 
400
378
  @dataclasses.dataclass
@@ -408,7 +386,7 @@ class _FeatureControlled:
408
386
  def __call__(
409
387
  self,
410
388
  fgs_composite: FlyScanXRayCentreComposite,
411
- parameters: ThreeDGridScan,
389
+ parameters: HyperionThreeDGridScan,
412
390
  initial_xyz: np.ndarray,
413
391
  ) -> MsgGenerator: ...
414
392
 
@@ -420,7 +398,7 @@ class _FeatureControlled:
420
398
 
421
399
  def _get_feature_controlled(
422
400
  fgs_composite: FlyScanXRayCentreComposite,
423
- parameters: ThreeDGridScan,
401
+ parameters: HyperionThreeDGridScan,
424
402
  ):
425
403
  if parameters.features.use_panda_for_gridscan:
426
404
  return _FeatureControlled(
@@ -469,7 +447,7 @@ def _panda_tidy(fgs_composite: FlyScanXRayCentreComposite):
469
447
 
470
448
  def _zebra_triggering_setup(
471
449
  fgs_composite: FlyScanXRayCentreComposite,
472
- parameters: ThreeDGridScan,
450
+ parameters: HyperionThreeDGridScan,
473
451
  initial_xyz: np.ndarray,
474
452
  ):
475
453
  yield from setup_zebra_for_gridscan(
@@ -479,7 +457,7 @@ def _zebra_triggering_setup(
479
457
 
480
458
  def _panda_triggering_setup(
481
459
  fgs_composite: FlyScanXRayCentreComposite,
482
- parameters: ThreeDGridScan,
460
+ parameters: HyperionThreeDGridScan,
483
461
  initial_xyz: np.ndarray,
484
462
  ):
485
463
  LOGGER.info("Setting up Panda for flyscan")
@@ -498,15 +476,15 @@ def _panda_triggering_setup(
498
476
  )
499
477
 
500
478
  sample_velocity_mm_per_s = (
501
- parameters.panda_FGS_params.x_step_size * 1e3 / time_between_x_steps_ms
479
+ parameters.panda_FGS_params.x_step_size_mm * 1e3 / time_between_x_steps_ms
502
480
  )
503
481
  if sample_velocity_mm_per_s > smargon_speed_limit_mm_per_s:
504
482
  raise SmargonSpeedException(
505
483
  f"Smargon speed was calculated from x step size\
506
- {parameters.panda_FGS_params.x_step_size} and\
484
+ {parameters.panda_FGS_params.x_step_size_mm}mm and\
507
485
  time_between_x_steps_ms {time_between_x_steps_ms} as\
508
- {sample_velocity_mm_per_s}. The smargon's speed limit is\
509
- {smargon_speed_limit_mm_per_s} mm/s."
486
+ {sample_velocity_mm_per_s}mm/s. The smargon's speed limit is\
487
+ {smargon_speed_limit_mm_per_s}mm/s."
510
488
  )
511
489
  else:
512
490
  LOGGER.info(
@@ -515,8 +493,8 @@ def _panda_triggering_setup(
515
493
  )
516
494
 
517
495
  yield from bps.mv(
518
- fgs_composite.panda_fast_grid_scan.time_between_x_steps_ms,
519
- time_between_x_steps_ms,
496
+ fgs_composite.panda_fast_grid_scan.time_between_x_steps_ms, # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
497
+ time_between_x_steps_ms, # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
520
498
  )
521
499
 
522
500
  directory_provider_root = Path(parameters.storage_directory)
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- import dataclasses
4
3
  from pathlib import Path
5
4
 
6
- from blueapi.core import BlueskyContext, MsgGenerator
5
+ import pydantic
6
+ from blueapi.core import BlueskyContext
7
7
  from bluesky import plan_stubs as bps
8
8
  from bluesky import preprocessors as bpp
9
+ from bluesky.preprocessors import subs_decorator
10
+ from bluesky.utils import MsgGenerator
9
11
  from dodal.devices.aperturescatterguard import ApertureScatterguard
10
12
  from dodal.devices.attenuator import Attenuator
11
13
  from dodal.devices.backlight import Backlight, BacklightPosition
@@ -15,7 +17,7 @@ from dodal.devices.eiger import EigerDetector
15
17
  from dodal.devices.fast_grid_scan import PandAFastGridScan, ZebraFastGridScan
16
18
  from dodal.devices.flux import Flux
17
19
  from dodal.devices.oav.oav_detector import OAV
18
- from dodal.devices.oav.oav_parameters import OAV_CONFIG_JSON, OAVParameters
20
+ from dodal.devices.oav.oav_parameters import OAVParameters
19
21
  from dodal.devices.oav.pin_image_recognition import PinTipDetection
20
22
  from dodal.devices.robot import BartRobot
21
23
  from dodal.devices.s4_slit_gaps import S4SlitGaps
@@ -28,17 +30,23 @@ from dodal.devices.zebra_controlled_shutter import ZebraShutter
28
30
  from dodal.devices.zocalo import ZocaloResults
29
31
  from ophyd_async.fastcs.panda import HDFPanda
30
32
 
33
+ from mx_bluesky.common.parameters.constants import OavConstants
34
+ from mx_bluesky.common.parameters.gridscan import GridScanWithEdgeDetect
31
35
  from mx_bluesky.hyperion.device_setup_plans.manipulate_sample import (
32
36
  move_aperture_if_required,
33
37
  )
34
38
  from mx_bluesky.hyperion.device_setup_plans.utils import (
35
39
  start_preparing_data_collection_then_do_plan,
36
40
  )
41
+ from mx_bluesky.hyperion.experiment_plans.change_aperture_then_move_plan import (
42
+ change_aperture_then_move_to_xtal,
43
+ )
37
44
  from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import (
38
45
  FlyScanXRayCentreComposite as FlyScanXRayCentreComposite,
39
46
  )
40
47
  from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import (
41
- flyscan_xray_centre,
48
+ XRayCentreEventHandler,
49
+ flyscan_xray_centre_no_move,
42
50
  )
43
51
  from mx_bluesky.hyperion.experiment_plans.oav_grid_detection_plan import (
44
52
  OavGridDetectionComposite,
@@ -54,13 +62,12 @@ from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callba
54
62
  from mx_bluesky.hyperion.log import LOGGER
55
63
  from mx_bluesky.hyperion.parameters.constants import CONST
56
64
  from mx_bluesky.hyperion.parameters.gridscan import (
57
- GridScanWithEdgeDetect,
58
- ThreeDGridScan,
65
+ HyperionThreeDGridScan,
59
66
  )
60
67
  from mx_bluesky.hyperion.utils.context import device_composite_from_context
61
68
 
62
69
 
63
- @dataclasses.dataclass
70
+ @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
64
71
  class GridDetectThenXRayCentreComposite:
65
72
  """All devices which are directly or indirectly required by this plan"""
66
73
 
@@ -86,6 +93,10 @@ class GridDetectThenXRayCentreComposite:
86
93
  robot: BartRobot
87
94
  sample_shutter: ZebraShutter
88
95
 
96
+ @property
97
+ def sample_motors(self):
98
+ return self.smargon
99
+
89
100
 
90
101
  def create_devices(context: BlueskyContext) -> GridDetectThenXRayCentreComposite:
91
102
  return device_composite_from_context(context, GridDetectThenXRayCentreComposite)
@@ -94,10 +105,10 @@ def create_devices(context: BlueskyContext) -> GridDetectThenXRayCentreComposite
94
105
  def create_parameters_for_flyscan_xray_centre(
95
106
  grid_scan_with_edge_params: GridScanWithEdgeDetect,
96
107
  grid_parameters: GridParamUpdate,
97
- ) -> ThreeDGridScan:
108
+ ) -> HyperionThreeDGridScan:
98
109
  params_json = grid_scan_with_edge_params.model_dump()
99
110
  params_json.update(grid_parameters)
100
- flyscan_xray_centre_parameters = ThreeDGridScan(**params_json)
111
+ flyscan_xray_centre_parameters = HyperionThreeDGridScan(**params_json)
101
112
  LOGGER.info(f"Parameters for FGS: {flyscan_xray_centre_parameters}")
102
113
  return flyscan_xray_centre_parameters
103
114
 
@@ -109,7 +120,7 @@ def detect_grid_and_do_gridscan(
109
120
  ):
110
121
  snapshot_template = f"{parameters.detector_params.prefix}_{parameters.detector_params.run_number}_{{angle}}"
111
122
 
112
- grid_params_callback = GridDetectionCallback(composite.oav.parameters)
123
+ grid_params_callback = GridDetectionCallback()
113
124
 
114
125
  @bpp.subs_decorator([grid_params_callback])
115
126
  def run_grid_detection_plan(
@@ -129,7 +140,8 @@ def detect_grid_and_do_gridscan(
129
140
  oav_params,
130
141
  snapshot_template,
131
142
  str(snapshot_dir),
132
- grid_width_microns=parameters.grid_width_um,
143
+ parameters.grid_width_um,
144
+ parameters.box_size_um,
133
145
  )
134
146
 
135
147
  yield from run_grid_detection_plan(
@@ -148,7 +160,7 @@ def detect_grid_and_do_gridscan(
148
160
  group=CONST.WAIT.GRID_READY_FOR_DC,
149
161
  )
150
162
 
151
- yield from flyscan_xray_centre(
163
+ yield from flyscan_xray_centre_no_move(
152
164
  FlyScanXRayCentreComposite(
153
165
  aperture_scatterguard=composite.aperture_scatterguard,
154
166
  attenuator=composite.attenuator,
@@ -178,7 +190,7 @@ def detect_grid_and_do_gridscan(
178
190
  def grid_detect_then_xray_centre(
179
191
  composite: GridDetectThenXRayCentreComposite,
180
192
  parameters: GridScanWithEdgeDetect,
181
- oav_config: str = OAV_CONFIG_JSON,
193
+ oav_config: str = OavConstants.OAV_CONFIG_JSON,
182
194
  ) -> MsgGenerator:
183
195
  """
184
196
  A plan which combines the collection of snapshots from the OAV and the determination
@@ -191,19 +203,33 @@ def grid_detect_then_xray_centre(
191
203
 
192
204
  oav_params = OAVParameters("xrayCentring", oav_config)
193
205
 
194
- plan_to_perform = ispyb_activation_wrapper(
195
- detect_grid_and_do_gridscan(
196
- composite,
206
+ flyscan_event_handler = XRayCentreEventHandler()
207
+
208
+ @subs_decorator(flyscan_event_handler)
209
+ def plan_to_perform():
210
+ yield from ispyb_activation_wrapper(
211
+ detect_grid_and_do_gridscan(
212
+ composite,
213
+ parameters,
214
+ oav_params,
215
+ ),
197
216
  parameters,
198
- oav_params,
199
- ),
200
- parameters,
201
- )
217
+ )
202
218
 
203
- return start_preparing_data_collection_then_do_plan(
219
+ yield from start_preparing_data_collection_then_do_plan(
204
220
  eiger,
205
221
  composite.detector_motion,
206
222
  parameters.detector_params.detector_distance,
207
- plan_to_perform,
223
+ plan_to_perform(),
208
224
  group=CONST.WAIT.GRID_READY_FOR_DC,
209
225
  )
226
+
227
+ assert (
228
+ flyscan_event_handler.xray_centre_results
229
+ ), "Flyscan result event not received or no crystal found and exception not raised"
230
+
231
+ yield from change_aperture_then_move_to_xtal(
232
+ flyscan_event_handler.xray_centre_results[0],
233
+ composite.smargon,
234
+ composite.aperture_scatterguard,
235
+ )