mx-bluesky 1.4.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 (78) hide show
  1. mx_bluesky/_version.py +2 -2
  2. mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +178 -0
  3. mx_bluesky/beamlines/i04/thawing_plan.py +1 -1
  4. mx_bluesky/beamlines/i24/serial/dcid.py +143 -171
  5. mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +1 -1
  6. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +54 -21
  7. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +2 -5
  8. mx_bluesky/beamlines/i24/serial/fixed_target/ft_utils.py +0 -1
  9. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +67 -50
  10. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +26 -79
  11. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +0 -199
  12. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +4 -6
  13. mx_bluesky/beamlines/i24/serial/log.py +1 -1
  14. mx_bluesky/beamlines/i24/serial/parameters/__init__.py +4 -0
  15. mx_bluesky/beamlines/i24/serial/parameters/constants.py +6 -1
  16. mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py +42 -15
  17. mx_bluesky/beamlines/i24/serial/run_fixed_target.sh +4 -3
  18. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +2 -0
  19. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +103 -81
  20. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +1 -2
  21. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +24 -26
  22. mx_bluesky/beamlines/i24/serial/write_nexus.py +74 -72
  23. mx_bluesky/common/external_interaction/config_server.py +46 -0
  24. mx_bluesky/common/parameters/components.py +52 -15
  25. mx_bluesky/common/parameters/constants.py +11 -1
  26. mx_bluesky/common/parameters/gridscan.py +94 -0
  27. mx_bluesky/{hyperion → common}/parameters/robot_load.py +2 -2
  28. mx_bluesky/common/plans/do_fgs.py +2 -2
  29. mx_bluesky/common/utils/log.py +2 -0
  30. mx_bluesky/hyperion/__main__.py +2 -1
  31. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +21 -31
  32. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +4 -4
  33. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +1 -1
  34. mx_bluesky/hyperion/device_setup_plans/smargon.py +3 -3
  35. mx_bluesky/hyperion/exceptions.py +13 -1
  36. mx_bluesky/hyperion/experiment_plans/__init__.py +4 -0
  37. mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py +83 -0
  38. mx_bluesky/hyperion/experiment_plans/common/xrc_result.py +47 -0
  39. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +9 -9
  40. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +133 -97
  41. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +42 -18
  42. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +75 -9
  43. mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +2 -2
  44. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +1 -1
  45. mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +2 -2
  46. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +36 -17
  47. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +5 -5
  48. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +28 -28
  49. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +64 -16
  50. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +11 -3
  51. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +10 -10
  52. mx_bluesky/hyperion/external_interaction/callbacks/__init__.py +0 -4
  53. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +4 -0
  54. mx_bluesky/hyperion/external_interaction/callbacks/common/abstract_event.py +66 -0
  55. mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py +5 -0
  56. mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py +15 -15
  57. mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +18 -10
  58. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +3 -1
  59. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +5 -3
  60. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/__init__.py +0 -0
  61. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/sample_handling_callback.py +84 -0
  62. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +15 -9
  63. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py +5 -4
  64. mx_bluesky/hyperion/external_interaction/config_server.py +8 -37
  65. mx_bluesky/hyperion/external_interaction/exceptions.py +0 -9
  66. mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +65 -15
  67. mx_bluesky/hyperion/parameters/components.py +4 -9
  68. mx_bluesky/hyperion/parameters/constants.py +0 -1
  69. mx_bluesky/hyperion/parameters/gridscan.py +33 -76
  70. mx_bluesky/hyperion/parameters/load_centre_collect.py +14 -9
  71. mx_bluesky/hyperion/parameters/rotation.py +15 -6
  72. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1.dist-info}/METADATA +35 -34
  73. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1.dist-info}/RECORD +77 -70
  74. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1.dist-info}/WHEEL +1 -1
  75. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Mapping_py3v1.py +0 -150
  76. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1.dist-info}/LICENSE +0 -0
  77. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1.dist-info}/entry_points.txt +0 -0
  78. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,7 @@ from functools import wraps
3
3
 
4
4
  import bluesky.plan_stubs as bps
5
5
  import bluesky.preprocessors as bpp
6
- from blueapi.core import MsgGenerator
6
+ from bluesky.utils import MsgGenerator
7
7
  from dodal.devices.zebra import (
8
8
  AUTO_SHUTTER_GATE,
9
9
  AUTO_SHUTTER_INPUT_1,
@@ -2,17 +2,17 @@ import numpy as np
2
2
  from bluesky import plan_stubs as bps
3
3
  from dodal.devices.smargon import Smargon
4
4
 
5
- from mx_bluesky.hyperion.exceptions import WarningException
5
+ from mx_bluesky.hyperion.exceptions import SampleException
6
6
 
7
7
 
8
8
  def move_smargon_warn_on_out_of_range(
9
9
  smargon: Smargon, position: np.ndarray | list[float] | tuple[float, float, float]
10
10
  ):
11
- """Throws a WarningException if the specified position is out of range for the
11
+ """Throws a SampleException if the specified position is out of range for the
12
12
  smargon. Otherwise moves to that position."""
13
13
  limits = yield from smargon.get_xyz_limits()
14
14
  if not limits.position_valid(position):
15
- raise WarningException(
15
+ raise SampleException(
16
16
  "Pin tip centring failed - pin too long/short/bent and out of range"
17
17
  )
18
18
  yield from bps.mv(
@@ -13,9 +13,21 @@ class WarningException(Exception):
13
13
  pass
14
14
 
15
15
 
16
+ class SampleException(WarningException):
17
+ """An exception which identifies an issue relating to the sample."""
18
+
19
+ pass
20
+
21
+
16
22
  T = TypeVar("T")
17
23
 
18
24
 
25
+ class CrystalNotFoundException(SampleException):
26
+ """Raised if grid detection completed normally but no crystal was found."""
27
+
28
+ pass
29
+
30
+
19
31
  def catch_exception_and_warn(
20
32
  exception_to_catch: type[Exception],
21
33
  func: Callable[..., Generator[Msg, None, T]],
@@ -36,7 +48,7 @@ def catch_exception_and_warn(
36
48
 
37
49
  def warn_if_exception_matches(exception: Exception):
38
50
  if isinstance(exception, exception_to_catch):
39
- raise WarningException(str(exception))
51
+ raise SampleException(str(exception)) from exception
40
52
  yield from null()
41
53
 
42
54
  return (
@@ -9,6 +9,9 @@ from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import (
9
9
  from mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan import (
10
10
  grid_detect_then_xray_centre,
11
11
  )
12
+ from mx_bluesky.hyperion.experiment_plans.load_centre_collect_full_plan import (
13
+ load_centre_collect_full,
14
+ )
12
15
  from mx_bluesky.hyperion.experiment_plans.pin_centre_then_xray_centre_plan import (
13
16
  pin_tip_centre_then_xray_centre,
14
17
  )
@@ -27,4 +30,5 @@ __all__ = [
27
30
  "pin_tip_centre_then_xray_centre",
28
31
  "multi_rotation_scan",
29
32
  "robot_load_then_centre",
33
+ "load_centre_collect_full",
30
34
  ]
@@ -0,0 +1,83 @@
1
+ import bluesky.plan_stubs as bps
2
+ import bluesky.preprocessors as bpp
3
+ import numpy
4
+ from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue
5
+ from dodal.devices.smargon import Smargon, StubPosition
6
+
7
+ from mx_bluesky.common.utils.tracing import TRACER
8
+ from mx_bluesky.hyperion.device_setup_plans.manipulate_sample import move_x_y_z
9
+ from mx_bluesky.hyperion.experiment_plans.common.xrc_result import XRayCentreResult
10
+ from mx_bluesky.hyperion.log import LOGGER
11
+ from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan
12
+
13
+
14
+ def change_aperture_then_move_to_xtal(
15
+ best_hit: XRayCentreResult,
16
+ smargon: Smargon,
17
+ aperture_scatterguard: ApertureScatterguard,
18
+ parameters: HyperionThreeDGridScan | None = None,
19
+ ):
20
+ """For the given x-ray centring result,
21
+ * Change the aperture so that the beam size is comparable to the crystal size
22
+ * Centre on the centre-of-mass
23
+ * Reset the stub offsets if specified by params"""
24
+ if best_hit.bounding_box_mm is not None:
25
+ bounding_box_size = numpy.abs(
26
+ best_hit.bounding_box_mm[1] - best_hit.bounding_box_mm[0]
27
+ )
28
+ with TRACER.start_span("change_aperture"):
29
+ yield from _set_aperture_for_bbox_mm(
30
+ aperture_scatterguard, bounding_box_size
31
+ )
32
+ else:
33
+ LOGGER.warning("No bounding box size received")
34
+
35
+ # once we have the results, go to the appropriate position
36
+ LOGGER.info("Moving to centre of mass.")
37
+ with TRACER.start_span("move_to_result"):
38
+ x, y, z = best_hit.centre_of_mass_mm
39
+ yield from move_x_y_z(smargon, x, y, z, wait=True)
40
+
41
+ # TODO support for setting stub offsets in multipin
42
+ # https://github.com/DiamondLightSource/mx-bluesky/issues/552
43
+ if parameters and parameters.FGS_params.set_stub_offsets:
44
+ LOGGER.info("Recentring smargon co-ordinate system to this point.")
45
+ yield from bps.mv(
46
+ # See: https://github.com/bluesky/bluesky/issues/1809
47
+ smargon.stub_offsets, # type: ignore
48
+ StubPosition.CURRENT_AS_CENTER, # type: ignore
49
+ )
50
+
51
+
52
+ def _set_aperture_for_bbox_mm(
53
+ aperture_device: ApertureScatterguard, bbox_size_mm: list[float] | numpy.ndarray
54
+ ):
55
+ # TODO confirm correction factor see https://github.com/DiamondLightSource/mx-bluesky/issues/618
56
+ ASSUMED_BOX_SIZE_MM = 0.020
57
+ bbox_size_boxes = [round(mm / ASSUMED_BOX_SIZE_MM) for mm in bbox_size_mm]
58
+ yield from set_aperture_for_bbox_size(aperture_device, bbox_size_boxes)
59
+
60
+
61
+ def set_aperture_for_bbox_size(
62
+ aperture_device: ApertureScatterguard,
63
+ bbox_size: list[int] | numpy.ndarray,
64
+ ):
65
+ # bbox_size is [x,y,z], for i03 we only care about x
66
+ new_selected_aperture = (
67
+ ApertureValue.MEDIUM if bbox_size[0] < 2 else ApertureValue.LARGE
68
+ )
69
+ LOGGER.info(
70
+ f"Setting aperture to {new_selected_aperture} based on bounding box size {bbox_size}."
71
+ )
72
+
73
+ @bpp.set_run_key_decorator("change_aperture")
74
+ @bpp.run_decorator(
75
+ md={
76
+ "subplan_name": "change_aperture",
77
+ "aperture_size": new_selected_aperture.value,
78
+ }
79
+ )
80
+ def set_aperture():
81
+ yield from bps.abs_set(aperture_device, new_selected_aperture)
82
+
83
+ yield from set_aperture()
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ from collections.abc import Callable, Sequence
5
+ from functools import partial
6
+
7
+ import numpy as np
8
+
9
+ from mx_bluesky.common.parameters.components import (
10
+ MultiXtalSelection,
11
+ TopNByMaxCountSelection,
12
+ )
13
+
14
+
15
+ @dataclasses.dataclass
16
+ class XRayCentreResult:
17
+ """Represents information about a hit from an X-ray centring."""
18
+
19
+ centre_of_mass_mm: np.ndarray
20
+ bounding_box_mm: tuple[np.ndarray, np.ndarray]
21
+ max_count: int
22
+ total_count: int
23
+
24
+ def __eq__(self, o):
25
+ return (
26
+ isinstance(o, XRayCentreResult)
27
+ and o.max_count == self.max_count
28
+ and o.total_count == self.total_count
29
+ and all(o.centre_of_mass_mm == self.centre_of_mass_mm)
30
+ and all(o.bounding_box_mm[0] == self.bounding_box_mm[0])
31
+ and all(o.bounding_box_mm[1] == self.bounding_box_mm[1])
32
+ )
33
+
34
+
35
+ def top_n_by_max_count(
36
+ unfiltered: Sequence[XRayCentreResult], n: int
37
+ ) -> Sequence[XRayCentreResult]:
38
+ sorted_hits = sorted(unfiltered, key=lambda result: result.max_count, reverse=True)
39
+ return sorted_hits[:n]
40
+
41
+
42
+ def resolve_selection_fn(
43
+ params: MultiXtalSelection,
44
+ ) -> Callable[[Sequence[XRayCentreResult]], Sequence[XRayCentreResult]]:
45
+ if isinstance(params, TopNByMaxCountSelection):
46
+ return partial(top_n_by_max_count, n=params.n)
47
+ raise ValueError(f"Invalid selection function {params.name}")
@@ -5,6 +5,11 @@ from typing import TypedDict
5
5
 
6
6
  import mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan as flyscan_xray_centre_plan
7
7
  import mx_bluesky.hyperion.experiment_plans.rotation_scan_plan as rotation_scan_plan
8
+ from mx_bluesky.common.parameters.gridscan import (
9
+ GridScanWithEdgeDetect,
10
+ PinTipCentreThenXrayCentre,
11
+ RobotLoadThenCentre,
12
+ )
8
13
  from mx_bluesky.hyperion.experiment_plans import (
9
14
  grid_detect_then_xray_centre_plan,
10
15
  load_centre_collect_full_plan,
@@ -18,12 +23,7 @@ from mx_bluesky.hyperion.external_interaction.callbacks.common.callback_util imp
18
23
  create_robot_load_and_centre_callbacks,
19
24
  create_rotation_callbacks,
20
25
  )
21
- from mx_bluesky.hyperion.parameters.gridscan import (
22
- GridScanWithEdgeDetect,
23
- PinTipCentreThenXrayCentre,
24
- RobotLoadThenCentre,
25
- ThreeDGridScan,
26
- )
26
+ from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan
27
27
  from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
28
28
  from mx_bluesky.hyperion.parameters.rotation import MultiRotationScan, RotationScan
29
29
 
@@ -39,7 +39,7 @@ def do_nothing():
39
39
  class ExperimentRegistryEntry(TypedDict):
40
40
  setup: Callable
41
41
  param_type: type[
42
- ThreeDGridScan
42
+ HyperionThreeDGridScan
43
43
  | GridScanWithEdgeDetect
44
44
  | RotationScan
45
45
  | MultiRotationScan
@@ -53,7 +53,7 @@ class ExperimentRegistryEntry(TypedDict):
53
53
  PLAN_REGISTRY: dict[str, ExperimentRegistryEntry] = {
54
54
  "flyscan_xray_centre": {
55
55
  "setup": flyscan_xray_centre_plan.create_devices,
56
- "param_type": ThreeDGridScan,
56
+ "param_type": HyperionThreeDGridScan,
57
57
  "callbacks_factory": create_gridscan_callbacks,
58
58
  },
59
59
  "grid_detect_then_xray_centre": {
@@ -81,7 +81,7 @@ PLAN_REGISTRY: dict[str, ExperimentRegistryEntry] = {
81
81
  "param_type": MultiRotationScan,
82
82
  "callbacks_factory": create_rotation_callbacks,
83
83
  },
84
- "load_centre_collect_full_plan": {
84
+ "load_centre_collect_full": {
85
85
  "setup": load_centre_collect_full_plan.create_devices,
86
86
  "param_type": LoadCentreCollect,
87
87
  "callbacks_factory": create_load_centre_collect_callbacks,
@@ -1,7 +1,7 @@
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
7
  from typing import Protocol
@@ -9,10 +9,12 @@ from typing import Protocol
9
9
  import bluesky.plan_stubs as bps
10
10
  import bluesky.preprocessors as bpp
11
11
  import numpy as np
12
- 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
13
16
  from dodal.devices.aperturescatterguard import (
14
17
  ApertureScatterguard,
15
- ApertureValue,
16
18
  )
17
19
  from dodal.devices.attenuator import Attenuator
18
20
  from dodal.devices.backlight import Backlight
@@ -29,7 +31,7 @@ from dodal.devices.fast_grid_scan import (
29
31
  from dodal.devices.flux import Flux
30
32
  from dodal.devices.robot import BartRobot
31
33
  from dodal.devices.s4_slit_gaps import S4SlitGaps
32
- from dodal.devices.smargon import Smargon, StubPosition
34
+ from dodal.devices.smargon import Smargon
33
35
  from dodal.devices.synchrotron import Synchrotron
34
36
  from dodal.devices.undulator import Undulator
35
37
  from dodal.devices.xbpm_feedback import XBPMFeedback
@@ -38,14 +40,15 @@ from dodal.devices.zebra_controlled_shutter import ZebraShutter
38
40
  from dodal.devices.zocalo.zocalo_results import (
39
41
  ZOCALO_READING_PLAN_NAME,
40
42
  ZOCALO_STAGE_GROUP,
43
+ XrcResult,
41
44
  ZocaloResults,
42
45
  get_full_processing_results,
43
46
  )
47
+ from event_model import RunStart
44
48
  from ophyd_async.fastcs.panda import HDFPanda
45
49
 
46
50
  from mx_bluesky.common.plans.do_fgs import kickoff_and_complete_gridscan
47
51
  from mx_bluesky.common.utils.tracing import TRACER
48
- from mx_bluesky.hyperion.device_setup_plans.manipulate_sample import move_x_y_z
49
52
  from mx_bluesky.hyperion.device_setup_plans.read_hardware_for_setup import (
50
53
  read_hardware_during_collection,
51
54
  read_hardware_pre_collection,
@@ -63,10 +66,17 @@ from mx_bluesky.hyperion.device_setup_plans.setup_zebra import (
63
66
  from mx_bluesky.hyperion.device_setup_plans.xbpm_feedback import (
64
67
  transmission_and_xbpm_feedback_for_collection_decorator,
65
68
  )
66
- 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
+ )
67
77
  from mx_bluesky.hyperion.log import LOGGER
68
78
  from mx_bluesky.hyperion.parameters.constants import CONST
69
- from mx_bluesky.hyperion.parameters.gridscan import ThreeDGridScan
79
+ from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan
70
80
  from mx_bluesky.hyperion.utils.context import device_composite_from_context
71
81
 
72
82
 
@@ -74,13 +84,7 @@ class SmargonSpeedException(Exception):
74
84
  pass
75
85
 
76
86
 
77
- class CrystalNotFoundException(WarningException):
78
- """Raised if grid detection completed normally but no crystal was found."""
79
-
80
- pass
81
-
82
-
83
- @dataclasses.dataclass
87
+ @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
84
88
  class FlyScanXRayCentreComposite:
85
89
  """All devices which are directly or indirectly required by this plan"""
86
90
 
@@ -109,26 +113,29 @@ class FlyScanXRayCentreComposite:
109
113
  return self.smargon
110
114
 
111
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
+
112
130
  def create_devices(context: BlueskyContext) -> FlyScanXRayCentreComposite:
113
131
  """Creates the devices required for the plan and connect to them"""
114
132
  return device_composite_from_context(context, FlyScanXRayCentreComposite)
115
133
 
116
134
 
117
- def flyscan_xray_centre(
118
- composite: FlyScanXRayCentreComposite,
119
- parameters: ThreeDGridScan,
135
+ def flyscan_xray_centre_no_move(
136
+ composite: FlyScanXRayCentreComposite, parameters: HyperionThreeDGridScan
120
137
  ) -> MsgGenerator:
121
- """Create the plan to run the grid scan based on provided parameters.
122
-
123
- The ispyb handler should be added to the whole gridscan as we want to capture errors
124
- at any point in it.
125
-
126
- Args:
127
- parameters (ThreeDGridScan): The parameters to run the scan.
128
-
129
- Returns:
130
- Generator: The plan for the gridscan
131
- """
138
+ """Perform a flyscan and determine the centres of interest"""
132
139
  parameters.features.update_self_from_server()
133
140
  composite.eiger.set_detector_parameters(parameters.detector_params)
134
141
  composite.zocalo.zocalo_environment = CONST.ZOCALO_ENV
@@ -152,25 +159,66 @@ def flyscan_xray_centre(
152
159
  composite.attenuator,
153
160
  parameters.transmission_frac,
154
161
  )
155
- def run_gridscan_and_move_and_tidy(
162
+ def run_gridscan_and_fetch_and_tidy(
156
163
  fgs_composite: FlyScanXRayCentreComposite,
157
- params: ThreeDGridScan,
164
+ params: HyperionThreeDGridScan,
158
165
  feature_controlled: _FeatureControlled,
159
- ):
160
- 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.
161
184
 
162
- return run_gridscan_and_move_and_tidy(composite, parameters, feature_controlled)
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
+ )
198
+
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
+ )
163
211
 
164
212
 
165
213
  @bpp.set_run_key_decorator(CONST.PLAN.GRIDSCAN_AND_MOVE)
166
214
  @bpp.run_decorator(md={"subplan_name": CONST.PLAN.GRIDSCAN_AND_MOVE})
167
- def run_gridscan_and_move(
215
+ def run_gridscan_and_fetch_results(
168
216
  fgs_composite: FlyScanXRayCentreComposite,
169
- parameters: ThreeDGridScan,
217
+ parameters: HyperionThreeDGridScan,
170
218
  feature_controlled: _FeatureControlled,
171
219
  ) -> MsgGenerator:
172
220
  """A multi-run plan which runs a gridscan, gets the results from zocalo
173
- and moves to the centre of mass determined by zocalo"""
221
+ and fires an event with the centres of mass determined by zocalo"""
174
222
 
175
223
  # We get the initial motor positions so we can return to them on zocalo failure
176
224
  initial_xyz = np.array(
@@ -198,37 +246,17 @@ def run_gridscan_and_move(
198
246
  )
199
247
  LOGGER.info("Zocalo triggered and read, interpreting results.")
200
248
  xrc_results = yield from get_full_processing_results(fgs_composite.zocalo)
201
- LOGGER.info(f"Got xray centring results: {xrc_results}")
249
+ LOGGER.info(f"Got xray centres, top 5: {xrc_results[:5]}")
202
250
  if xrc_results:
203
- best_result = xrc_results[0]
204
- xrc_centre_grid_coords = best_result["centre_of_mass"]
205
- xray_centre = parameters.FGS_params.grid_position_to_motor_position(
206
- np.array(xrc_centre_grid_coords)
207
- )
208
- with TRACER.start_span("change_aperture"):
209
- bbox_size = np.abs(
210
- np.array(best_result["bounding_box"][1])
211
- - np.array(best_result["bounding_box"][0])
212
- )
213
- yield from set_aperture_for_bbox_size(
214
- fgs_composite.aperture_scatterguard, bbox_size
215
- )
251
+ flyscan_results = [
252
+ _xrc_result_in_boxes_to_result_in_mm(xr, parameters)
253
+ for xr in xrc_results
254
+ ]
216
255
  else:
217
256
  LOGGER.warning("No X-ray centre received")
218
257
  raise CrystalNotFoundException()
258
+ yield from _fire_xray_centre_result_event(flyscan_results)
219
259
 
220
- # once we have the results, go to the appropriate position
221
- LOGGER.info("Moving to centre of mass.")
222
- with TRACER.start_span("move_to_result"):
223
- x, y, z = xray_centre
224
- yield from move_x_y_z(fgs_composite.sample_motors, x, y, z, wait=True)
225
-
226
- if parameters.FGS_params.set_stub_offsets:
227
- LOGGER.info("Recentring smargon co-ordinate system to this point.")
228
- yield from bps.mv(
229
- fgs_composite.sample_motors.stub_offsets, # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
230
- StubPosition.CURRENT_AS_CENTER, # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
231
- )
232
260
  finally:
233
261
  # Turn off dev/shm streaming to avoid filling disk, see https://github.com/DiamondLightSource/hyperion/issues/1395
234
262
  LOGGER.info("Turning off Eiger dev/shm streaming")
@@ -239,11 +267,44 @@ def run_gridscan_and_move(
239
267
  yield from bps.wait()
240
268
 
241
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
+
242
303
  @bpp.set_run_key_decorator(CONST.PLAN.GRIDSCAN_MAIN)
243
304
  @bpp.run_decorator(md={"subplan_name": CONST.PLAN.GRIDSCAN_MAIN})
244
305
  def run_gridscan(
245
306
  fgs_composite: FlyScanXRayCentreComposite,
246
- parameters: ThreeDGridScan,
307
+ parameters: HyperionThreeDGridScan,
247
308
  feature_controlled: _FeatureControlled,
248
309
  md={ # noqa
249
310
  "plan_name": CONST.PLAN.GRIDSCAN_MAIN,
@@ -311,32 +372,7 @@ def wait_for_gridscan_valid(fgs_motors: FastGridScanCommon, timeout=0.5):
311
372
  LOGGER.info("Gridscan scan valid and position counter reset")
312
373
  return
313
374
  yield from bps.sleep(SLEEP_PER_CHECK)
314
- raise WarningException("Scan invalid - pin too long/short/bent and out of range")
315
-
316
-
317
- def set_aperture_for_bbox_size(
318
- aperture_device: ApertureScatterguard,
319
- bbox_size: list[int] | np.ndarray,
320
- ):
321
- # bbox_size is [x,y,z], for i03 we only care about x
322
- new_selected_aperture = (
323
- ApertureValue.MEDIUM if bbox_size[0] < 2 else ApertureValue.LARGE
324
- )
325
- LOGGER.info(
326
- f"Setting aperture to {new_selected_aperture} based on bounding box size {bbox_size}."
327
- )
328
-
329
- @bpp.set_run_key_decorator("change_aperture")
330
- @bpp.run_decorator(
331
- md={
332
- "subplan_name": "change_aperture",
333
- "aperture_size": new_selected_aperture.value,
334
- }
335
- )
336
- def set_aperture():
337
- yield from bps.abs_set(aperture_device, new_selected_aperture)
338
-
339
- yield from set_aperture()
375
+ raise SampleException("Scan invalid - pin too long/short/bent and out of range")
340
376
 
341
377
 
342
378
  @dataclasses.dataclass
@@ -350,7 +386,7 @@ class _FeatureControlled:
350
386
  def __call__(
351
387
  self,
352
388
  fgs_composite: FlyScanXRayCentreComposite,
353
- parameters: ThreeDGridScan,
389
+ parameters: HyperionThreeDGridScan,
354
390
  initial_xyz: np.ndarray,
355
391
  ) -> MsgGenerator: ...
356
392
 
@@ -362,7 +398,7 @@ class _FeatureControlled:
362
398
 
363
399
  def _get_feature_controlled(
364
400
  fgs_composite: FlyScanXRayCentreComposite,
365
- parameters: ThreeDGridScan,
401
+ parameters: HyperionThreeDGridScan,
366
402
  ):
367
403
  if parameters.features.use_panda_for_gridscan:
368
404
  return _FeatureControlled(
@@ -411,7 +447,7 @@ def _panda_tidy(fgs_composite: FlyScanXRayCentreComposite):
411
447
 
412
448
  def _zebra_triggering_setup(
413
449
  fgs_composite: FlyScanXRayCentreComposite,
414
- parameters: ThreeDGridScan,
450
+ parameters: HyperionThreeDGridScan,
415
451
  initial_xyz: np.ndarray,
416
452
  ):
417
453
  yield from setup_zebra_for_gridscan(
@@ -421,7 +457,7 @@ def _zebra_triggering_setup(
421
457
 
422
458
  def _panda_triggering_setup(
423
459
  fgs_composite: FlyScanXRayCentreComposite,
424
- parameters: ThreeDGridScan,
460
+ parameters: HyperionThreeDGridScan,
425
461
  initial_xyz: np.ndarray,
426
462
  ):
427
463
  LOGGER.info("Setting up Panda for flyscan")
@@ -440,15 +476,15 @@ def _panda_triggering_setup(
440
476
  )
441
477
 
442
478
  sample_velocity_mm_per_s = (
443
- 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
444
480
  )
445
481
  if sample_velocity_mm_per_s > smargon_speed_limit_mm_per_s:
446
482
  raise SmargonSpeedException(
447
483
  f"Smargon speed was calculated from x step size\
448
- {parameters.panda_FGS_params.x_step_size} and\
484
+ {parameters.panda_FGS_params.x_step_size_mm}mm and\
449
485
  time_between_x_steps_ms {time_between_x_steps_ms} as\
450
- {sample_velocity_mm_per_s}. The smargon's speed limit is\
451
- {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."
452
488
  )
453
489
  else:
454
490
  LOGGER.info(