mx-bluesky 1.5.14__py3-none-any.whl → 1.5.16__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 (74) hide show
  1. mx_bluesky/Getting started.ipynb +1 -0
  2. mx_bluesky/_version.py +2 -2
  3. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +18 -0
  4. mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +6 -7
  5. mx_bluesky/beamlines/i04/thawing_plan.py +1 -0
  6. mx_bluesky/beamlines/i24/jungfrau_commissioning/__init__.py +13 -0
  7. mx_bluesky/beamlines/i24/jungfrau_commissioning/callbacks/__init__.py +0 -0
  8. mx_bluesky/beamlines/i24/jungfrau_commissioning/callbacks/metadata_writer.py +86 -0
  9. mx_bluesky/beamlines/i24/jungfrau_commissioning/composites.py +35 -0
  10. mx_bluesky/beamlines/i24/jungfrau_commissioning/experiment_plans/do_darks.py +18 -19
  11. mx_bluesky/beamlines/i24/jungfrau_commissioning/experiment_plans/rotation_scan_plan.py +292 -0
  12. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/do_external_acquisition.py +3 -8
  13. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/do_internal_acquisition.py +4 -5
  14. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/plan_utils.py +14 -18
  15. mx_bluesky/beamlines/i24/parameters/__init__.py +0 -0
  16. mx_bluesky/beamlines/i24/parameters/constants.py +9 -0
  17. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_extruder_collect_py3v2.py +1 -1
  18. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +2 -2
  19. mx_bluesky/beamlines/i24/serial/web_gui_plans/oav_plans.py +21 -0
  20. mx_bluesky/common/device_setup_plans/robot_load_unload.py +2 -24
  21. mx_bluesky/common/device_setup_plans/setup_zebra_and_shutter.py +5 -2
  22. mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +1 -1
  23. mx_bluesky/common/experiment_plans/inner_plans/read_hardware.py +2 -0
  24. mx_bluesky/common/experiment_plans/rotation/__init__.py +0 -0
  25. mx_bluesky/common/experiment_plans/rotation/rotation_utils.py +127 -0
  26. mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +14 -2
  27. mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +1 -2
  28. mx_bluesky/common/external_interaction/ispyb/data_model.py +4 -1
  29. mx_bluesky/common/external_interaction/ispyb/exp_eye_store.py +3 -1
  30. mx_bluesky/common/parameters/components.py +17 -7
  31. mx_bluesky/common/parameters/constants.py +6 -0
  32. mx_bluesky/{hyperion → common}/parameters/rotation.py +10 -8
  33. mx_bluesky/common/preprocessors/preprocessors.py +98 -36
  34. mx_bluesky/hyperion/__main__.py +55 -22
  35. mx_bluesky/hyperion/baton_handler.py +24 -64
  36. mx_bluesky/hyperion/blueapi_config.yaml +17 -0
  37. mx_bluesky/hyperion/blueapi_dev_config.yaml +16 -0
  38. mx_bluesky/hyperion/blueapi_plans/__init__.py +96 -0
  39. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +8 -6
  40. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +1 -1
  41. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +3 -1
  42. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +4 -5
  43. mx_bluesky/hyperion/experiment_plans/hyperion_grid_detect_then_xray_centre_plan.py +2 -2
  44. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +3 -1
  45. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +17 -6
  46. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +0 -3
  47. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +12 -126
  48. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +2 -2
  49. mx_bluesky/hyperion/experiment_plans/udc_default_state.py +8 -2
  50. mx_bluesky/hyperion/external_interaction/agamemnon.py +3 -8
  51. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +121 -47
  52. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +3 -1
  53. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +3 -1
  54. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +6 -3
  55. mx_bluesky/hyperion/external_interaction/callbacks/stomp/__init__.py +0 -0
  56. mx_bluesky/hyperion/external_interaction/callbacks/stomp/dispatcher.py +33 -0
  57. mx_bluesky/hyperion/in_process_runner.py +132 -0
  58. mx_bluesky/hyperion/parameters/cli.py +43 -4
  59. mx_bluesky/hyperion/parameters/components.py +13 -0
  60. mx_bluesky/hyperion/parameters/constants.py +2 -9
  61. mx_bluesky/hyperion/parameters/load_centre_collect.py +3 -1
  62. mx_bluesky/hyperion/plan_runner.py +45 -66
  63. mx_bluesky/hyperion/plan_runner_api.py +3 -4
  64. mx_bluesky/hyperion/supervisor/__init__.py +3 -0
  65. mx_bluesky/hyperion/supervisor/_supervisor.py +116 -0
  66. mx_bluesky/hyperion/supervisor/client_config.yaml +6 -0
  67. mx_bluesky/hyperion/supervisor/supervisor_config.yaml +10 -0
  68. mx_bluesky/hyperion/supervisor/supervisor_dev_config.yaml +9 -0
  69. {mx_bluesky-1.5.14.dist-info → mx_bluesky-1.5.16.dist-info}/METADATA +3 -31
  70. {mx_bluesky-1.5.14.dist-info → mx_bluesky-1.5.16.dist-info}/RECORD +74 -54
  71. {mx_bluesky-1.5.14.dist-info → mx_bluesky-1.5.16.dist-info}/WHEEL +0 -0
  72. {mx_bluesky-1.5.14.dist-info → mx_bluesky-1.5.16.dist-info}/entry_points.txt +0 -0
  73. {mx_bluesky-1.5.14.dist-info → mx_bluesky-1.5.16.dist-info}/licenses/LICENSE +0 -0
  74. {mx_bluesky-1.5.14.dist-info → mx_bluesky-1.5.16.dist-info}/top_level.txt +0 -0
@@ -13,6 +13,9 @@ from dodal.devices.oav.oav_parameters import OAVParameters
13
13
 
14
14
  import mx_bluesky.common.xrc_result as flyscan_result
15
15
  from mx_bluesky.common.parameters.components import WithSnapshot
16
+ from mx_bluesky.common.parameters.rotation import (
17
+ RotationScanPerSweep,
18
+ )
16
19
  from mx_bluesky.common.utils.context import device_composite_from_context
17
20
  from mx_bluesky.common.utils.exceptions import CrystalNotFoundError
18
21
  from mx_bluesky.common.utils.log import LOGGER
@@ -31,7 +34,6 @@ from mx_bluesky.hyperion.external_interaction.config_server import (
31
34
  )
32
35
  from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants
33
36
  from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
34
- from mx_bluesky.hyperion.parameters.rotation import RotationScanPerSweep
35
37
 
36
38
 
37
39
  @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
@@ -28,7 +28,10 @@ from mx_bluesky.common.experiment_plans.pin_tip_centring_plan import (
28
28
  from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import (
29
29
  ispyb_activation_wrapper,
30
30
  )
31
- from mx_bluesky.common.parameters.constants import OavConstants
31
+ from mx_bluesky.common.parameters.constants import OavConstants, PlanNameConstants
32
+ from mx_bluesky.common.preprocessors.preprocessors import (
33
+ pause_xbpm_feedback_during_collection_at_desired_transmission_decorator,
34
+ )
32
35
  from mx_bluesky.common.utils.context import device_composite_from_context
33
36
  from mx_bluesky.common.utils.log import LOGGER
34
37
  from mx_bluesky.common.xrc_result import XRayCentreEventHandler
@@ -103,13 +106,21 @@ def pin_centre_then_flyscan_plan(
103
106
  grid_detect_params = create_parameters_for_grid_detection(parameters)
104
107
  oav_params = OAVParameters("xrayCentring", oav_config_file)
105
108
 
106
- yield from detect_grid_and_do_gridscan(
109
+ @pause_xbpm_feedback_during_collection_at_desired_transmission_decorator(
107
110
  composite,
108
- grid_detect_params,
109
- oav_params,
110
- HyperionSpecifiedThreeDGridScan,
111
- construct_hyperion_specific_features,
111
+ parameters.transmission_frac,
112
+ PlanNameConstants.GRIDSCAN_OUTER,
112
113
  )
114
+ def _grid_detect_plan():
115
+ yield from detect_grid_and_do_gridscan(
116
+ composite,
117
+ grid_detect_params,
118
+ oav_params,
119
+ HyperionSpecifiedThreeDGridScan,
120
+ construct_hyperion_specific_features,
121
+ )
122
+
123
+ yield from _grid_detect_plan()
113
124
 
114
125
  yield from ispyb_activation_wrapper(_pin_centre_then_flyscan_plan(), parameters)
115
126
 
@@ -27,7 +27,6 @@ from dodal.devices.xbpm_feedback import XBPMFeedback
27
27
  from mx_bluesky.common.device_setup_plans.robot_load_unload import (
28
28
  do_plan_while_lower_gonio_at_home,
29
29
  prepare_for_robot_load,
30
- wait_for_smargon_not_disabled,
31
30
  )
32
31
  from mx_bluesky.hyperion.experiment_plans.set_energy_plan import (
33
32
  SetEnergyComposite,
@@ -95,8 +94,6 @@ def do_robot_load(
95
94
 
96
95
  yield from bps.wait("robot_load")
97
96
 
98
- yield from wait_for_smargon_not_disabled(composite.smargon)
99
-
100
97
  yield from bps.mv(composite.thawer, OnOff.ON)
101
98
 
102
99
 
@@ -1,7 +1,3 @@
1
- from __future__ import annotations
2
-
3
- import dataclasses
4
-
5
1
  import bluesky.plan_stubs as bps
6
2
  import bluesky.preprocessors as bpp
7
3
  import pydantic
@@ -25,7 +21,7 @@ from dodal.devices.synchrotron import Synchrotron
25
21
  from dodal.devices.thawer import Thawer
26
22
  from dodal.devices.undulator import UndulatorInKeV
27
23
  from dodal.devices.xbpm_feedback import XBPMFeedback
28
- from dodal.devices.zebra.zebra import RotationDirection, Zebra
24
+ from dodal.devices.zebra.zebra import Zebra
29
25
  from dodal.devices.zebra.zebra_controlled_shutter import ZebraShutter
30
26
  from dodal.plan_stubs.check_topup import check_topup_and_wait_if_necessary
31
27
  from dodal.plans.preprocessors.verify_undulator_gap import (
@@ -53,20 +49,24 @@ from mx_bluesky.common.experiment_plans.oav_snapshot_plan import (
53
49
  oav_snapshot_plan,
54
50
  setup_beamline_for_oav,
55
51
  )
52
+ from mx_bluesky.common.experiment_plans.rotation.rotation_utils import (
53
+ RotationMotionProfile,
54
+ calculate_motion_profile,
55
+ )
56
56
  from mx_bluesky.common.parameters.components import WithSnapshot
57
+ from mx_bluesky.common.parameters.rotation import (
58
+ RotationScan,
59
+ SingleRotationScan,
60
+ )
57
61
  from mx_bluesky.common.preprocessors.preprocessors import (
58
- transmission_and_xbpm_feedback_for_collection_decorator,
62
+ pause_xbpm_feedback_during_collection_at_desired_transmission_decorator,
59
63
  )
60
64
  from mx_bluesky.common.utils.context import device_composite_from_context
61
65
  from mx_bluesky.common.utils.log import LOGGER
62
66
  from mx_bluesky.hyperion.device_setup_plans.setup_zebra import (
63
67
  arm_zebra,
64
68
  )
65
- from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants
66
- from mx_bluesky.hyperion.parameters.rotation import (
67
- RotationScan,
68
- SingleRotationScan,
69
- )
69
+ from mx_bluesky.hyperion.parameters.constants import CONST
70
70
 
71
71
 
72
72
  @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
@@ -100,120 +100,6 @@ def create_devices(context: BlueskyContext) -> RotationScanComposite:
100
100
  return device_composite_from_context(context, RotationScanComposite)
101
101
 
102
102
 
103
- DEFAULT_DIRECTION = RotationDirection.NEGATIVE
104
- DEFAULT_MAX_VELOCITY = 120
105
- # Use a slightly larger time to acceleration than EPICS as it's better to be cautious
106
- ACCELERATION_MARGIN = 1.5
107
-
108
-
109
- @dataclasses.dataclass
110
- class RotationMotionProfile:
111
- start_scan_deg: float
112
- start_motion_deg: float
113
- scan_width_deg: float
114
- shutter_time_s: float
115
- direction: RotationDirection
116
- speed_for_rotation_deg_s: float
117
- acceleration_offset_deg: float
118
- shutter_opening_deg: float
119
- total_exposure_s: float
120
- distance_to_move_deg: float
121
- max_velocity_deg_s: float
122
-
123
-
124
- def calculate_motion_profile(
125
- params: SingleRotationScan,
126
- motor_time_to_speed_s: float,
127
- max_velocity_deg_s: float,
128
- ) -> RotationMotionProfile:
129
- """Calculates the various numbers needed for motions in the rotation scan.
130
- Rotates through "scan width" plus twice an "offset" to take into account
131
- acceleration at the start and deceleration at the end, plus the number of extra
132
- degrees of rotation needed to make sure the fast shutter has fully opened before the
133
- detector trigger is sent.
134
- See https://github.com/DiamondLightSource/hyperion/wiki/rotation-scan-geometry
135
- for a simple pictorial explanation."""
136
-
137
- assert params.rotation_increment_deg > 0
138
-
139
- direction = params.rotation_direction
140
- start_scan_deg = params.omega_start_deg
141
-
142
- if I03Constants.OMEGA_FLIP:
143
- # If omega_flip is True then the motor omega axis is inverted with respect to the
144
- # hyperion coordinate system.
145
- start_scan_deg = -start_scan_deg
146
- direction = (
147
- direction.POSITIVE
148
- if direction == direction.NEGATIVE
149
- else direction.NEGATIVE
150
- )
151
-
152
- num_images = params.num_images
153
- shutter_time_s = params.shutter_opening_time_s
154
- image_width_deg = params.rotation_increment_deg
155
- exposure_time_s = params.exposure_time_s
156
- motor_time_to_speed_s *= ACCELERATION_MARGIN
157
-
158
- LOGGER.info("Calculating rotation scan motion profile:")
159
- LOGGER.info(
160
- f"{num_images=}, {shutter_time_s=}, {image_width_deg=}, {exposure_time_s=}, {direction=}"
161
- )
162
-
163
- scan_width_deg = num_images * params.rotation_increment_deg
164
- LOGGER.info(f"{scan_width_deg=} = {num_images=} * {params.rotation_increment_deg=}")
165
-
166
- speed_for_rotation_deg_s = image_width_deg / exposure_time_s
167
- LOGGER.info("speed_for_rotation_deg_s = image_width_deg / exposure_time_s")
168
- LOGGER.info(
169
- f"{speed_for_rotation_deg_s=} = {image_width_deg=} / {exposure_time_s=}"
170
- )
171
-
172
- acceleration_offset_deg = motor_time_to_speed_s * speed_for_rotation_deg_s
173
- LOGGER.info(
174
- f"{acceleration_offset_deg=} = {motor_time_to_speed_s=} * {speed_for_rotation_deg_s=}"
175
- )
176
-
177
- start_motion_deg = start_scan_deg - (acceleration_offset_deg * direction.multiplier)
178
- LOGGER.info(
179
- f"{start_motion_deg=} = {start_scan_deg=} - ({acceleration_offset_deg=} * {direction.multiplier=})"
180
- )
181
-
182
- shutter_opening_deg = speed_for_rotation_deg_s * shutter_time_s
183
- LOGGER.info(
184
- f"{shutter_opening_deg=} = {speed_for_rotation_deg_s=} * {shutter_time_s=}"
185
- )
186
-
187
- shutter_opening_deg = speed_for_rotation_deg_s * shutter_time_s
188
- LOGGER.info(
189
- f"{shutter_opening_deg=} = {speed_for_rotation_deg_s=} * {shutter_time_s=}"
190
- )
191
-
192
- total_exposure_s = num_images * exposure_time_s
193
- LOGGER.info(f"{total_exposure_s=} = {num_images=} * {exposure_time_s=}")
194
-
195
- distance_to_move_deg = (
196
- scan_width_deg + shutter_opening_deg + acceleration_offset_deg * 2
197
- ) * direction.multiplier
198
- LOGGER.info(
199
- f"{distance_to_move_deg=} = ({scan_width_deg=} + {shutter_opening_deg=} + {acceleration_offset_deg=} * 2) * {direction=})"
200
- )
201
-
202
- return RotationMotionProfile(
203
- start_scan_deg=start_scan_deg,
204
- start_motion_deg=start_motion_deg,
205
- scan_width_deg=scan_width_deg,
206
- shutter_time_s=shutter_time_s,
207
- direction=direction,
208
- speed_for_rotation_deg_s=speed_for_rotation_deg_s,
209
- acceleration_offset_deg=acceleration_offset_deg,
210
- shutter_opening_deg=shutter_opening_deg,
211
- total_exposure_s=total_exposure_s,
212
- distance_to_move_deg=distance_to_move_deg,
213
- max_velocity_deg_s=max_velocity_deg_s,
214
- )
215
-
216
-
217
103
  def rotation_scan_plan(
218
104
  composite: RotationScanComposite,
219
105
  params: SingleRotationScan,
@@ -399,7 +285,7 @@ def rotation_scan_internal(
399
285
  eiger: EigerDetector = composite.eiger
400
286
  eiger.set_detector_parameters(parameters.detector_params)
401
287
 
402
- @transmission_and_xbpm_feedback_for_collection_decorator(
288
+ @pause_xbpm_feedback_during_collection_at_desired_transmission_decorator(
403
289
  composite,
404
290
  parameters.transmission_frac,
405
291
  )
@@ -17,7 +17,7 @@ from dodal.devices.xbpm_feedback import XBPMFeedback
17
17
 
18
18
  from mx_bluesky.common.parameters.constants import PlanNameConstants
19
19
  from mx_bluesky.common.preprocessors.preprocessors import (
20
- transmission_and_xbpm_feedback_for_collection_wrapper,
20
+ pause_xbpm_feedback_during_collection_at_desired_transmission_wrapper,
21
21
  )
22
22
  from mx_bluesky.hyperion.device_setup_plans import dcm_pitch_roll_mirror_adjuster
23
23
 
@@ -74,7 +74,7 @@ def set_energy_plan(
74
74
  )
75
75
 
76
76
  if energy_ev:
77
- yield from transmission_and_xbpm_feedback_for_collection_wrapper(
77
+ yield from pause_xbpm_feedback_during_collection_at_desired_transmission_wrapper(
78
78
  _set_energy_plan(energy_ev / 1000, composite),
79
79
  composite_for_wrapper,
80
80
  DESIRED_TRANSMISSION_FRACTION,
@@ -17,6 +17,7 @@ from dodal.devices.fluorescence_detector_motion import FluorescenceDetector
17
17
  from dodal.devices.fluorescence_detector_motion import InOut as FlouInOut
18
18
  from dodal.devices.hutch_shutter import HutchShutter, ShutterDemand
19
19
  from dodal.devices.mx_phase1.beamstop import BeamstopPositions
20
+ from dodal.devices.oav.oav_detector import OAV
20
21
  from dodal.devices.robot import BartRobot, PinMounted
21
22
  from dodal.devices.scintillator import InOut as ScinInOut
22
23
  from dodal.devices.scintillator import Scintillator
@@ -49,6 +50,7 @@ class UDCDefaultDevices(BeamstopCheckDevices):
49
50
  robot: BartRobot
50
51
  scintillator: Scintillator
51
52
  smargon: Smargon
53
+ oav: OAV
52
54
 
53
55
 
54
56
  class UnexpectedSampleError(BeamlineCheckFailureError): ...
@@ -109,7 +111,7 @@ def move_to_udc_default_state(devices: UDCDefaultDevices):
109
111
  )
110
112
 
111
113
  # Wait for all of the above to complete
112
- yield from bps.wait(group=_GROUP_PRE_BEAMSTOP_CHECK, timeout=0.1)
114
+ yield from bps.wait(group=_GROUP_PRE_BEAMSTOP_CHECK, timeout=10)
113
115
 
114
116
  feature_flags: HyperionFeatureSettings = (
115
117
  get_hyperion_config_client().get_feature_flags()
@@ -141,7 +143,11 @@ def move_to_udc_default_state(devices: UDCDefaultDevices):
141
143
  devices.cryojet.fine, CryoInOut.IN, group=_GROUP_POST_BEAMSTOP_CHECK
142
144
  )
143
145
 
144
- yield from bps.wait(_GROUP_POST_BEAMSTOP_CHECK)
146
+ yield from bps.abs_set(
147
+ devices.oav.zoom_controller, "1.0x", group=_GROUP_POST_BEAMSTOP_CHECK
148
+ )
149
+
150
+ yield from bps.wait(_GROUP_POST_BEAMSTOP_CHECK, timeout=10)
145
151
 
146
152
 
147
153
  def _verify_correct_cryostream_selected(
@@ -12,12 +12,11 @@ import requests
12
12
  from deepdiff.diff import DeepDiff
13
13
  from dodal.utils import get_beamline_name
14
14
  from jsonschema import ValidationError
15
- from pydantic_extra_types.semantic_version import SemanticVersion
16
15
 
17
16
  from mx_bluesky.common.parameters.components import (
18
- PARAMETER_VERSION,
19
17
  MxBlueskyParameters,
20
18
  WithVisit,
19
+ get_param_version,
21
20
  )
22
21
  from mx_bluesky.common.parameters.constants import (
23
22
  GridscanParamConstants,
@@ -88,7 +87,7 @@ def create_parameters_from_agamemnon() -> Sequence[MxBlueskyParameters]:
88
87
  Wait.model_validate(
89
88
  {
90
89
  "duration_s": data,
91
- "parameter_model_version": _get_param_version(),
90
+ "parameter_model_version": get_param_version(),
92
91
  }
93
92
  )
94
93
  ]
@@ -228,10 +227,6 @@ def _get_withenergy_parameters_from_agamemnon(parameters: dict) -> dict[str, Any
228
227
  return {"demand_energy_ev": None}
229
228
 
230
229
 
231
- def _get_param_version() -> SemanticVersion:
232
- return SemanticVersion.validate_from_str(str(PARAMETER_VERSION))
233
-
234
-
235
230
  def _populate_parameters_from_agamemnon(
236
231
  agamemnon_params,
237
232
  ) -> Sequence[LoadCentreCollect]:
@@ -250,7 +245,7 @@ def _populate_parameters_from_agamemnon(
250
245
  return [
251
246
  LoadCentreCollect.model_validate(
252
247
  {
253
- "parameter_model_version": _get_param_version(),
248
+ "parameter_model_version": get_param_version(),
254
249
  "visit": visit,
255
250
  "detector_distance_mm": detector_distance,
256
251
  "sample_id": agamemnon_params["sample"]["id"],
@@ -1,12 +1,17 @@
1
1
  import logging
2
- from collections.abc import Callable, Sequence
2
+ from abc import abstractmethod
3
+ from collections.abc import Callable
4
+ from contextlib import AbstractContextManager
3
5
  from threading import Thread
4
6
  from time import sleep # noqa
5
7
  from urllib import request
6
8
  from urllib.error import URLError
7
9
 
10
+ from blueapi.config import ApplicationConfig, ConfigLoader
8
11
  from bluesky.callbacks import CallbackBase
9
12
  from bluesky.callbacks.zmq import Proxy, RemoteDispatcher
13
+ from bluesky_stomp.messaging import StompClient
14
+ from bluesky_stomp.models import Broker
10
15
  from dodal.log import LOGGER as DODAL_LOGGER
11
16
  from dodal.log import set_up_all_logging_handlers
12
17
 
@@ -52,8 +57,11 @@ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.nexus_callback
52
57
  from mx_bluesky.hyperion.external_interaction.callbacks.snapshot_callback import (
53
58
  BeamDrawingCallback,
54
59
  )
55
- from mx_bluesky.hyperion.parameters.cli import parse_callback_dev_mode_arg
56
- from mx_bluesky.hyperion.parameters.constants import CONST, HyperionConstants
60
+ from mx_bluesky.hyperion.external_interaction.callbacks.stomp.dispatcher import (
61
+ StompDispatcher,
62
+ )
63
+ from mx_bluesky.hyperion.parameters.cli import CallbackArgs, parse_callback_args
64
+ from mx_bluesky.hyperion.parameters.constants import CONST
57
65
  from mx_bluesky.hyperion.parameters.gridscan import (
58
66
  GridCommonWithHyperionDetectorParams,
59
67
  HyperionSpecifiedThreeDGridScan,
@@ -143,29 +151,72 @@ def log_debug(msg, *args, **kwargs):
143
151
  NEXUS_LOGGER.debug(msg, *args, **kwargs)
144
152
 
145
153
 
146
- def wait_for_threads_forever(threads: Sequence[Thread]):
147
- alive = [t.is_alive() for t in threads]
148
- try:
149
- log_debug("Trying to wait forever on callback and dispatcher threads")
150
- while all(alive):
151
- sleep(LIVENESS_POLL_SECONDS)
152
- alive = [t.is_alive() for t in threads]
153
- except KeyboardInterrupt:
154
- log_info("Main thread received interrupt - exiting.")
155
- else:
156
- log_info("Proxy or dispatcher thread ended - exiting.")
157
-
158
-
159
154
  class HyperionCallbackRunner:
160
155
  """Runs Nexus, ISPyB and Zocalo callbacks in their own process."""
161
156
 
162
- def __init__(self, dev_mode) -> None:
163
- setup_logging(dev_mode)
157
+ def __init__(self, callback_args: CallbackArgs) -> None:
158
+ setup_logging(callback_args.dev_mode)
164
159
  log_info("Hyperion callback process started.")
165
160
  set_alerting_service(LoggingAlertService(CONST.GRAYLOG_STREAM_ID))
166
161
 
167
162
  self.callbacks = setup_callbacks()
168
163
 
164
+ self.watchdog_thread = Thread(
165
+ target=run_watchdog,
166
+ daemon=True,
167
+ name="Watchdog",
168
+ args=[callback_args.watchdog_port],
169
+ )
170
+
171
+ self._dispatcher_cm: DispatcherContextMgr
172
+ if callback_args.stomp_config:
173
+ self._dispatcher_cm = StompDispatcherContextMgr(
174
+ callback_args, self.callbacks
175
+ )
176
+ else:
177
+ self._dispatcher_cm = RemoteDispatcherContextMgr(self.callbacks)
178
+
179
+ def start(self):
180
+ log_info(f"Launching threads, with callbacks: {self.callbacks}")
181
+ self.watchdog_thread.start()
182
+ with self._dispatcher_cm:
183
+ ping_watchdog_while_alive(self._dispatcher_cm, self.watchdog_thread)
184
+
185
+
186
+ def run_watchdog(watchdog_port: int):
187
+ log_info("Hyperion watchdog keepalive running")
188
+ while True:
189
+ try:
190
+ with request.urlopen(
191
+ f"http://localhost:{watchdog_port}/callbackPing",
192
+ timeout=PING_TIMEOUT_S,
193
+ ) as response:
194
+ if response.status != 200:
195
+ log_debug(
196
+ f"Unable to ping Hyperion liveness endpoint, status {response.status}"
197
+ )
198
+ except URLError as e:
199
+ log_debug("Unable to ping Hyperion liveness endpoint", exc_info=e)
200
+ sleep(HYPERION_PING_INTERVAL_S)
201
+
202
+
203
+ def main(dev_mode=False) -> None:
204
+ callback_args = parse_callback_args()
205
+ callback_args.dev_mode = dev_mode or callback_args.dev_mode
206
+ print(f"In dev mode: {dev_mode}")
207
+ runner = HyperionCallbackRunner(callback_args)
208
+ runner.start()
209
+
210
+
211
+ class DispatcherContextMgr(AbstractContextManager):
212
+ @abstractmethod
213
+ def is_alive(self) -> bool: ...
214
+
215
+
216
+ class RemoteDispatcherContextMgr(DispatcherContextMgr):
217
+ def __init__(self, callbacks: list[CallbackBase]):
218
+ super().__init__()
219
+
169
220
  self.proxy = Proxy(*CONST.CALLBACK_0MQ_PROXY_PORTS)
170
221
  self.proxy_thread = Thread(
171
222
  target=self.proxy.start, daemon=True, name="0MQ Proxy"
@@ -182,47 +233,70 @@ class HyperionCallbackRunner:
182
233
 
183
234
  self.dispatcher_thread = Thread(
184
235
  target=start_dispatcher,
185
- args=[self.callbacks],
236
+ args=[callbacks],
186
237
  daemon=True,
187
238
  name="0MQ Dispatcher",
188
239
  )
189
-
190
- self.watchdog_thread = Thread(target=run_watchdog, daemon=True, name="Watchdog")
191
240
  log_info("Created 0MQ proxy and local RemoteDispatcher.")
192
241
 
193
- def start(self):
194
- log_info(f"Launching threads, with callbacks: {self.callbacks}")
242
+ def __enter__(self):
243
+ log_info("Proxy and dispatcher thread launched.")
195
244
  self.proxy_thread.start()
196
245
  self.dispatcher_thread.start()
197
- self.watchdog_thread.start()
198
- log_info("Proxy and dispatcher thread launched.")
199
- wait_for_threads_forever(
200
- [self.proxy_thread, self.dispatcher_thread, self.watchdog_thread]
246
+ return self
247
+
248
+ def __exit__(self, exc_type, exc_value, traceback, /):
249
+ self.dispatcher.stop()
250
+ # proxy has no way to stop
251
+
252
+ def is_alive(self):
253
+ return self.proxy_thread.is_alive() and self.dispatcher_thread.is_alive()
254
+
255
+
256
+ class StompDispatcherContextMgr(DispatcherContextMgr):
257
+ def __init__(self, args: CallbackArgs, callbacks: list[CallbackBase]):
258
+ super().__init__()
259
+ loader = ConfigLoader(ApplicationConfig)
260
+ loader.use_values_from_yaml(args.stomp_config)
261
+ config = loader.load()
262
+ log_info(
263
+ f"Stomp client configured on {config.stomp.url.host}:{config.stomp.url.port}"
264
+ )
265
+ self._stomp_client = StompClient.for_broker(
266
+ broker=Broker(
267
+ host=config.stomp.url.host,
268
+ port=config.stomp.url.port,
269
+ auth=config.stomp.auth,
270
+ )
201
271
  )
272
+ self._dispatcher = StompDispatcher(self._stomp_client)
273
+ for cb in callbacks:
274
+ self._dispatcher.subscribe(cb)
202
275
 
276
+ def is_alive(self) -> bool:
277
+ return self._stomp_client.is_connected()
203
278
 
204
- def run_watchdog():
205
- log_info("Hyperion watchdog keepalive running")
206
- while True:
207
- try:
208
- with request.urlopen(
209
- f"http://localhost:{HyperionConstants.HYPERION_PORT}/callbackPing",
210
- timeout=PING_TIMEOUT_S,
211
- ) as response:
212
- if response.status != 200:
213
- log_debug(
214
- f"Unable to ping Hyperion liveness endpoint, status {response.status}"
215
- )
216
- except URLError as e:
217
- log_debug("Unable to ping Hyperion liveness endpoint", exc_info=e)
218
- sleep(HYPERION_PING_INTERVAL_S)
279
+ def __enter__(self):
280
+ self._dispatcher.__enter__()
281
+ return self
219
282
 
283
+ def __exit__(self, exc_type, exc_value, traceback, /):
284
+ self._dispatcher.__exit__(exc_type, exc_value, traceback)
220
285
 
221
- def main(dev_mode=False) -> None:
222
- dev_mode = dev_mode or parse_callback_dev_mode_arg()
223
- print(f"In dev mode: {dev_mode}")
224
- runner = HyperionCallbackRunner(dev_mode)
225
- runner.start()
286
+
287
+ def ping_watchdog_while_alive(
288
+ dispatcher_cm: DispatcherContextMgr, watchdog_thread: Thread
289
+ ):
290
+ alive = watchdog_thread.is_alive() and dispatcher_cm.is_alive()
291
+ try:
292
+ log_debug("Trying to wait forever on callback and dispatcher threads")
293
+ while alive:
294
+ sleep(LIVENESS_POLL_SECONDS)
295
+ alive = watchdog_thread.is_alive() and dispatcher_cm.is_alive()
296
+ except KeyboardInterrupt:
297
+ log_info("Main thread received interrupt - exiting.")
298
+ else:
299
+ log_info("Proxy or dispatcher thread ended - exiting.")
226
300
 
227
301
 
228
302
  if __name__ == "__main__":
@@ -25,13 +25,15 @@ from mx_bluesky.common.external_interaction.ispyb.ispyb_store import (
25
25
  StoreInIspyb,
26
26
  )
27
27
  from mx_bluesky.common.parameters.components import IspybExperimentType
28
+ from mx_bluesky.common.parameters.rotation import (
29
+ SingleRotationScan,
30
+ )
28
31
  from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, set_dcgid_tag
29
32
  from mx_bluesky.common.utils.utils import number_of_frames_from_scan_spec
30
33
  from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_mapping import (
31
34
  populate_data_collection_info_for_rotation,
32
35
  )
33
36
  from mx_bluesky.hyperion.parameters.constants import CONST
34
- from mx_bluesky.hyperion.parameters.rotation import SingleRotationScan
35
37
 
36
38
  if TYPE_CHECKING:
37
39
  from event_model.documents import Event, RunStart, RunStop
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from mx_bluesky.common.external_interaction.ispyb.data_model import DataCollectionInfo
4
- from mx_bluesky.hyperion.parameters.rotation import SingleRotationScan
4
+ from mx_bluesky.common.parameters.rotation import (
5
+ SingleRotationScan,
6
+ )
5
7
 
6
8
 
7
9
  def populate_data_collection_info_for_rotation(params: SingleRotationScan):
@@ -11,9 +11,12 @@ from mx_bluesky.common.external_interaction.nexus.nexus_utils import (
11
11
  vds_type_based_on_bit_depth,
12
12
  )
13
13
  from mx_bluesky.common.external_interaction.nexus.write_nexus import NexusWriter
14
+ from mx_bluesky.common.parameters.constants import RotationParamConstants
15
+ from mx_bluesky.common.parameters.rotation import (
16
+ SingleRotationScan,
17
+ )
14
18
  from mx_bluesky.common.utils.log import NEXUS_LOGGER, format_doc_for_log
15
- from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants
16
- from mx_bluesky.hyperion.parameters.rotation import SingleRotationScan
19
+ from mx_bluesky.hyperion.parameters.constants import CONST
17
20
 
18
21
  if TYPE_CHECKING:
19
22
  from event_model.documents import Event, EventDescriptor, RunStart
@@ -100,6 +103,6 @@ class RotationNexusFileCallback(PlanReactiveCallback):
100
103
  full_num_of_images=self.full_num_of_images,
101
104
  meta_data_run_number=self.meta_data_run_number,
102
105
  axis_direction=AxisDirection.NEGATIVE
103
- if I03Constants.OMEGA_FLIP
106
+ if RotationParamConstants.OMEGA_FLIP
104
107
  else AxisDirection.POSITIVE,
105
108
  )
@@ -0,0 +1,33 @@
1
+ from blueapi.client.event_bus import AnyEvent
2
+ from blueapi.core import DataEvent
3
+ from bluesky.run_engine import Dispatcher
4
+ from bluesky_stomp.messaging import MessageContext, StompClient
5
+ from bluesky_stomp.models import MessageTopic
6
+ from event_model import DocumentNames
7
+
8
+ from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER as LOGGER
9
+
10
+ BLUEAPI_EVENT_TOPIC = "public.worker.event"
11
+
12
+
13
+ class StompDispatcher(Dispatcher):
14
+ def __init__(self, stomp_client: StompClient):
15
+ super().__init__()
16
+ self._client = stomp_client
17
+
18
+ def __enter__(self):
19
+ self._subscription_id = self._client.subscribe(
20
+ MessageTopic(name=BLUEAPI_EVENT_TOPIC), self._on_event
21
+ )
22
+ LOGGER.info("Connecting to stomp broker...")
23
+ self._client.connect()
24
+
25
+ def __exit__(self, exc_type, exc_val, exc_tb):
26
+ LOGGER.info("Disconnecting from stomp and unsubscribing...")
27
+ self._client.disconnect()
28
+ self._client.unsubscribe(self._subscription_id)
29
+
30
+ def _on_event(self, event: AnyEvent, context: MessageContext):
31
+ match event:
32
+ case DataEvent(name=name, doc=doc): # type: ignore
33
+ self.process(DocumentNames[name], doc)