mx-bluesky 1.5.5__py3-none-any.whl → 1.5.7__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 (38) hide show
  1. mx_bluesky/_version.py +2 -2
  2. mx_bluesky/beamlines/i02_1/parameters/__init__.py +0 -0
  3. mx_bluesky/beamlines/i02_1/parameters/gridscan.py +35 -0
  4. mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +6 -3
  5. mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +3 -1
  6. mx_bluesky/beamlines/i04/thawing_plan.py +15 -8
  7. mx_bluesky/beamlines/i24/jungfrau_commissioning/do_external_acquisition.py +44 -0
  8. mx_bluesky/beamlines/i24/jungfrau_commissioning/do_internal_acquisition.py +46 -0
  9. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_utils.py +73 -0
  10. mx_bluesky/common/device_setup_plans/robot_load_unload.py +2 -1
  11. mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +29 -5
  12. mx_bluesky/common/experiment_plans/inner_plans/do_fgs.py +7 -8
  13. mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +6 -6
  14. mx_bluesky/common/external_interaction/callbacks/common/zocalo_callback.py +30 -22
  15. mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py +73 -15
  16. mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_mapping.py +0 -20
  17. mx_bluesky/common/parameters/components.py +1 -0
  18. mx_bluesky/common/parameters/device_composites.py +2 -2
  19. mx_bluesky/common/parameters/gridscan.py +67 -49
  20. mx_bluesky/hyperion/__main__.py +16 -3
  21. mx_bluesky/hyperion/baton_handler.py +39 -9
  22. mx_bluesky/hyperion/device_setup_plans/smargon.py +13 -8
  23. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +19 -8
  24. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +2 -2
  25. mx_bluesky/hyperion/external_interaction/agamemnon.py +6 -2
  26. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +10 -2
  27. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +37 -1
  28. mx_bluesky/hyperion/parameters/constants.py +1 -0
  29. mx_bluesky/hyperion/parameters/device_composites.py +2 -2
  30. mx_bluesky/hyperion/parameters/gridscan.py +3 -3
  31. mx_bluesky/hyperion/plan_runner.py +2 -4
  32. mx_bluesky/hyperion/plan_runner_api.py +43 -0
  33. {mx_bluesky-1.5.5.dist-info → mx_bluesky-1.5.7.dist-info}/METADATA +2 -2
  34. {mx_bluesky-1.5.5.dist-info → mx_bluesky-1.5.7.dist-info}/RECORD +38 -32
  35. {mx_bluesky-1.5.5.dist-info → mx_bluesky-1.5.7.dist-info}/WHEEL +0 -0
  36. {mx_bluesky-1.5.5.dist-info → mx_bluesky-1.5.7.dist-info}/entry_points.txt +0 -0
  37. {mx_bluesky-1.5.5.dist-info → mx_bluesky-1.5.7.dist-info}/licenses/LICENSE +0 -0
  38. {mx_bluesky-1.5.5.dist-info → mx_bluesky-1.5.7.dist-info}/top_level.txt +0 -0
mx_bluesky/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.5.5'
32
- __version_tuple__ = version_tuple = (1, 5, 5)
31
+ __version__ = version = '1.5.7'
32
+ __version_tuple__ = version_tuple = (1, 5, 7)
33
33
 
34
34
  __commit_id__ = commit_id = None
File without changes
@@ -0,0 +1,35 @@
1
+ from dodal.devices.i02_1.fast_grid_scan import ZebraGridScanParamsTwoD
2
+ from scanspec.specs import Product
3
+
4
+ from mx_bluesky.common.parameters.components import SplitScan, WithOptionalEnergyChange
5
+ from mx_bluesky.common.parameters.gridscan import SpecifiedGrid
6
+
7
+
8
+ class SpecifiedTwoDGridScan(
9
+ SpecifiedGrid[ZebraGridScanParamsTwoD],
10
+ SplitScan,
11
+ WithOptionalEnergyChange,
12
+ ):
13
+ """Parameters representing a so-called 2D grid scan, which consists of doing a
14
+ gridscan in X and Y."""
15
+
16
+ @property
17
+ def scan_spec(self) -> Product[str]:
18
+ """A fully specified ScanSpec object representing the grid, with x, y, z and
19
+ omega positions."""
20
+ return self.grid_1_spec
21
+
22
+ @property
23
+ def FGS_params(self) -> ZebraGridScanParamsTwoD:
24
+ return ZebraGridScanParamsTwoD(
25
+ x_steps=self.x_steps,
26
+ y_steps=self.y_steps,
27
+ x_step_size_mm=self.x_step_size_um / 1000,
28
+ y_step_size_mm=self.y_step_size_um / 1000,
29
+ x_start_mm=self.x_start_um / 1000,
30
+ y1_start_mm=self.y_start_um / 1000,
31
+ z1_start_mm=self.z_start_um / 1000,
32
+ set_stub_offsets=self._set_stub_offsets,
33
+ transmission_fraction=0.5,
34
+ dwell_time_ms=self.exposure_time_s,
35
+ )
@@ -13,7 +13,7 @@ from dodal.devices.common_dcm import BaseDCM
13
13
  from dodal.devices.detector.detector_motion import DetectorMotion
14
14
  from dodal.devices.eiger import EigerDetector
15
15
  from dodal.devices.fast_grid_scan import (
16
- ZebraFastGridScan,
16
+ ZebraFastGridScanThreeD,
17
17
  set_fast_grid_scan_params,
18
18
  )
19
19
  from dodal.devices.flux import Flux
@@ -48,6 +48,7 @@ from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback imp
48
48
  )
49
49
  from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import (
50
50
  GridscanISPyBCallback,
51
+ generate_start_info_from_omega_map,
51
52
  )
52
53
  from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import (
53
54
  GridscanNexusFileCallback,
@@ -80,7 +81,7 @@ def i04_grid_detect_then_xray_centre(
80
81
  backlight: Backlight = inject("backlight"),
81
82
  beamstop: Beamstop = inject("beamstop"),
82
83
  dcm: BaseDCM = inject("dcm"),
83
- zebra_fast_grid_scan: ZebraFastGridScan = inject("zebra_fast_grid_scan"),
84
+ zebra_fast_grid_scan: ZebraFastGridScanThreeD = inject("zebra_fast_grid_scan"),
84
85
  flux: Flux = inject("flux"),
85
86
  oav: OAV = inject("oav"),
86
87
  pin_tip_detection: PinTipDetection = inject("pin_tip_detection"),
@@ -198,7 +199,9 @@ def create_gridscan_callbacks() -> tuple[
198
199
  GridscanISPyBCallback(
199
200
  param_type=GridCommon,
200
201
  emit=ZocaloCallback(
201
- PlanNameConstants.DO_FGS, EnvironmentConstants.ZOCALO_ENV
202
+ PlanNameConstants.DO_FGS,
203
+ EnvironmentConstants.ZOCALO_ENV,
204
+ generate_start_info_from_omega_map,
202
205
  ),
203
206
  ),
204
207
  )
@@ -7,7 +7,7 @@ from typing import TypedDict
7
7
  import numpy as np
8
8
  import zmq
9
9
  from dodal.devices.i04.constants import RedisConstants
10
- from dodal.devices.i04.murko_results import FullMurkoResults, MurkoResult
10
+ from dodal.devices.i04.murko_results import MurkoResult
11
11
  from numpy.typing import NDArray
12
12
  from PIL import Image
13
13
  from redis import StrictRedis
@@ -16,6 +16,8 @@ from mx_bluesky.common.utils.log import LOGGER
16
16
 
17
17
  MURKO_ADDRESS = "tcp://i04-murko-prod.diamond.ac.uk:8008"
18
18
 
19
+ FullMurkoResults = dict[str, list[MurkoResult]]
20
+
19
21
 
20
22
  class MurkoRequest(TypedDict):
21
23
  """See https://github.com/MartinSavko/murko#usage for more information."""
@@ -1,8 +1,9 @@
1
1
  from collections.abc import Callable
2
+ from functools import partial
2
3
 
3
4
  import bluesky.plan_stubs as bps
4
5
  import bluesky.preprocessors as bpp
5
- from bluesky.preprocessors import run_decorator, stage_wrapper, subs_decorator
6
+ from bluesky.preprocessors import run_decorator, subs_decorator
6
7
  from bluesky.utils import MsgGenerator
7
8
  from dodal.common import inject
8
9
  from dodal.devices.i04.constants import RedisConstants
@@ -42,7 +43,7 @@ def thaw_and_stream_to_redis(
42
43
  robot: BartRobot = inject("robot"),
43
44
  thawer: Thawer = inject("thawer"),
44
45
  smargon: Smargon = inject("smargon"),
45
- oav: OAV = inject("oav"),
46
+ oav: OAV = inject("oav_full_screen"),
46
47
  oav_to_redis_forwarder: OAVToRedisForwarder = inject("oav_to_redis_forwarder"),
47
48
  ) -> MsgGenerator:
48
49
  """Turns on the thawer and rotates the sample by {rotation} degrees to thaw it, then
@@ -84,7 +85,7 @@ def thaw_and_murko_centre(
84
85
  robot: BartRobot = inject("robot"),
85
86
  thawer: Thawer = inject("thawer"),
86
87
  smargon: Smargon = inject("smargon"),
87
- oav: OAV = inject("oav"),
88
+ oav: OAV = inject("oav_full_screen"),
88
89
  murko_results: MurkoResultsDevice = inject("murko_results"),
89
90
  oav_to_redis_forwarder: OAVToRedisForwarder = inject("oav_to_redis_forwarder"),
90
91
  ) -> MsgGenerator:
@@ -109,14 +110,14 @@ def thaw_and_murko_centre(
109
110
  defaults are always correct
110
111
  """
111
112
 
113
+ MURKO_RESULTS_GROUP = "get_results"
114
+
112
115
  def centre_then_switch_forwarder_to_ROI() -> MsgGenerator:
113
116
  yield from bps.complete(oav_to_redis_forwarder, wait=True)
114
117
 
115
- yield from bps.trigger(murko_results, group="get_results")
116
-
117
118
  yield from bps.mv(oav_to_redis_forwarder.selected_source, Source.ROI.value)
118
119
 
119
- yield from bps.wait("get_results")
120
+ yield from bps.wait(MURKO_RESULTS_GROUP)
120
121
  x_predict = yield from bps.rd(murko_results.x_mm)
121
122
  y_predict = yield from bps.rd(murko_results.y_mm)
122
123
  z_predict = yield from bps.rd(murko_results.z_mm)
@@ -127,7 +128,13 @@ def thaw_and_murko_centre(
127
128
 
128
129
  yield from bps.kickoff(oav_to_redis_forwarder, wait=True)
129
130
 
130
- yield from stage_wrapper(
131
+ sample_id = yield from bps.rd(robot.sample_id)
132
+ yield from bps.abs_set(murko_results.sample_id, int(sample_id))
133
+
134
+ yield from bps.stage(murko_results)
135
+ yield from bps.trigger(murko_results, group=MURKO_RESULTS_GROUP)
136
+
137
+ yield from bpp.contingency_wrapper(
131
138
  _thaw_and_stream_to_redis(
132
139
  time_to_thaw,
133
140
  rotation,
@@ -138,7 +145,7 @@ def thaw_and_murko_centre(
138
145
  oav_to_redis_forwarder,
139
146
  centre_then_switch_forwarder_to_ROI,
140
147
  ),
141
- [murko_results],
148
+ final_plan=partial(bps.unstage, murko_results),
142
149
  )
143
150
 
144
151
 
@@ -0,0 +1,44 @@
1
+ from bluesky.utils import MsgGenerator
2
+ from dodal.common import inject
3
+ from dodal.devices.i24.commissioning_jungfrau import CommissioningJungfrau
4
+ from ophyd_async.core import (
5
+ WatchableAsyncStatus,
6
+ )
7
+ from ophyd_async.fastcs.jungfrau import (
8
+ create_jungfrau_external_triggering_info,
9
+ )
10
+ from pydantic import PositiveInt
11
+
12
+ from mx_bluesky.beamlines.i24.jungfrau_commissioning.plan_utils import (
13
+ fly_jungfrau,
14
+ override_file_path,
15
+ )
16
+
17
+
18
+ def do_external_acquisition(
19
+ exp_time_s: float,
20
+ total_triggers: PositiveInt = 1,
21
+ output_file_name: str | None = None,
22
+ wait: bool = False,
23
+ jungfrau: CommissioningJungfrau = inject("commissioning_jungfrau"),
24
+ ) -> MsgGenerator[WatchableAsyncStatus]:
25
+ """
26
+ Kickoff external triggering on the Jungfrau, and optionally wait for completion.
27
+
28
+ Must be used within an open Bluesky run.
29
+
30
+ Args:
31
+ exp_time_s: Length of detector exposure for each frame.
32
+ total_triggers: Number of external triggers recieved before acquisition is marked as complete.
33
+ jungfrau: Jungfrau device
34
+ output_file_name: Absolute path of the detector file output, including file name. If None, then use the PathProvider
35
+ set during jungfrau device instantiation
36
+ wait: Optionally block until data collection is complete.
37
+ """
38
+
39
+ if output_file_name:
40
+ override_file_path(jungfrau, output_file_name)
41
+
42
+ trigger_info = create_jungfrau_external_triggering_info(total_triggers, exp_time_s)
43
+ status = yield from fly_jungfrau(jungfrau, trigger_info, wait)
44
+ return status
@@ -0,0 +1,46 @@
1
+ from bluesky.utils import MsgGenerator
2
+ from dodal.beamlines.i24 import CommissioningJungfrau
3
+ from dodal.common import inject
4
+ from ophyd_async.core import (
5
+ WatchableAsyncStatus,
6
+ )
7
+ from ophyd_async.fastcs.jungfrau import (
8
+ create_jungfrau_internal_triggering_info,
9
+ )
10
+ from pydantic import PositiveInt
11
+
12
+ from mx_bluesky.beamlines.i24.jungfrau_commissioning.plan_utils import (
13
+ fly_jungfrau,
14
+ override_file_path,
15
+ )
16
+
17
+
18
+ def do_internal_acquisition(
19
+ exp_time_s: float,
20
+ total_frames: PositiveInt = 1,
21
+ jungfrau: CommissioningJungfrau = inject("jungfrau"),
22
+ path_of_output_file: str | None = None,
23
+ wait: bool = False,
24
+ ) -> MsgGenerator[WatchableAsyncStatus]:
25
+ """
26
+ Kickoff internal triggering on the Jungfrau, and optionally wait for completion. Frames
27
+ per trigger will trigger as rapidly as possible according to the Jungfrau deadtime.
28
+
29
+ Must be used within an open Bluesky run.
30
+
31
+ Args:
32
+ exp_time_s: Length of detector exposure for each frame.
33
+ total_frames: Number of frames taken after being internally triggered.
34
+ period_between_frames_s: Time between each detector frame, including deadtime. Not needed if frames_per_triggers is 1.
35
+ jungfrau: Jungfrau device
36
+ path_of_output_file: Absolute path of the detector file output, including file name. If None, then use the PathProvider
37
+ set during jungfrau device instantiation
38
+ wait: Optionally block until data collection is complete.
39
+ """
40
+
41
+ if path_of_output_file:
42
+ override_file_path(jungfrau, path_of_output_file)
43
+
44
+ trigger_info = create_jungfrau_internal_triggering_info(total_frames, exp_time_s)
45
+ status = yield from fly_jungfrau(jungfrau, trigger_info, wait)
46
+ return status
@@ -0,0 +1,73 @@
1
+ from pathlib import PurePath
2
+ from typing import cast
3
+
4
+ import bluesky.plan_stubs as bps
5
+ import bluesky.preprocessors as bpp
6
+ from bluesky.utils import MsgGenerator
7
+ from dodal.common.watcher_utils import log_on_percentage_complete
8
+ from dodal.devices.i24.commissioning_jungfrau import CommissioningJungfrau
9
+ from ophyd_async.core import (
10
+ AutoIncrementingPathProvider,
11
+ StaticFilenameProvider,
12
+ TriggerInfo,
13
+ WatchableAsyncStatus,
14
+ )
15
+
16
+ from mx_bluesky.common.utils.log import LOGGER
17
+
18
+ JF_COMPLETE_GROUP = "JF complete"
19
+
20
+
21
+ def fly_jungfrau(
22
+ jungfrau: CommissioningJungfrau, trigger_info: TriggerInfo, wait: bool = False
23
+ ) -> MsgGenerator[WatchableAsyncStatus]:
24
+ """Stage, prepare, and kickoff Jungfrau with a configured TriggerInfo. Optionally wait
25
+ for completion.
26
+
27
+ Note that this plan doesn't include unstaging of the Jungfrau, and a run must be open
28
+ before this plan is called.
29
+
30
+ Args:
31
+ jungfrau: Jungfrau device.
32
+ trigger_info: TriggerInfo which should be acquired using jungfrau util functions create_jungfrau_internal_triggering_info
33
+ or create_jungfrau_external_triggering_info.
34
+ wait: Optionally block until data collection is complete.
35
+ """
36
+
37
+ @bpp.contingency_decorator(
38
+ except_plan=lambda _: (yield from bps.unstage(jungfrau, wait=True))
39
+ )
40
+ def _fly_with_unstage_contingency():
41
+ yield from bps.stage(jungfrau)
42
+ LOGGER.info("Setting up detector...")
43
+ yield from bps.prepare(jungfrau, trigger_info, wait=True)
44
+ LOGGER.info("Detector prepared. Starting acquisition")
45
+ yield from bps.kickoff(jungfrau, wait=True)
46
+ LOGGER.info("Waiting for acquisition to complete...")
47
+ status = yield from bps.complete(jungfrau, group=JF_COMPLETE_GROUP)
48
+
49
+ # StandardDetector.complete converts regular status to watchable status,
50
+ # but bluesky plan stubs can't see this currently
51
+ status = cast(WatchableAsyncStatus, status)
52
+ log_on_percentage_complete(
53
+ status, "Jungfrau data collection triggers recieved", 10
54
+ )
55
+ if wait:
56
+ yield from bps.wait(JF_COMPLETE_GROUP)
57
+ return status
58
+
59
+ return (yield from _fly_with_unstage_contingency())
60
+
61
+
62
+ def override_file_path(jungfrau: CommissioningJungfrau, path_of_output_file: str):
63
+ """While we should generally use device instantiation to set the path,
64
+ during commissioning, it is useful to be able to explicitly set the filename
65
+ and path.
66
+
67
+ This function must be called before the Jungfrau is prepared.
68
+ """
69
+ _file_path = PurePath(path_of_output_file)
70
+ _new_filename_provider = StaticFilenameProvider(_file_path.name)
71
+ jungfrau._writer._path_info = AutoIncrementingPathProvider( # noqa: SLF001
72
+ _new_filename_provider, _file_path.parent
73
+ )
@@ -12,6 +12,8 @@ from dodal.plan_stubs.motor_utils import MoveTooLarge, home_and_reset_wrapper
12
12
  from mx_bluesky.common.utils.log import LOGGER
13
13
  from mx_bluesky.hyperion.parameters.constants import CONST
14
14
 
15
+ SLEEP_PER_CHECK = 0.1
16
+
15
17
 
16
18
  def wait_for_smargon_not_disabled(smargon: Smargon, timeout=60):
17
19
  """Waits for the smargon disabled flag to go low. The robot hardware is responsible
@@ -19,7 +21,6 @@ def wait_for_smargon_not_disabled(smargon: Smargon, timeout=60):
19
21
  connection between the robot and the smargon.
20
22
  """
21
23
  LOGGER.info("Waiting for smargon enabled")
22
- SLEEP_PER_CHECK = 0.1
23
24
  times_to_check = int(timeout / SLEEP_PER_CHECK)
24
25
  for _ in range(times_to_check):
25
26
  smargon_disabled = yield from bps.rd(smargon.disabled)
@@ -9,8 +9,10 @@ import bluesky.preprocessors as bpp
9
9
  import numpy as np
10
10
  from bluesky.protocols import Readable
11
11
  from bluesky.utils import MsgGenerator
12
+ from dodal.common.beamlines.commissioning_mode import read_commissioning_mode
12
13
  from dodal.devices.fast_grid_scan import (
13
14
  FastGridScanCommon,
15
+ FastGridScanThreeD,
14
16
  )
15
17
  from dodal.devices.zocalo import ZocaloResults
16
18
  from dodal.devices.zocalo.zocalo_results import (
@@ -226,11 +228,33 @@ def _fetch_xrc_results_from_zocalo(
226
228
  for xr in filtered_results
227
229
  ]
228
230
  else:
229
- LOGGER.warning("No X-ray centre received")
230
- raise CrystalNotFoundException()
231
+ commissioning_mode = yield from read_commissioning_mode()
232
+ if commissioning_mode:
233
+ LOGGER.info("Commissioning mode enabled, returning dummy result")
234
+ flyscan_results = [_generate_dummy_xrc_result(parameters)]
235
+ else:
236
+ LOGGER.warning("No X-ray centre received")
237
+ raise CrystalNotFoundException()
231
238
  yield from _fire_xray_centre_result_event(flyscan_results)
232
239
 
233
240
 
241
+ def _generate_dummy_xrc_result(params: SpecifiedThreeDGridScan) -> XRayCentreResult:
242
+ com = [params.x_steps / 2, params.y_steps / 2, params.z_steps / 2]
243
+ max_voxel = [round(p) for p in com]
244
+ return _xrc_result_in_boxes_to_result_in_mm(
245
+ XrcResult(
246
+ centre_of_mass=com,
247
+ max_voxel=max_voxel,
248
+ bounding_box=[max_voxel, [p + 1 for p in max_voxel]],
249
+ n_voxels=1,
250
+ max_count=10000,
251
+ total_count=100000,
252
+ sample_id=params.sample_id,
253
+ ),
254
+ params,
255
+ )
256
+
257
+
234
258
  @bpp.set_run_key_decorator(PlanNameConstants.GRIDSCAN_MAIN)
235
259
  @bpp.run_decorator(md={"subplan_name": PlanNameConstants.GRIDSCAN_MAIN})
236
260
  def run_gridscan(
@@ -260,13 +284,13 @@ def run_gridscan(
260
284
  fgs_composite.eiger,
261
285
  fgs_composite.synchrotron,
262
286
  [parameters.scan_points_first_grid, parameters.scan_points_second_grid],
263
- parameters.scan_indices,
264
287
  plan_during_collection=beamline_specific.read_during_collection_plan,
265
288
  )
266
289
 
267
- # GDA's gridscans requires Z steps to be at 0, so make sure we leave this device
290
+ # GDA's 3D gridscans requires Z steps to be at 0, so make sure we leave this device
268
291
  # in a GDA-happy state.
269
- yield from bps.abs_set(beamline_specific.fgs_motors.z_steps, 0, wait=False)
292
+ if isinstance(beamline_specific.fgs_motors, FastGridScanThreeD):
293
+ yield from bps.abs_set(beamline_specific.fgs_motors.z_steps, 0, wait=False)
270
294
 
271
295
 
272
296
  def wait_for_gridscan_valid(fgs_motors: FastGridScanCommon, timeout=0.5):
@@ -17,6 +17,9 @@ from scanspec.core import AxesPoints, Axis
17
17
  from mx_bluesky.common.experiment_plans.inner_plans.read_hardware import (
18
18
  read_hardware_for_zocalo,
19
19
  )
20
+ from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import (
21
+ GridscanPlane,
22
+ )
20
23
  from mx_bluesky.common.parameters.constants import (
21
24
  PlanNameConstants,
22
25
  )
@@ -66,7 +69,6 @@ def kickoff_and_complete_gridscan(
66
69
  detector: EigerDetector, # Once Eiger inherits from StandardDetector, use that type instead
67
70
  synchrotron: Synchrotron,
68
71
  scan_points: list[AxesPoints[Axis]],
69
- scan_start_indices: list[int],
70
72
  plan_during_collection: Callable[[], MsgGenerator] | None = None,
71
73
  ):
72
74
  """Triggers a grid scan motion program and waits for completion, accounting for synchrotron topup.
@@ -80,15 +82,10 @@ def kickoff_and_complete_gridscan(
80
82
  synchrotron (Synchrotron): Synchrotron device
81
83
  scan_points (list[AxesPoints[Axis]]): Each element in the list contains all the grid points for that grid scan.
82
84
  Two elements in this list indicates that two grid scans will be done, eg for Hyperion's 3D grid scans.
83
- scan_start_indices (list[int]): Contains the first index of each grid scan
84
85
  plan_during_collection (Optional, MsgGenerator): Generic plan called in between kickoff and completion,
85
86
  eg waiting on zocalo.
86
87
  """
87
88
 
88
- assert len(scan_points) == len(scan_start_indices), (
89
- "scan_points and scan_start_indices must be lists of the same length!"
90
- )
91
-
92
89
  plan_name = PlanNameConstants.DO_FGS
93
90
 
94
91
  @TRACER.start_as_current_span(plan_name)
@@ -96,8 +93,10 @@ def kickoff_and_complete_gridscan(
96
93
  @bpp.run_decorator(
97
94
  md={
98
95
  "subplan_name": plan_name,
99
- "scan_points": scan_points,
100
- "scan_start_indices": scan_start_indices,
96
+ "omega_to_scan_spec": {
97
+ GridscanPlane.OMEGA_XY: scan_points[0],
98
+ GridscanPlane.OMEGA_XZ: scan_points[1],
99
+ },
101
100
  }
102
101
  )
103
102
  @bpp.contingency_decorator(
@@ -86,11 +86,11 @@ class BaseISPyBCallback(PlanReactiveCallback):
86
86
 
87
87
  def activity_gated_start(self, doc: RunStart):
88
88
  self._oav_snapshot_event_idx = 0
89
- return self._tag_doc(doc)
89
+ return self.tag_doc(doc)
90
90
 
91
91
  def activity_gated_descriptor(self, doc: EventDescriptor):
92
92
  self.descriptors[doc["uid"]] = doc
93
- return self._tag_doc(doc)
93
+ return self.tag_doc(doc)
94
94
 
95
95
  def activity_gated_event(self, doc: Event) -> Event:
96
96
  """Subclasses should extend this to add a call to set_dcig_tag from
@@ -112,10 +112,10 @@ class BaseISPyBCallback(PlanReactiveCallback):
112
112
  case DocDescriptorNames.HARDWARE_READ_DURING:
113
113
  scan_data_infos = self._handle_ispyb_transmission_flux_read(doc)
114
114
  case _:
115
- return self._tag_doc(doc)
115
+ return self.tag_doc(doc)
116
116
  self.ispyb_ids = self.ispyb.update_deposition(self.ispyb_ids, scan_data_infos)
117
117
  ISPYB_ZOCALO_CALLBACK_LOGGER.info(f"Received ISPYB IDs: {self.ispyb_ids}")
118
- return self._tag_doc(doc)
118
+ return self.tag_doc(doc)
119
119
 
120
120
  def _handle_ispyb_hardware_read(self, doc) -> Sequence[ScanDataInfo]:
121
121
  assert self.params, "Event handled before activity_gated_start received params"
@@ -203,7 +203,7 @@ class BaseISPyBCallback(PlanReactiveCallback):
203
203
  ISPYB_ZOCALO_CALLBACK_LOGGER.warning(
204
204
  f"Failed to finalise ISPyB deposition on stop document: {format_doc_for_log(doc)} with exception: {e}"
205
205
  )
206
- return self._tag_doc(doc)
206
+ return self.tag_doc(doc)
207
207
 
208
208
  def _append_to_comment(self, id: int, comment: str) -> None:
209
209
  assert self.ispyb is not None
@@ -218,7 +218,7 @@ class BaseISPyBCallback(PlanReactiveCallback):
218
218
  for id in self.ispyb_ids.data_collection_ids:
219
219
  self._append_to_comment(id, comment)
220
220
 
221
- def _tag_doc(self, doc: D) -> D:
221
+ def tag_doc(self, doc: D) -> D:
222
222
  assert isinstance(doc, dict)
223
223
  if self.ispyb_ids:
224
224
  doc["ispyb_dcids"] = self.ispyb_ids.data_collection_ids
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Callable, Generator
3
4
  from typing import TYPE_CHECKING
4
5
 
5
6
  from bluesky.callbacks import CallbackBase
@@ -10,12 +11,14 @@ from mx_bluesky.common.parameters.constants import (
10
11
  )
11
12
  from mx_bluesky.common.utils.exceptions import ISPyBDepositionNotMade
12
13
  from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER
13
- from mx_bluesky.common.utils.utils import number_of_frames_from_scan_spec
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from event_model.documents import Event, EventDescriptor, RunStart, RunStop
17
17
 
18
18
 
19
+ ZocaloInfoGenerator = Generator[list[ZocaloStartInfo], dict, None]
20
+
21
+
19
22
  class ZocaloCallback(CallbackBase):
20
23
  """Callback class to handle the triggering of Zocalo processing.
21
24
  Will start listening for collections when {triggering_plan} has been started.
@@ -26,40 +29,45 @@ class ZocaloCallback(CallbackBase):
26
29
 
27
30
  Shouldn't be subscribed directly to the RunEngine, instead should be passed to the
28
31
  `emit` argument of an ISPyB callback which appends DCIDs to the relevant start doc.
32
+
33
+ Args:
34
+ triggering_plan: Name of the bluesky sub-plan inside of which we generate information
35
+ to be submitted to zocalo; this is identified by the 'subplan_name' entry in the
36
+ run start metadata.
37
+ zocalo_environment: Name of the zocalo environment we use to connect to zocalo
38
+ start_info_generator_factory: A factory method which returns a Generator,
39
+ the generator is sent the ZOCALO_HW_READ event document and in return yields
40
+ one or more ZocaloStartInfo which will each be submitted to zocalo as a job.
29
41
  """
30
42
 
43
+ def __init__(
44
+ self,
45
+ triggering_plan: str,
46
+ zocalo_environment: str,
47
+ start_info_generator_factory: Callable[[], ZocaloInfoGenerator],
48
+ ):
49
+ super().__init__()
50
+ self._info_generator_factory = start_info_generator_factory
51
+ self.triggering_plan = triggering_plan
52
+ self.zocalo_interactor = ZocaloTrigger(zocalo_environment)
53
+ self._reset_state()
54
+
31
55
  def _reset_state(self):
32
56
  self.run_uid: str | None = None
33
57
  self.zocalo_info: list[ZocaloStartInfo] = []
34
58
  self._started_zocalo_collections: list[ZocaloStartInfo] = []
35
59
  self.descriptors: dict[str, EventDescriptor] = {}
36
- self.start_frame = 0
37
-
38
- def __init__(self, triggering_plan: str, zocalo_environment: str):
39
- super().__init__()
40
- self.triggering_plan = triggering_plan
41
- self.zocalo_interactor = ZocaloTrigger(zocalo_environment)
42
- self._reset_state()
60
+ self._info_generator = self._info_generator_factory()
61
+ # Prime the generator
62
+ next(self._info_generator)
43
63
 
44
64
  def start(self, doc: RunStart):
45
65
  ISPYB_ZOCALO_CALLBACK_LOGGER.info("Zocalo handler received start document.")
46
66
  if self.triggering_plan and doc.get("subplan_name") == self.triggering_plan:
47
67
  self.run_uid = doc.get("uid")
48
68
  if self.run_uid:
49
- if (
50
- isinstance(scan_points := doc.get("scan_points"), list)
51
- and isinstance(ispyb_ids := doc.get("ispyb_dcids"), tuple)
52
- and len(ispyb_ids) > 0
53
- ):
54
- ISPYB_ZOCALO_CALLBACK_LOGGER.info(f"Zocalo triggering for {ispyb_ids}")
55
- ids_and_shape = list(zip(ispyb_ids, scan_points, strict=False))
56
- for idx, id_and_shape in enumerate(ids_and_shape):
57
- id, shape = id_and_shape
58
- num_frames = number_of_frames_from_scan_spec(shape)
59
- self.zocalo_info.append(
60
- ZocaloStartInfo(id, None, self.start_frame, num_frames, idx)
61
- )
62
- self.start_frame += num_frames
69
+ zocalo_infos = self._info_generator.send(doc) # type: ignore
70
+ self.zocalo_info.extend(zocalo_infos)
63
71
 
64
72
  def descriptor(self, doc: EventDescriptor):
65
73
  self.descriptors[doc["uid"]] = doc