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
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- import dataclasses
4
3
  from typing import cast
5
4
 
6
- from blueapi.core import BlueskyContext, MsgGenerator
5
+ import bluesky.preprocessors as bpp
6
+ import pydantic
7
+ from blueapi.core import BlueskyContext
8
+ from bluesky.utils import MsgGenerator
7
9
  from dodal.devices.aperturescatterguard import ApertureScatterguard
8
10
  from dodal.devices.attenuator import Attenuator
9
11
  from dodal.devices.backlight import Backlight
@@ -32,26 +34,36 @@ from dodal.log import LOGGER
32
34
  from ophyd_async.fastcs.panda import HDFPanda
33
35
 
34
36
  from mx_bluesky.common.parameters.constants import OavConstants
37
+ from mx_bluesky.common.parameters.gridscan import RobotLoadThenCentre
35
38
  from mx_bluesky.hyperion.device_setup_plans.utils import (
36
39
  fill_in_energy_if_not_supplied,
37
40
  start_preparing_data_collection_then_do_plan,
38
41
  )
42
+ from mx_bluesky.hyperion.experiment_plans.change_aperture_then_move_plan import (
43
+ change_aperture_then_move_to_xtal,
44
+ )
45
+ from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import (
46
+ XRayCentreEventHandler,
47
+ )
39
48
  from mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan import (
40
49
  GridDetectThenXRayCentreComposite,
41
50
  )
42
51
  from mx_bluesky.hyperion.experiment_plans.pin_centre_then_xray_centre_plan import (
43
- pin_centre_then_xray_centre_plan,
52
+ pin_centre_then_flyscan_plan,
44
53
  )
45
54
  from mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy import (
46
55
  RobotLoadAndEnergyChangeComposite,
47
56
  pin_already_loaded,
48
57
  robot_load_and_change_energy_plan,
49
58
  )
59
+ from mx_bluesky.hyperion.experiment_plans.set_energy_plan import (
60
+ SetEnergyComposite,
61
+ set_energy_plan,
62
+ )
50
63
  from mx_bluesky.hyperion.parameters.constants import CONST
51
- from mx_bluesky.hyperion.parameters.gridscan import RobotLoadThenCentre
52
64
 
53
65
 
54
- @dataclasses.dataclass
66
+ @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
55
67
  class RobotLoadThenCentreComposite:
56
68
  # common fields
57
69
  xbpm_feedback: XBPMFeedback
@@ -88,6 +100,10 @@ class RobotLoadThenCentreComposite:
88
100
  webcam: Webcam
89
101
  lower_gonio: XYZPositioner
90
102
 
103
+ @property
104
+ def sample_motors(self):
105
+ return self.smargon
106
+
91
107
 
92
108
  def create_devices(context: BlueskyContext) -> RobotLoadThenCentreComposite:
93
109
  from mx_bluesky.hyperion.utils.context import device_composite_from_context
@@ -95,18 +111,18 @@ def create_devices(context: BlueskyContext) -> RobotLoadThenCentreComposite:
95
111
  return device_composite_from_context(context, RobotLoadThenCentreComposite)
96
112
 
97
113
 
98
- def centring_plan_from_robot_load_params(
114
+ def _flyscan_plan_from_robot_load_params(
99
115
  composite: RobotLoadThenCentreComposite,
100
116
  params: RobotLoadThenCentre,
101
117
  oav_config_file: str = OavConstants.OAV_CONFIG_JSON,
102
118
  ):
103
- yield from pin_centre_then_xray_centre_plan(
119
+ yield from pin_centre_then_flyscan_plan(
104
120
  cast(GridDetectThenXRayCentreComposite, composite),
105
121
  params.pin_centre_then_xray_centre_params(),
106
122
  )
107
123
 
108
124
 
109
- def robot_load_then_centre_plan(
125
+ def _robot_load_then_flyscan_plan(
110
126
  composite: RobotLoadThenCentreComposite,
111
127
  params: RobotLoadThenCentre,
112
128
  oav_config_file: str = OavConstants.OAV_CONFIG_JSON,
@@ -116,13 +132,36 @@ def robot_load_then_centre_plan(
116
132
  params.robot_load_params(),
117
133
  )
118
134
 
119
- yield from centring_plan_from_robot_load_params(composite, params, oav_config_file)
135
+ yield from _flyscan_plan_from_robot_load_params(composite, params, oav_config_file)
120
136
 
121
137
 
122
138
  def robot_load_then_centre(
123
139
  composite: RobotLoadThenCentreComposite,
124
140
  parameters: RobotLoadThenCentre,
125
141
  ) -> MsgGenerator:
142
+ """Perform pin-tip detection followed by a flyscan to determine centres of interest.
143
+ Performs a robot load if necessary. Centre on the best diffracting centre.
144
+ """
145
+
146
+ xray_centre_event_handler = XRayCentreEventHandler()
147
+
148
+ yield from bpp.subs_wrapper(
149
+ robot_load_then_xray_centre(composite, parameters), xray_centre_event_handler
150
+ )
151
+ flyscan_results = xray_centre_event_handler.xray_centre_results
152
+ if flyscan_results is not None:
153
+ yield from change_aperture_then_move_to_xtal(
154
+ flyscan_results[0], composite.smargon, composite.aperture_scatterguard
155
+ )
156
+ # else no chi change, no need to recentre.
157
+
158
+
159
+ def robot_load_then_xray_centre(
160
+ composite: RobotLoadThenCentreComposite,
161
+ parameters: RobotLoadThenCentre,
162
+ ) -> MsgGenerator:
163
+ """Perform pin-tip detection followed by a flyscan to determine centres of interest.
164
+ Performs a robot load if necessary."""
126
165
  eiger: EigerDetector = composite.eiger
127
166
 
128
167
  # TODO: get these from one source of truth #254
@@ -138,17 +177,26 @@ def robot_load_then_centre(
138
177
  doing_chi_change = parameters.chi_start_deg is not None
139
178
 
140
179
  if doing_sample_load:
141
- plan = robot_load_then_centre_plan(
180
+ LOGGER.info("Pin not loaded, loading and centring")
181
+ plan = _robot_load_then_flyscan_plan(
142
182
  composite,
143
183
  parameters,
144
184
  )
145
- LOGGER.info("Pin not loaded, loading and centring")
146
- elif doing_chi_change:
147
- plan = centring_plan_from_robot_load_params(composite, parameters)
148
- LOGGER.info("Pin already loaded but chi changed so centring")
149
185
  else:
150
- LOGGER.info("Pin already loaded and chi not changed so doing nothing")
151
- return
186
+ # Robot load normally sets the energy so we should do this explicitly if no load is
187
+ # being done
188
+ demand_energy_ev = parameters.demand_energy_ev
189
+ LOGGER.info(f"Setting the energy to {demand_energy_ev}eV")
190
+ yield from set_energy_plan(
191
+ demand_energy_ev, cast(SetEnergyComposite, composite)
192
+ )
193
+
194
+ if doing_chi_change:
195
+ plan = _flyscan_plan_from_robot_load_params(composite, parameters)
196
+ LOGGER.info("Pin already loaded but chi changed so centring")
197
+ else:
198
+ LOGGER.info("Pin already loaded and chi not changed so doing nothing")
199
+ return
152
200
 
153
201
  detector_params = yield from fill_in_energy_if_not_supplied(
154
202
  composite.dcm, parameters.detector_params
@@ -4,7 +4,9 @@ import dataclasses
4
4
 
5
5
  import bluesky.plan_stubs as bps
6
6
  import bluesky.preprocessors as bpp
7
- from blueapi.core import BlueskyContext, MsgGenerator
7
+ import pydantic
8
+ from blueapi.core import BlueskyContext
9
+ from bluesky.utils import MsgGenerator
8
10
  from dodal.devices.aperturescatterguard import ApertureScatterguard
9
11
  from dodal.devices.attenuator import Attenuator
10
12
  from dodal.devices.backlight import Backlight
@@ -22,7 +24,7 @@ from dodal.devices.undulator import Undulator
22
24
  from dodal.devices.xbpm_feedback import XBPMFeedback
23
25
  from dodal.devices.zebra import RotationDirection, Zebra
24
26
  from dodal.devices.zebra_controlled_shutter import ZebraShutter
25
- from dodal.plans.check_topup import check_topup_and_wait_if_necessary
27
+ from dodal.plan_stubs.check_topup import check_topup_and_wait_if_necessary
26
28
 
27
29
  from mx_bluesky.common.device_setup_plans.read_hardware_for_setup import (
28
30
  read_hardware_for_zocalo,
@@ -62,7 +64,7 @@ from mx_bluesky.hyperion.parameters.rotation import (
62
64
  from mx_bluesky.hyperion.utils.context import device_composite_from_context
63
65
 
64
66
 
65
- @dataclasses.dataclass
67
+ @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
66
68
  class RotationScanComposite(OavSnapshotComposite):
67
69
  """All devices which are directly or indirectly required by this plan"""
68
70
 
@@ -409,6 +411,11 @@ def multi_rotation_scan(
409
411
  }
410
412
  )
411
413
  @bpp.stage_decorator([eiger])
414
+ @transmission_and_xbpm_feedback_for_collection_decorator(
415
+ composite.xbpm_feedback,
416
+ composite.attenuator,
417
+ parameters.transmission_frac,
418
+ )
412
419
  @bpp.finalize_decorator(lambda: _cleanup_plan(composite))
413
420
  def _multi_rotation_scan():
414
421
  for single_scan in parameters.single_rotation_scans:
@@ -418,6 +425,7 @@ def multi_rotation_scan(
418
425
  md={
419
426
  "subplan_name": CONST.PLAN.ROTATION_OUTER,
420
427
  CONST.TRIGGER.ZOCALO: CONST.PLAN.ROTATION_MAIN,
428
+ "zocalo_environment": CONST.ZOCALO_ENV,
421
429
  "hyperion_parameters": single_scan.model_dump_json(),
422
430
  }
423
431
  )
@@ -5,8 +5,7 @@
5
5
  * reenable feedback
6
6
  """
7
7
 
8
- import dataclasses
9
-
8
+ import pydantic
10
9
  from bluesky import plan_stubs as bps
11
10
  from dodal.devices.attenuator import Attenuator
12
11
  from dodal.devices.dcm import DCM
@@ -24,7 +23,7 @@ DESIRED_TRANSMISSION_FRACTION = 0.1
24
23
  UNDULATOR_GROUP = "UNDULATOR_GROUP"
25
24
 
26
25
 
27
- @dataclasses.dataclass
26
+ @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
28
27
  class SetEnergyComposite:
29
28
  vfm: FocusingMirrorWithStripes
30
29
  mirror_voltages: MirrorVoltages
@@ -49,12 +48,13 @@ def _set_energy_plan(
49
48
 
50
49
 
51
50
  def set_energy_plan(
52
- energy_kev,
51
+ energy_ev: float | None,
53
52
  composite: SetEnergyComposite,
54
53
  ):
55
- yield from transmission_and_xbpm_feedback_for_collection_wrapper(
56
- _set_energy_plan(energy_kev, composite),
57
- composite.xbpm_feedback,
58
- composite.attenuator,
59
- DESIRED_TRANSMISSION_FRACTION,
60
- )
54
+ if energy_ev:
55
+ yield from transmission_and_xbpm_feedback_for_collection_wrapper(
56
+ _set_energy_plan(energy_ev / 1000, composite),
57
+ composite.xbpm_feedback,
58
+ composite.attenuator,
59
+ DESIRED_TRANSMISSION_FRACTION,
60
+ )
@@ -4,7 +4,3 @@ execution of an experimental plan.
4
4
 
5
5
  Callbacks used for the Hyperion fast grid scan are prefixed with 'FGS'.
6
6
  """
7
-
8
- from .__main__ import main
9
-
10
- __all__ = ["main"]
@@ -20,6 +20,9 @@ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_callback
20
20
  from mx_bluesky.hyperion.external_interaction.callbacks.rotation.nexus_callback import (
21
21
  RotationNexusFileCallback,
22
22
  )
23
+ from mx_bluesky.hyperion.external_interaction.callbacks.sample_handling.sample_handling_callback import (
24
+ SampleHandlingCallback,
25
+ )
23
26
  from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import (
24
27
  GridscanISPyBCallback,
25
28
  )
@@ -49,6 +52,7 @@ def setup_callbacks():
49
52
  RotationISPyBCallback(emit=zocalo),
50
53
  LogUidTaggingCallback(),
51
54
  RobotLoadISPyBCallback(),
55
+ SampleHandlingCallback(),
52
56
  ]
53
57
 
54
58
 
@@ -0,0 +1,66 @@
1
+ import builtins
2
+ import dataclasses
3
+ import time
4
+ from abc import ABC
5
+ from typing import Literal
6
+
7
+ from bluesky.protocols import Readable, Reading
8
+ from event_model import DataKey
9
+
10
+
11
+ @dataclasses.dataclass(frozen=True)
12
+ class AbstractEvent(Readable, ABC):
13
+ """An abstract superclass that can be extended to provide lightweight software events
14
+ for bluesky plans, without having to incur the overhead of creating ophyd-async devices
15
+ specifically for the purpose.
16
+
17
+ The currently supported types for field annotations in the event are ``str``, ``int``, ``float``, ``bool``
18
+
19
+ In future array types may be supported.
20
+
21
+ Examples:
22
+ Subclasses should extend this class and decorate with::
23
+
24
+ @dataclasses.dataclass(frozen=True)
25
+
26
+ To raise an event, simply construct the event and then ``read`` it as you would a device::
27
+
28
+ yield from bps.create("MY_EVENT_NAME")
29
+ my_event = MyEvent(an_int=1)
30
+ yield from bps.read(my_event)
31
+ yield from bps.save()
32
+ """
33
+
34
+ def read(self) -> dict[str, Reading]:
35
+ return {
36
+ f.name: AbstractEvent._reading_from_value(getattr(self, f.name))
37
+ for f in dataclasses.fields(self)
38
+ }
39
+
40
+ def describe(self) -> dict[str, DataKey]:
41
+ return {
42
+ f.name: DataKey(dtype=AbstractEvent._dtype_of(f.type), shape=[], source="")
43
+ for f in dataclasses.fields(self)
44
+ }
45
+
46
+ @classmethod
47
+ def _reading_from_value(cls, value):
48
+ return Reading(timestamp=time.time(), value=value)
49
+
50
+ @classmethod
51
+ def _dtype_of(cls, t) -> Literal["string", "number", "boolean", "integer"]:
52
+ match t:
53
+ case builtins.str:
54
+ return "string"
55
+ case builtins.bool:
56
+ return "boolean"
57
+ case builtins.int:
58
+ return "integer"
59
+ case builtins.float:
60
+ return "number"
61
+ # TODO array support
62
+ raise ValueError(f"Unsupported type for AbstractEvent: {t}")
63
+
64
+ @property
65
+ def name(self) -> str:
66
+ return type(self).__name__
@@ -11,6 +11,9 @@ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_callback
11
11
  from mx_bluesky.hyperion.external_interaction.callbacks.rotation.nexus_callback import (
12
12
  RotationNexusFileCallback,
13
13
  )
14
+ from mx_bluesky.hyperion.external_interaction.callbacks.sample_handling.sample_handling_callback import (
15
+ SampleHandlingCallback,
16
+ )
14
17
  from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import (
15
18
  GridscanISPyBCallback,
16
19
  )
@@ -53,6 +56,7 @@ def create_load_centre_collect_callbacks() -> (
53
56
  RobotLoadISPyBCallback,
54
57
  RotationNexusFileCallback,
55
58
  RotationISPyBCallback,
59
+ SampleHandlingCallback,
56
60
  ]
57
61
  ):
58
62
  return (
@@ -61,4 +65,5 @@ def create_load_centre_collect_callbacks() -> (
61
65
  RobotLoadISPyBCallback(),
62
66
  RotationNexusFileCallback(),
63
67
  RotationISPyBCallback(emit=ZocaloCallback()),
68
+ SampleHandlingCallback(),
64
69
  )
@@ -28,7 +28,7 @@ class GridDetectionCallback(CallbackBase):
28
28
  *args,
29
29
  ) -> None:
30
30
  super().__init__(*args)
31
- self.start_positions: list = []
31
+ self.start_positions_mm: list = []
32
32
  self.box_numbers: list = []
33
33
 
34
34
  def event(self, doc: Event):
@@ -55,16 +55,16 @@ class GridDetectionCallback(CallbackBase):
55
55
  beam_x = data["oav-beam_centre_i"]
56
56
  beam_y = data["oav-beam_centre_j"]
57
57
 
58
- position_grid_start = calculate_x_y_z_of_pixel(
58
+ position_grid_start_mm = calculate_x_y_z_of_pixel(
59
59
  current_xyz,
60
60
  smargon_omega,
61
61
  centre_of_first_box,
62
62
  (beam_x, beam_y),
63
63
  (microns_per_pixel_x, microns_per_pixel_y),
64
64
  )
65
- LOGGER.info(f"Calculated start position {position_grid_start}")
65
+ LOGGER.info(f"Calculated start position {position_grid_start_mm}")
66
66
 
67
- self.start_positions.append(position_grid_start)
67
+ self.start_positions_mm.append(position_grid_start_mm)
68
68
  self.box_numbers.append(
69
69
  (
70
70
  data["oav-grid_snapshot-num_boxes_x"],
@@ -72,22 +72,22 @@ class GridDetectionCallback(CallbackBase):
72
72
  )
73
73
  )
74
74
 
75
- self.x_step_size_mm = box_width_px * microns_per_pixel_x / 1000
76
- self.y_step_size_mm = box_width_px * microns_per_pixel_y / 1000
77
- self.z_step_size_mm = box_width_px * microns_per_pixel_y / 1000
75
+ self.x_step_size_um = box_width_px * microns_per_pixel_x
76
+ self.y_step_size_um = box_width_px * microns_per_pixel_y
77
+ self.z_step_size_um = box_width_px * microns_per_pixel_y
78
78
  return doc
79
79
 
80
80
  def get_grid_parameters(self) -> GridParamUpdate:
81
81
  return {
82
- "x_start_um": self.start_positions[0][0],
83
- "y_start_um": self.start_positions[0][1],
84
- "y2_start_um": self.start_positions[0][1],
85
- "z_start_um": self.start_positions[1][2],
86
- "z2_start_um": self.start_positions[1][2],
82
+ "x_start_um": self.start_positions_mm[0][0] * 1000,
83
+ "y_start_um": self.start_positions_mm[0][1] * 1000,
84
+ "y2_start_um": self.start_positions_mm[0][1] * 1000,
85
+ "z_start_um": self.start_positions_mm[1][2] * 1000,
86
+ "z2_start_um": self.start_positions_mm[1][2] * 1000,
87
87
  "x_steps": self.box_numbers[0][0],
88
88
  "y_steps": self.box_numbers[0][1],
89
89
  "z_steps": self.box_numbers[1][1],
90
- "x_step_size_um": self.x_step_size_mm,
91
- "y_step_size_um": self.y_step_size_mm,
92
- "z_step_size_um": self.z_step_size_mm,
90
+ "x_step_size_um": self.x_step_size_um,
91
+ "y_step_size_um": self.y_step_size_um,
92
+ "z_step_size_um": self.z_step_size_um,
93
93
  }
@@ -2,8 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
- from event_model.documents import EventDescriptor
6
-
7
5
  from mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping import (
8
6
  get_proposal_and_session_from_visit_string,
9
7
  )
@@ -11,6 +9,7 @@ from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback i
11
9
  PlanReactiveCallback,
12
10
  )
13
11
  from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import (
12
+ BLSampleStatus,
14
13
  ExpeyeInteraction,
15
14
  RobotActionID,
16
15
  )
@@ -25,6 +24,7 @@ class RobotLoadISPyBCallback(PlanReactiveCallback):
25
24
  def __init__(self) -> None:
26
25
  ISPYB_LOGGER.debug("Initialising ISPyB Robot Load Callback")
27
26
  super().__init__(log=ISPYB_LOGGER)
27
+ self._metadata: dict | None = None
28
28
  self.run_uid: str | None = None
29
29
  self.descriptors: dict[str, EventDescriptor] = {}
30
30
  self.action_id: RobotActionID | None = None
@@ -35,16 +35,17 @@ class RobotLoadISPyBCallback(PlanReactiveCallback):
35
35
  if doc.get("subplan_name") == CONST.PLAN.ROBOT_LOAD:
36
36
  ISPYB_LOGGER.debug(f"ISPyB robot load callback received: {doc}")
37
37
  self.run_uid = doc.get("uid")
38
- assert isinstance(metadata := doc.get("metadata"), dict)
38
+ self._metadata = doc.get("metadata")
39
+ assert isinstance(self._metadata, dict)
39
40
  proposal, session = get_proposal_and_session_from_visit_string(
40
- metadata["visit"]
41
+ self._metadata["visit"]
41
42
  )
42
43
  self.action_id = self.expeye.start_load(
43
44
  proposal,
44
45
  session,
45
- metadata["sample_id"],
46
- metadata["sample_puck"],
47
- metadata["sample_pin"],
46
+ self._metadata["sample_id"],
47
+ self._metadata["sample_puck"],
48
+ self._metadata["sample_pin"],
48
49
  )
49
50
  return super().activity_gated_start(doc)
50
51
 
@@ -77,10 +78,17 @@ class RobotLoadISPyBCallback(PlanReactiveCallback):
77
78
  assert (
78
79
  self.action_id is not None
79
80
  ), "ISPyB Robot load callback stop called unexpectedly"
80
- exit_status = (
81
- doc.get("exit_status") or "Exit status not available in stop document!"
82
- )
81
+ exit_status = doc.get("exit_status")
82
+ assert exit_status, "Exit status not available in stop document!"
83
+ assert self._metadata, "Metadata not received before stop document."
83
84
  reason = doc.get("reason") or "OK"
85
+
84
86
  self.expeye.end_load(self.action_id, exit_status, reason)
87
+ self.expeye.update_sample_status(
88
+ self._metadata["sample_id"],
89
+ BLSampleStatus.LOADED
90
+ if exit_status == "success"
91
+ else BLSampleStatus.ERROR_BEAMLINE,
92
+ )
85
93
  self.action_id = None
86
94
  return super().activity_gated_stop(doc)
@@ -61,7 +61,9 @@ class RotationISPyBCallback(BaseISPyBCallback):
61
61
  ISPYB_LOGGER.info(
62
62
  "ISPyB callback received start document with experiment parameters."
63
63
  )
64
- self.params = RotationScan.from_json(doc.get("hyperion_parameters"))
64
+ hyperion_params = doc.get("hyperion_parameters")
65
+ assert isinstance(hyperion_params, str)
66
+ self.params = RotationScan.model_validate_json(hyperion_params)
65
67
  dcgid = (
66
68
  self.ispyb_ids.data_collection_group_id
67
69
  if (self.params.sample_id == self.last_sample_id)
@@ -78,12 +78,14 @@ class RotationNexusFileCallback(PlanReactiveCallback):
78
78
  self.meta_data_run_number = doc.get("meta_data_run_number")
79
79
  if doc.get("subplan_name") == CONST.PLAN.ROTATION_OUTER:
80
80
  self.run_uid = doc.get("uid")
81
- json_params = doc.get("hyperion_parameters")
81
+ hyperion_params = doc.get("hyperion_parameters")
82
+ assert isinstance(hyperion_params, str)
82
83
  NEXUS_LOGGER.info(
83
- f"Nexus writer received start document with experiment parameters {json_params}"
84
+ f"Nexus writer received start document with experiment parameters {hyperion_params}"
84
85
  )
85
- parameters = RotationScan.from_json(json_params)
86
+ parameters = RotationScan.model_validate_json(hyperion_params)
86
87
  NEXUS_LOGGER.info("Setting up nexus file...")
88
+
87
89
  det_size = (
88
90
  parameters.detector_params.detector_size_constants.det_size_pixels
89
91
  )
@@ -0,0 +1,84 @@
1
+ import dataclasses
2
+ from collections.abc import Generator
3
+ from functools import partial
4
+ from typing import Any
5
+
6
+ import bluesky.plan_stubs as bps
7
+ from bluesky.preprocessors import contingency_wrapper
8
+ from bluesky.utils import Msg, make_decorator
9
+ from event_model import Event, EventDescriptor, RunStart
10
+
11
+ from mx_bluesky.hyperion.exceptions import CrystalNotFoundException, SampleException
12
+ from mx_bluesky.hyperion.external_interaction.callbacks.common.abstract_event import (
13
+ AbstractEvent,
14
+ )
15
+ from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import (
16
+ PlanReactiveCallback,
17
+ )
18
+ from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import (
19
+ BLSampleStatus,
20
+ ExpeyeInteraction,
21
+ )
22
+ from mx_bluesky.hyperion.log import ISPYB_LOGGER
23
+ from mx_bluesky.hyperion.parameters.constants import CONST
24
+
25
+ # TODO remove this event-raising shenanigans once
26
+ # https://github.com/bluesky/bluesky/issues/1829 is addressed
27
+
28
+
29
+ @dataclasses.dataclass(frozen=True)
30
+ class _ExceptionEvent(AbstractEvent):
31
+ exception_type: str
32
+
33
+
34
+ def _exception_interceptor(exception: Exception) -> Generator[Msg, Any, Any]:
35
+ yield from bps.create(CONST.DESCRIPTORS.SAMPLE_HANDLING_EXCEPTION)
36
+ yield from bps.read(_ExceptionEvent(type(exception).__name__))
37
+ yield from bps.save()
38
+
39
+
40
+ sample_handling_callback_decorator = make_decorator(
41
+ partial(contingency_wrapper, except_plan=_exception_interceptor)
42
+ )
43
+
44
+
45
+ class SampleHandlingCallback(PlanReactiveCallback):
46
+ """Intercepts exceptions from experiment plans and updates the ISPyB BLSampleStatus
47
+ field according to the type of exception raised."""
48
+
49
+ def __init__(self):
50
+ super().__init__(log=ISPYB_LOGGER)
51
+ self._sample_id: int | None = None
52
+ self._descriptor: str | None = None
53
+
54
+ def activity_gated_start(self, doc: RunStart):
55
+ if not self._sample_id:
56
+ sample_id = doc.get("metadata", {}).get("sample_id")
57
+ self.log.info(f"Recording sample ID at run start {sample_id}")
58
+ self._sample_id = sample_id
59
+
60
+ def activity_gated_descriptor(self, doc: EventDescriptor) -> EventDescriptor | None:
61
+ if doc.get("name") == CONST.DESCRIPTORS.SAMPLE_HANDLING_EXCEPTION:
62
+ self._descriptor = doc["uid"]
63
+ return super().activity_gated_descriptor(doc)
64
+
65
+ def activity_gated_event(self, doc: Event) -> Event | None:
66
+ if doc["descriptor"] == self._descriptor:
67
+ exception_type = doc["data"]["exception_type"]
68
+ self.log.info(
69
+ f"Sample handling callback intercepted exception of type {exception_type}"
70
+ )
71
+ self._record_exception(exception_type)
72
+ return doc
73
+
74
+ def _record_exception(self, exception_type: str):
75
+ expeye = ExpeyeInteraction()
76
+ assert self._sample_id, "Unable to record exception due to no sample ID"
77
+ sample_status = self._decode_sample_status(exception_type)
78
+ expeye.update_sample_status(self._sample_id, sample_status)
79
+
80
+ def _decode_sample_status(self, exception_type: str) -> BLSampleStatus:
81
+ match exception_type:
82
+ case SampleException.__name__ | CrystalNotFoundException.__name__:
83
+ return BLSampleStatus.ERROR_SAMPLE
84
+ return BLSampleStatus.ERROR_BEAMLINE
@@ -5,8 +5,8 @@ from time import time
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
7
  import numpy as np
8
- from blueapi.core import MsgGenerator
9
8
  from bluesky import preprocessors as bpp
9
+ from bluesky.utils import MsgGenerator
10
10
  from dodal.devices.zocalo.zocalo_results import (
11
11
  ZOCALO_READING_PLAN_NAME,
12
12
  get_processing_results_from_event,
@@ -53,13 +53,16 @@ if TYPE_CHECKING:
53
53
 
54
54
 
55
55
  def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters):
56
- return bpp.run_wrapper(
57
- plan_generator,
58
- md={
59
- "activate_callbacks": ["GridscanISPyBCallback"],
60
- "subplan_name": CONST.PLAN.GRID_DETECT_AND_DO_GRIDSCAN,
61
- "hyperion_parameters": parameters.model_dump_json(),
62
- },
56
+ return bpp.set_run_key_wrapper(
57
+ bpp.run_wrapper(
58
+ plan_generator,
59
+ md={
60
+ "activate_callbacks": ["GridscanISPyBCallback"],
61
+ "subplan_name": CONST.PLAN.GRID_DETECT_AND_DO_GRIDSCAN,
62
+ "hyperion_parameters": parameters.model_dump_json(),
63
+ },
64
+ ),
65
+ CONST.PLAN.ISPYB_ACTIVATION,
63
66
  )
64
67
 
65
68
 
@@ -98,7 +101,9 @@ class GridscanISPyBCallback(BaseISPyBCallback):
98
101
  "ISPyB callback received start document with experiment parameters and "
99
102
  f"uid: {self.uid_to_finalize_on}"
100
103
  )
101
- self.params = GridCommon.from_json(doc.get("hyperion_parameters"))
104
+ hyperion_params = doc.get("hyperion_parameters")
105
+ assert isinstance(hyperion_params, str)
106
+ self.params = GridCommon.model_validate_json(hyperion_params)
102
107
  self.ispyb = StoreInIspyb(self.ispyb_config)
103
108
  data_collection_group_info = populate_data_collection_group(self.params)
104
109
 
@@ -152,6 +157,7 @@ class GridscanISPyBCallback(BaseISPyBCallback):
152
157
  ISPYB_LOGGER.info(
153
158
  f"Amending comment based on Zocalo reading doc: {format_doc_for_log(doc)}"
154
159
  )
160
+
155
161
  raw_results = get_processing_results_from_event("zocalo", doc)
156
162
  if len(raw_results) > 0:
157
163
  for n, res in enumerate(raw_results):