mx-bluesky 1.5.1__py3-none-any.whl → 1.5.3__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 (57) hide show
  1. mx_bluesky/_version.py +16 -3
  2. mx_bluesky/beamlines/i04/__init__.py +8 -1
  3. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +56 -1
  4. mx_bluesky/beamlines/i04/experiment_plans/__init__.py +0 -0
  5. mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +262 -0
  6. mx_bluesky/beamlines/i24/serial/blueapi_config.yaml +2 -2
  7. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +3 -1
  8. mx_bluesky/common/experiment_plans/change_aperture_then_move_plan.py +5 -1
  9. mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +26 -3
  10. mx_bluesky/common/experiment_plans/common_grid_detect_then_xray_centre_plan.py +1 -0
  11. mx_bluesky/common/experiment_plans/inner_plans/do_fgs.py +3 -1
  12. mx_bluesky/common/experiment_plans/oav_grid_detection_plan.py +12 -2
  13. mx_bluesky/common/external_interaction/alerting/__init__.py +13 -0
  14. mx_bluesky/common/external_interaction/alerting/_service.py +82 -0
  15. mx_bluesky/common/external_interaction/alerting/log_based_service.py +57 -0
  16. mx_bluesky/common/external_interaction/callbacks/sample_handling/sample_handling_callback.py +28 -4
  17. mx_bluesky/common/external_interaction/config_server.py +151 -54
  18. mx_bluesky/common/external_interaction/ispyb/ispyb_store.py +11 -6
  19. mx_bluesky/common/parameters/__init__.py +0 -0
  20. mx_bluesky/common/parameters/constants.py +27 -8
  21. mx_bluesky/common/parameters/device_composites.py +1 -1
  22. mx_bluesky/common/parameters/gridscan.py +2 -1
  23. mx_bluesky/hyperion/__main__.py +51 -179
  24. mx_bluesky/hyperion/baton_handler.py +142 -54
  25. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +29 -24
  26. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +4 -93
  27. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +23 -38
  28. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +12 -4
  29. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +1 -1
  30. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +7 -8
  31. mx_bluesky/hyperion/external_interaction/agamemnon.py +128 -73
  32. mx_bluesky/hyperion/external_interaction/alerting/__init__.py +0 -0
  33. mx_bluesky/hyperion/external_interaction/alerting/constants.py +12 -0
  34. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +5 -0
  35. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +2 -2
  36. mx_bluesky/hyperion/external_interaction/config_server.py +12 -31
  37. mx_bluesky/hyperion/parameters/cli.py +15 -3
  38. mx_bluesky/hyperion/parameters/components.py +7 -5
  39. mx_bluesky/hyperion/parameters/constants.py +21 -6
  40. mx_bluesky/hyperion/parameters/gridscan.py +22 -14
  41. mx_bluesky/hyperion/parameters/load_centre_collect.py +1 -14
  42. mx_bluesky/hyperion/parameters/robot_load.py +1 -4
  43. mx_bluesky/hyperion/parameters/rotation.py +1 -2
  44. mx_bluesky/hyperion/plan_runner.py +78 -0
  45. mx_bluesky/hyperion/runner.py +189 -0
  46. mx_bluesky/hyperion/utils/context.py +19 -5
  47. mx_bluesky/phase1_zebra/__init__.py +1 -0
  48. mx_bluesky/phase1_zebra/device_setup_plans/__init__.py +0 -0
  49. mx_bluesky/phase1_zebra/device_setup_plans/setup_zebra.py +112 -0
  50. {mx_bluesky-1.5.1.dist-info → mx_bluesky-1.5.3.dist-info}/METADATA +5 -4
  51. {mx_bluesky-1.5.1.dist-info → mx_bluesky-1.5.3.dist-info}/RECORD +57 -44
  52. {mx_bluesky-1.5.1.dist-info → mx_bluesky-1.5.3.dist-info}/entry_points.txt +0 -2
  53. /mx_bluesky/common/experiment_plans/{read_hardware.py → inner_plans/read_hardware.py} +0 -0
  54. /mx_bluesky/common/experiment_plans/{write_sample_status.py → inner_plans/write_sample_status.py} +0 -0
  55. {mx_bluesky-1.5.1.dist-info → mx_bluesky-1.5.3.dist-info}/WHEEL +0 -0
  56. {mx_bluesky-1.5.1.dist-info → mx_bluesky-1.5.3.dist-info}/licenses/LICENSE +0 -0
  57. {mx_bluesky-1.5.1.dist-info → mx_bluesky-1.5.3.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,4 @@
1
1
  import bluesky.plan_stubs as bps
2
- from bluesky.utils import MsgGenerator
3
2
  from dodal.devices.zebra.zebra import (
4
3
  ArmDemand,
5
4
  EncEnum,
@@ -12,9 +11,11 @@ from dodal.devices.zebra.zebra_controlled_shutter import (
12
11
  ZebraShutterControl,
13
12
  )
14
13
 
14
+ from mx_bluesky.common.parameters.constants import ZEBRA_STATUS_TIMEOUT
15
15
  from mx_bluesky.common.utils.log import LOGGER
16
-
17
- ZEBRA_STATUS_TIMEOUT = 30
16
+ from mx_bluesky.phase1_zebra.device_setup_plans.setup_zebra import (
17
+ configure_zebra_and_shutter_for_auto_shutter,
18
+ )
18
19
 
19
20
 
20
21
  def arm_zebra(zebra: Zebra):
@@ -35,47 +36,6 @@ def tidy_up_zebra_after_rotation_scan(
35
36
  yield from bps.wait(group, timeout=ZEBRA_STATUS_TIMEOUT)
36
37
 
37
38
 
38
- def set_shutter_auto_input(zebra: Zebra, input: int, group="set_shutter_trigger"):
39
- """Set the signal that controls the shutter. We use the second input to the
40
- Zebra's AND2 gate for this input. ZebraShutter control mode must be in auto for this input to take control
41
-
42
- For more details see the ZebraShutter device."""
43
- auto_gate = zebra.mapping.AND_GATE_FOR_AUTO_SHUTTER
44
- auto_shutter_control = zebra.logic_gates.and_gates[auto_gate]
45
- yield from bps.abs_set(auto_shutter_control.sources[2], input, group)
46
-
47
-
48
- def configure_zebra_and_shutter_for_auto_shutter(
49
- zebra: Zebra, zebra_shutter: ZebraShutter, input: int, group="use_automatic_shutter"
50
- ):
51
- """Set the shutter to auto mode, and configure the zebra to trigger the shutter on
52
- an input source. For the input, use one of the source constants in zebra.py
53
-
54
- When the shutter is in auto/manual, logic in EPICS sets the Zebra's
55
- SOFT_IN1 to low/high respectively. The Zebra's AND2 gate should be used to control the shutter while in auto mode.
56
- To do this, we need (AND2 = SOFT_IN1 AND input), where input is the zebra signal we want to control the shutter when in auto mode.
57
- """
58
- # See https://github.com/DiamondLightSource/dodal/issues/813 for better typing here.
59
-
60
- # Set shutter to auto mode
61
- yield from bps.abs_set(
62
- zebra_shutter.control_mode, ZebraShutterControl.AUTO, group=group
63
- )
64
-
65
- auto_gate = zebra.mapping.AND_GATE_FOR_AUTO_SHUTTER
66
-
67
- # Set first input of AND2 gate to SOFT_IN1, which is high when shutter is in auto mode
68
- # Note the Zebra should ALWAYS be setup this way. See https://github.com/DiamondLightSource/mx-bluesky/issues/551
69
- yield from bps.abs_set(
70
- zebra.logic_gates.and_gates[auto_gate].sources[1],
71
- zebra.mapping.sources.SOFT_IN1,
72
- group=group,
73
- )
74
-
75
- # Set the second input of AND2 gate to the requested zebra input source
76
- yield from set_shutter_auto_input(zebra, input, group=group)
77
-
78
-
79
39
  def setup_zebra_for_rotation(
80
40
  zebra: Zebra,
81
41
  zebra_shutter: ZebraShutter,
@@ -155,55 +115,6 @@ def setup_zebra_for_rotation(
155
115
  yield from bps.wait(group, timeout=ZEBRA_STATUS_TIMEOUT)
156
116
 
157
117
 
158
- def setup_zebra_for_gridscan(
159
- zebra: Zebra,
160
- zebra_shutter: ZebraShutter,
161
- group="setup_zebra_for_gridscan",
162
- wait=True,
163
- ):
164
- # Set shutter to automatic and to trigger via motion controller GPIO signal (IN4_TTL)
165
- yield from configure_zebra_and_shutter_for_auto_shutter(
166
- zebra, zebra_shutter, zebra.mapping.sources.IN4_TTL, group=group
167
- )
168
-
169
- yield from bps.abs_set(
170
- zebra.output.out_pvs[zebra.mapping.outputs.TTL_DETECTOR],
171
- zebra.mapping.sources.IN3_TTL,
172
- group=group,
173
- )
174
- yield from bps.abs_set(
175
- zebra.output.out_pvs[zebra.mapping.outputs.TTL_XSPRESS3],
176
- zebra.mapping.sources.DISCONNECT,
177
- group=group,
178
- )
179
- yield from bps.abs_set(
180
- zebra.output.pulse_1.input, zebra.mapping.sources.DISCONNECT, group=group
181
- )
182
-
183
- if wait:
184
- yield from bps.wait(group, timeout=ZEBRA_STATUS_TIMEOUT)
185
-
186
-
187
- def tidy_up_zebra_after_gridscan(
188
- zebra: Zebra,
189
- zebra_shutter: ZebraShutter,
190
- group="tidy_up_zebra_after_gridscan",
191
- wait=True,
192
- ) -> MsgGenerator:
193
- yield from bps.abs_set(
194
- zebra.output.out_pvs[zebra.mapping.outputs.TTL_DETECTOR],
195
- zebra.mapping.sources.PC_PULSE,
196
- group=group,
197
- )
198
- yield from bps.abs_set(
199
- zebra_shutter.control_mode, ZebraShutterControl.MANUAL, group=group
200
- )
201
- yield from set_shutter_auto_input(zebra, zebra.mapping.sources.PC_GATE, group=group)
202
-
203
- if wait:
204
- yield from bps.wait(group, timeout=ZEBRA_STATUS_TIMEOUT)
205
-
206
-
207
118
  def setup_zebra_for_panda_flyscan(
208
119
  zebra: Zebra,
209
120
  zebra_shutter: ZebraShutter,
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Callable
3
4
  from functools import partial
4
5
  from pathlib import Path
5
6
 
@@ -19,14 +20,19 @@ from mx_bluesky.hyperion.device_setup_plans.setup_panda import (
19
20
  setup_panda_for_flyscan,
20
21
  )
21
22
  from mx_bluesky.hyperion.device_setup_plans.setup_zebra import (
22
- setup_zebra_for_gridscan,
23
23
  setup_zebra_for_panda_flyscan,
24
- tidy_up_zebra_after_gridscan,
24
+ )
25
+ from mx_bluesky.hyperion.external_interaction.config_server import (
26
+ get_hyperion_config_client,
25
27
  )
26
28
  from mx_bluesky.hyperion.parameters.device_composites import (
27
29
  HyperionFlyScanXRayCentreComposite,
28
30
  )
29
31
  from mx_bluesky.hyperion.parameters.gridscan import HyperionSpecifiedThreeDGridScan
32
+ from mx_bluesky.phase1_zebra.device_setup_plans.setup_zebra import (
33
+ setup_zebra_for_gridscan,
34
+ tidy_up_zebra_after_gridscan,
35
+ )
30
36
 
31
37
 
32
38
  class SmargonSpeedException(Exception):
@@ -59,9 +65,11 @@ def construct_hyperion_specific_features(
59
65
  xrc_composite.eiger.bit_depth,
60
66
  ]
61
67
 
62
- if xrc_parameters.features.use_panda_for_gridscan:
68
+ setup_trigger_plan: Callable[..., MsgGenerator]
69
+
70
+ if get_hyperion_config_client().get_feature_flags().USE_PANDA_FOR_GRIDSCAN:
63
71
  setup_trigger_plan = _panda_triggering_setup
64
- tidy_plan = _panda_tidy
72
+ tidy_plan = partial(_panda_tidy, xrc_composite)
65
73
  set_flyscan_params_plan = partial(
66
74
  set_fast_grid_scan_params,
67
75
  xrc_composite.panda_fast_grid_scan,
@@ -70,8 +78,14 @@ def construct_hyperion_specific_features(
70
78
  fgs_motors = xrc_composite.panda_fast_grid_scan
71
79
 
72
80
  else:
73
- setup_trigger_plan = _zebra_triggering_setup
74
- tidy_plan = partial(_generic_tidy, group="flyscan_zebra_tidy", wait=True)
81
+ setup_trigger_plan = setup_zebra_for_gridscan
82
+ tidy_plan = partial(
83
+ tidy_up_zebra_after_gridscan,
84
+ xrc_composite.zebra,
85
+ xrc_composite.sample_shutter,
86
+ group="flyscan_zebra_tidy",
87
+ wait=True,
88
+ )
75
89
  set_flyscan_params_plan = partial(
76
90
  set_fast_grid_scan_params,
77
91
  xrc_composite.zebra_fast_grid_scan,
@@ -89,46 +103,17 @@ def construct_hyperion_specific_features(
89
103
  )
90
104
 
91
105
 
92
- def _generic_tidy(
93
- xrc_composite: HyperionFlyScanXRayCentreComposite, group, wait=True
94
- ) -> MsgGenerator:
95
- LOGGER.info("Tidying up Zebra")
96
- yield from tidy_up_zebra_after_gridscan(
97
- xrc_composite.zebra, xrc_composite.sample_shutter, group=group, wait=wait
98
- )
99
- LOGGER.info("Tidying up Zocalo")
100
- # make sure we don't consume any other results
101
- yield from bps.unstage(xrc_composite.zocalo, group=group, wait=wait)
102
-
103
- # Turn off dev/shm streaming to avoid filling disk, see https://github.com/DiamondLightSource/hyperion/issues/1395
104
- LOGGER.info("Turning off Eiger dev/shm streaming")
105
- # Fix types in ophyd-async (https://github.com/DiamondLightSource/mx-bluesky/issues/855)
106
- yield from bps.abs_set(
107
- xrc_composite.eiger.odin.fan.dev_shm_enable, # type: ignore
108
- 0,
109
- group=group,
110
- wait=wait,
111
- )
112
-
113
-
114
106
  def _panda_tidy(xrc_composite: HyperionFlyScanXRayCentreComposite):
115
107
  group = "panda_flyscan_tidy"
116
108
  LOGGER.info("Disabling panda blocks")
117
109
  yield from disarm_panda_for_gridscan(xrc_composite.panda, group)
118
- yield from _generic_tidy(xrc_composite, group, False)
110
+ yield from tidy_up_zebra_after_gridscan(
111
+ xrc_composite.zebra, xrc_composite.sample_shutter, group=group, wait=False
112
+ )
119
113
  yield from bps.wait(group, timeout=10)
120
114
  yield from bps.unstage(xrc_composite.panda)
121
115
 
122
116
 
123
- def _zebra_triggering_setup(
124
- xrc_composite: HyperionFlyScanXRayCentreComposite,
125
- parameters: HyperionSpecifiedThreeDGridScan,
126
- ) -> MsgGenerator:
127
- yield from setup_zebra_for_gridscan(
128
- xrc_composite.zebra, xrc_composite.sample_shutter, wait=True
129
- )
130
-
131
-
132
117
  def _panda_triggering_setup(
133
118
  xrc_composite: HyperionFlyScanXRayCentreComposite,
134
119
  parameters: HyperionSpecifiedThreeDGridScan,
@@ -24,7 +24,10 @@ from mx_bluesky.hyperion.experiment_plans.rotation_scan_plan import (
24
24
  RotationScanComposite,
25
25
  rotation_scan_internal,
26
26
  )
27
- from mx_bluesky.hyperion.parameters.constants import CONST
27
+ from mx_bluesky.hyperion.external_interaction.config_server import (
28
+ get_hyperion_config_client,
29
+ )
30
+ from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants
28
31
  from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
29
32
  from mx_bluesky.hyperion.parameters.rotation import RotationScanPerSweep
30
33
 
@@ -51,7 +54,8 @@ def load_centre_collect_full(
51
54
  * If X-ray centring finds a diffracting centre then move to that centre and
52
55
  * do a collection with the specified parameters.
53
56
  """
54
- parameters.features.update_self_from_server()
57
+
58
+ get_hyperion_config_client().refresh_cache()
55
59
 
56
60
  if not oav_params:
57
61
  oav_params = OAVParameters(context="xrayCentring")
@@ -60,7 +64,11 @@ def load_centre_collect_full(
60
64
  @set_run_key_decorator(CONST.PLAN.LOAD_CENTRE_COLLECT)
61
65
  @run_decorator(
62
66
  md={
63
- "metadata": {"sample_id": parameters.sample_id},
67
+ "metadata": {
68
+ "sample_id": parameters.sample_id,
69
+ "visit": parameters.visit,
70
+ "container": parameters.sample_puck,
71
+ },
64
72
  "activate_callbacks": ["BeamDrawingCallback", "SampleHandlingCallback"],
65
73
  "with_snapshot": parameters.multi_rotation_scan.model_dump_json(
66
74
  include=WithSnapshot.model_fields.keys() # type: ignore
@@ -117,7 +125,7 @@ def load_centre_collect_full(
117
125
 
118
126
  multi_rotation.rotation_scans.clear()
119
127
 
120
- is_alternating = parameters.features.alternate_rotation_direction
128
+ is_alternating = I03Constants.ALTERNATE_ROTATION_DIRECTION
121
129
 
122
130
  generator = rotation_scan_generator(is_alternating)
123
131
  next(generator)
@@ -155,7 +155,7 @@ def pin_tip_centre_plan(
155
155
  tip = yield from move_pin_into_view(pin_tip_detect, smargon)
156
156
  yield from offset_and_move(tip)
157
157
 
158
- yield from bps.mvr(smargon.omega, 90)
158
+ yield from bps.mvr(smargon.omega, -90)
159
159
 
160
160
  # need to wait for the OAV image to update
161
161
  # See #673 for improvements
@@ -37,16 +37,16 @@ from mx_bluesky.common.device_setup_plans.manipulate_sample import (
37
37
  from mx_bluesky.common.device_setup_plans.utils import (
38
38
  start_preparing_data_collection_then_do_plan,
39
39
  )
40
+ from mx_bluesky.common.experiment_plans.inner_plans.read_hardware import (
41
+ read_hardware_for_zocalo,
42
+ standard_read_hardware_during_collection,
43
+ standard_read_hardware_pre_collection,
44
+ )
40
45
  from mx_bluesky.common.experiment_plans.oav_snapshot_plan import (
41
46
  OavSnapshotComposite,
42
47
  oav_snapshot_plan,
43
48
  setup_beamline_for_OAV,
44
49
  )
45
- from mx_bluesky.common.experiment_plans.read_hardware import (
46
- read_hardware_for_zocalo,
47
- standard_read_hardware_during_collection,
48
- standard_read_hardware_pre_collection,
49
- )
50
50
  from mx_bluesky.common.parameters.components import WithSnapshot
51
51
  from mx_bluesky.common.preprocessors.preprocessors import (
52
52
  transmission_and_xbpm_feedback_for_collection_decorator,
@@ -58,7 +58,7 @@ from mx_bluesky.hyperion.device_setup_plans.setup_zebra import (
58
58
  setup_zebra_for_rotation,
59
59
  tidy_up_zebra_after_rotation_scan,
60
60
  )
61
- from mx_bluesky.hyperion.parameters.constants import CONST
61
+ from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants
62
62
  from mx_bluesky.hyperion.parameters.rotation import (
63
63
  RotationScan,
64
64
  SingleRotationScan,
@@ -133,7 +133,7 @@ def calculate_motion_profile(
133
133
  direction = params.rotation_direction
134
134
  start_scan_deg = params.omega_start_deg
135
135
 
136
- if params.features.omega_flip:
136
+ if I03Constants.OMEGA_FLIP:
137
137
  # If omega_flip is True then the motor omega axis is inverted with respect to the
138
138
  # hyperion coordinate system.
139
139
  start_scan_deg = -start_scan_deg
@@ -386,7 +386,6 @@ def rotation_scan_internal(
386
386
  parameters: RotationScan,
387
387
  oav_params: OAVParameters | None = None,
388
388
  ) -> MsgGenerator:
389
- parameters.features.update_self_from_server()
390
389
  if not oav_params:
391
390
  oav_params = OAVParameters(context="xrayCentring")
392
391
  eiger: EigerDetector = composite.eiger
@@ -3,6 +3,7 @@ import json
3
3
  import re
4
4
  import traceback
5
5
  from collections.abc import Sequence
6
+ from enum import StrEnum
6
7
  from os import path
7
8
  from typing import Any, TypeVar
8
9
 
@@ -14,6 +15,7 @@ from pydantic_extra_types.semantic_version import SemanticVersion
14
15
 
15
16
  from mx_bluesky.common.parameters.components import (
16
17
  PARAMETER_VERSION,
18
+ MxBlueskyParameters,
17
19
  WithVisit,
18
20
  )
19
21
  from mx_bluesky.common.parameters.constants import (
@@ -21,6 +23,7 @@ from mx_bluesky.common.parameters.constants import (
21
23
  )
22
24
  from mx_bluesky.common.utils.log import LOGGER
23
25
  from mx_bluesky.common.utils.utils import convert_angstrom_to_eV
26
+ from mx_bluesky.hyperion.parameters.components import Wait
24
27
  from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
25
28
 
26
29
  T = TypeVar("T", bound=WithVisit)
@@ -31,8 +34,13 @@ MULTIPIN_REGEX = rf"^{MULTIPIN_PREFIX}_(\d+)x(\d+(?:\.\d+)?)\+(\d+(?:\.\d+)?)$"
31
34
  MX_GENERAL_ROOT_REGEX = r"^/dls/(?P<beamline>[^/]+)/data/[^/]*/(?P<visit>[^/]+)(?:/|$)"
32
35
 
33
36
 
37
+ class _InstructionType(StrEnum):
38
+ WAIT = "wait"
39
+ COLLECT = "collect"
40
+
41
+
34
42
  @dataclasses.dataclass
35
- class PinType:
43
+ class _PinType:
36
44
  expected_number_of_crystals: int
37
45
  single_well_width_um: float
38
46
  tip_to_first_well_um: float = 0
@@ -54,7 +62,7 @@ class PinType:
54
62
  )
55
63
 
56
64
 
57
- class SinglePin(PinType):
65
+ class _SinglePin(_PinType):
58
66
  def __init__(self):
59
67
  super().__init__(1, GridscanParamConstants.WIDTH_UM)
60
68
 
@@ -63,23 +71,115 @@ class SinglePin(PinType):
63
71
  return self.single_well_width_um
64
72
 
65
73
 
74
+ def create_parameters_from_agamemnon() -> Sequence[MxBlueskyParameters]:
75
+ """Fetch the next instruction from agamemnon and convert it into one or more
76
+ mx-bluesky instructions.
77
+ Returns:
78
+ The generated sequence of mx-bluesky parameters, or empty list if
79
+ no instructions."""
80
+ beamline_name = get_beamline_name("i03")
81
+ agamemnon_instruction = _get_next_instruction(beamline_name)
82
+ if agamemnon_instruction:
83
+ match _instruction_and_data(agamemnon_instruction):
84
+ case (_InstructionType.COLLECT, data):
85
+ return _populate_parameters_from_agamemnon(data)
86
+ case (_InstructionType.WAIT, data):
87
+ return [
88
+ Wait.model_validate(
89
+ {
90
+ "duration_s": data,
91
+ "parameter_model_version": _get_param_version(),
92
+ }
93
+ )
94
+ ]
95
+
96
+ return []
97
+
98
+
99
+ def compare_params(load_centre_collect_params: LoadCentreCollect):
100
+ """Compare the supplied parameters (as supplied from GDA) with those directly
101
+ created from agamemnon. Any differences are logged.
102
+ Args:
103
+ load_centre_collect_params: The parameters from GDA to compare."""
104
+ try:
105
+ lcc_requests = create_parameters_from_agamemnon()
106
+ # Log differences against GDA populated parameters
107
+ if not lcc_requests:
108
+ LOGGER.info("Agamemnon returned no instructions")
109
+ else:
110
+ differences = DeepDiff(
111
+ lcc_requests[0], load_centre_collect_params, math_epsilon=1e-5
112
+ )
113
+ if differences:
114
+ LOGGER.info(
115
+ f"Different parameters found when directly reading from Hyperion: {differences}"
116
+ )
117
+ except (ValueError, KeyError):
118
+ LOGGER.warning(f"Failed to compare parameters: {traceback.format_exc()}")
119
+ except Exception:
120
+ LOGGER.warning(
121
+ f"Unexpected error occurred. Failed to compare parameters: {traceback.format_exc()}"
122
+ )
123
+
124
+
125
+ def update_params_from_agamemnon(parameters: T) -> T:
126
+ """Update the supplied parameters with additional information from agamemnon.
127
+ This is currently necessary for multipin processing and called when Hyperion is invoked
128
+ from GDA.
129
+
130
+ Args:
131
+ parameters: The LoadCentreCollectParameters that will be updated with additional info,
132
+ such as multipin dimensions, number of crystals.
133
+ """
134
+ try:
135
+ beamline_name = get_beamline_name("i03")
136
+ agamemnon_params = _get_next_instruction(beamline_name)
137
+ instruction, collect_params = _instruction_and_data(agamemnon_params)
138
+ assert instruction == _InstructionType.COLLECT, (
139
+ "Unable to augment GDA parameters from agamemnon, agamemnon reports 'wait'"
140
+ )
141
+ pin_type = _get_pin_type_from_agamemnon_collect_parameters(collect_params)
142
+ if isinstance(parameters, LoadCentreCollect):
143
+ parameters.robot_load_then_centre.tip_offset_um = pin_type.full_width / 2
144
+ parameters.robot_load_then_centre.grid_width_um = pin_type.full_width
145
+ parameters.select_centres.n = pin_type.expected_number_of_crystals
146
+ if pin_type != _SinglePin():
147
+ # Rotation snapshots will be generated from the gridscan snapshots,
148
+ # no need to specify snapshot omega.
149
+ parameters.multi_rotation_scan.snapshot_omegas_deg = []
150
+ parameters.multi_rotation_scan.use_grid_snapshots = True
151
+ except (ValueError, ValidationError) as e:
152
+ LOGGER.warning(f"Failed to update parameters: {e}")
153
+ except Exception as e:
154
+ LOGGER.warning(f"Unexpected error occurred. Failed to update parameters: {e}")
155
+
156
+ return parameters
157
+
158
+
159
+ def _instruction_and_data(agamemnon_instruction: dict) -> tuple[str, Any]:
160
+ instruction, data = next(iter(agamemnon_instruction.items()))
161
+ if instruction not in _InstructionType.__members__.values():
162
+ raise KeyError(
163
+ f"Unexpected instruction from agamemnon: {agamemnon_instruction}"
164
+ )
165
+ return instruction, data
166
+
167
+
66
168
  def _get_parameters_from_url(url: str) -> dict:
67
169
  response = requests.get(url, headers={"Accept": "application/json"})
68
170
  response.raise_for_status()
69
- response_json = json.loads(response.content)
70
- try:
71
- return response_json["collect"]
72
- except KeyError as e:
73
- raise KeyError(f"Unexpected json from agamemnon: {response_json}") from e
171
+ return json.loads(response.content)
74
172
 
75
173
 
76
- def get_pin_type_from_agamemnon_parameters(parameters: dict) -> PinType:
77
- loop_type_name: str | None = parameters["sample"]["loopType"]
174
+ def _get_pin_type_from_agamemnon_collect_parameters(
175
+ collect_parameters: dict,
176
+ ) -> _PinType:
177
+ loop_type_name: str | None = collect_parameters["sample"]["loopType"]
78
178
  if loop_type_name:
79
179
  regex_search = re.search(MULTIPIN_REGEX, loop_type_name)
80
180
  if regex_search:
81
181
  wells, well_size, tip_to_first_well = regex_search.groups()
82
- return PinType(int(wells), float(well_size), float(tip_to_first_well))
182
+ return _PinType(int(wells), float(well_size), float(tip_to_first_well))
83
183
  else:
84
184
  loop_type_message = (
85
185
  f"Agamemnon loop type of {loop_type_name} not recognised"
@@ -87,14 +187,14 @@ def get_pin_type_from_agamemnon_parameters(parameters: dict) -> PinType:
87
187
  if loop_type_name.startswith(MULTIPIN_PREFIX):
88
188
  raise ValueError(f"{loop_type_message}. {MULTIPIN_FORMAT_DESC}")
89
189
  LOGGER.warning(f"{loop_type_message}, assuming single pin")
90
- return SinglePin()
190
+ return _SinglePin()
91
191
 
92
192
 
93
- def get_next_instruction(beamline: str) -> dict:
193
+ def _get_next_instruction(beamline: str) -> dict:
94
194
  return _get_parameters_from_url(AGAMEMNON_URL + f"getnextcollect/{beamline}")
95
195
 
96
196
 
97
- def get_withvisit_parameters_from_agamemnon(parameters: dict) -> tuple:
197
+ def _get_withvisit_parameters_from_agamemnon(parameters: dict) -> tuple:
98
198
  try:
99
199
  prefix = parameters["prefix"]
100
200
  collection = parameters["collection"]
@@ -113,7 +213,7 @@ def get_withvisit_parameters_from_agamemnon(parameters: dict) -> tuple:
113
213
  )
114
214
 
115
215
 
116
- def get_withenergy_parameters_from_agamemnon(parameters: dict) -> dict[str, Any]:
216
+ def _get_withenergy_parameters_from_agamemnon(parameters: dict) -> dict[str, Any]:
117
217
  try:
118
218
  first_collection: dict = parameters["collection"][0]
119
219
  wavelength = first_collection.get("wavelength")
@@ -124,21 +224,29 @@ def get_withenergy_parameters_from_agamemnon(parameters: dict) -> dict[str, Any]
124
224
  return {"demand_energy_ev": None}
125
225
 
126
226
 
127
- def get_param_version() -> SemanticVersion:
227
+ def _get_param_version() -> SemanticVersion:
128
228
  return SemanticVersion.validate_from_str(str(PARAMETER_VERSION))
129
229
 
130
230
 
131
- def populate_parameters_from_agamemnon(agamemnon_params) -> Sequence[LoadCentreCollect]:
132
- visit, detector_distance = get_withvisit_parameters_from_agamemnon(agamemnon_params)
133
- with_energy_params = get_withenergy_parameters_from_agamemnon(agamemnon_params)
134
- pin_type = get_pin_type_from_agamemnon_parameters(agamemnon_params)
231
+ def _populate_parameters_from_agamemnon(
232
+ agamemnon_params,
233
+ ) -> Sequence[LoadCentreCollect]:
234
+ if not agamemnon_params:
235
+ # Empty dict means no instructions
236
+ return []
237
+
238
+ visit, detector_distance = _get_withvisit_parameters_from_agamemnon(
239
+ agamemnon_params
240
+ )
241
+ with_energy_params = _get_withenergy_parameters_from_agamemnon(agamemnon_params)
242
+ pin_type = _get_pin_type_from_agamemnon_collect_parameters(agamemnon_params)
135
243
  collections = agamemnon_params["collection"]
136
244
  visit_directory, file_name = path.split(agamemnon_params["prefix"])
137
245
 
138
246
  return [
139
247
  LoadCentreCollect.model_validate(
140
248
  {
141
- "parameter_model_version": get_param_version(),
249
+ "parameter_model_version": _get_param_version(),
142
250
  "visit": visit,
143
251
  "detector_distance_mm": detector_distance,
144
252
  "sample_id": agamemnon_params["sample"]["id"],
@@ -148,7 +256,6 @@ def populate_parameters_from_agamemnon(agamemnon_params) -> Sequence[LoadCentreC
148
256
  "name": "TopNByMaxCount",
149
257
  "n": pin_type.expected_number_of_crystals,
150
258
  },
151
- "features": {"use_gpu_results": True},
152
259
  "robot_load_then_centre": {
153
260
  "storage_directory": str(visit_directory) + "/xraycentring",
154
261
  "file_name": file_name,
@@ -186,55 +293,3 @@ def populate_parameters_from_agamemnon(agamemnon_params) -> Sequence[LoadCentreC
186
293
  )
187
294
  for collection in collections
188
295
  ]
189
-
190
-
191
- def create_parameters_from_agamemnon() -> Sequence[LoadCentreCollect]:
192
- beamline_name = get_beamline_name("i03")
193
- agamemnon_params = get_next_instruction(beamline_name)
194
- return (
195
- populate_parameters_from_agamemnon(agamemnon_params) if agamemnon_params else []
196
- )
197
-
198
-
199
- def compare_params(load_centre_collect_params: LoadCentreCollect):
200
- try:
201
- lcc_requests = create_parameters_from_agamemnon()
202
- # Log differences against GDA populated parameters
203
- if not lcc_requests:
204
- LOGGER.info("Agamemnon returned no instructions")
205
- else:
206
- differences = DeepDiff(
207
- lcc_requests[0], load_centre_collect_params, math_epsilon=1e-5
208
- )
209
- if differences:
210
- LOGGER.info(
211
- f"Different parameters found when directly reading from Hyperion: {differences}"
212
- )
213
- except (ValueError, KeyError):
214
- LOGGER.warning(f"Failed to compare parameters: {traceback.format_exc()}")
215
- except Exception:
216
- LOGGER.warning(
217
- f"Unexpected error occurred. Failed to compare parameters: {traceback.format_exc()}"
218
- )
219
-
220
-
221
- def update_params_from_agamemnon(parameters: T) -> T:
222
- try:
223
- beamline_name = get_beamline_name("i03")
224
- agamemnon_params = get_next_instruction(beamline_name)
225
- pin_type = get_pin_type_from_agamemnon_parameters(agamemnon_params)
226
- if isinstance(parameters, LoadCentreCollect):
227
- parameters.robot_load_then_centre.tip_offset_um = pin_type.full_width / 2
228
- parameters.robot_load_then_centre.grid_width_um = pin_type.full_width
229
- parameters.select_centres.n = pin_type.expected_number_of_crystals
230
- if pin_type != SinglePin():
231
- # Rotation snapshots will be generated from the gridscan snapshots,
232
- # no need to specify snapshot omega.
233
- parameters.multi_rotation_scan.snapshot_omegas_deg = []
234
- parameters.multi_rotation_scan.use_grid_snapshots = True
235
- except (ValueError, ValidationError) as e:
236
- LOGGER.warning(f"Failed to update parameters: {e}")
237
- except Exception as e:
238
- LOGGER.warning(f"Unexpected error occurred. Failed to update parameters: {e}")
239
-
240
- return parameters
@@ -0,0 +1,12 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class Subjects(StrEnum):
5
+ UDC_STARTED = "UDC Started"
6
+ UDC_BATON_PASSED = "UDC Baton was passed"
7
+ UDC_RESUMED_OPERATION = "UDC Resumed operation"
8
+ UDC_SUSPENDED_OPERATION = "UDC Suspended operation"
9
+ NEW_CONTAINER = "Hyperion is collecting from a new container"
10
+ NEW_VISIT = "Hyperion has changed visit"
11
+ SAMPLE_ERROR = "Hyperion has encountered a sample error"
12
+ BEAMLINE_ERROR = "Hyperion has encountered a beamline error"
@@ -8,6 +8,10 @@ from bluesky.callbacks.zmq import Proxy, RemoteDispatcher
8
8
  from dodal.log import LOGGER as dodal_logger
9
9
  from dodal.log import set_up_all_logging_handlers
10
10
 
11
+ from mx_bluesky.common.external_interaction.alerting import set_alerting_service
12
+ from mx_bluesky.common.external_interaction.alerting.log_based_service import (
13
+ LoggingAlertService,
14
+ )
11
15
  from mx_bluesky.common.external_interaction.callbacks.common.log_uid_tag_callback import (
12
16
  LogUidTaggingCallback,
13
17
  )
@@ -156,6 +160,7 @@ class HyperionCallbackRunner:
156
160
  def __init__(self, dev_mode) -> None:
157
161
  setup_logging(dev_mode)
158
162
  log_info("Hyperion callback process started.")
163
+ set_alerting_service(LoggingAlertService(CONST.GRAYLOG_STREAM_ID))
159
164
 
160
165
  self.callbacks = setup_callbacks()
161
166
  self.proxy, self.dispatcher, start_proxy, start_dispatcher = setup_threads()
@@ -15,7 +15,7 @@ from mx_bluesky.common.external_interaction.nexus.nexus_utils import (
15
15
  )
16
16
  from mx_bluesky.common.external_interaction.nexus.write_nexus import NexusWriter
17
17
  from mx_bluesky.common.utils.log import NEXUS_LOGGER
18
- from mx_bluesky.hyperion.parameters.constants import CONST
18
+ from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants
19
19
  from mx_bluesky.hyperion.parameters.rotation import SingleRotationScan
20
20
 
21
21
  if TYPE_CHECKING:
@@ -103,6 +103,6 @@ class RotationNexusFileCallback(PlanReactiveCallback):
103
103
  full_num_of_images=self.full_num_of_images,
104
104
  meta_data_run_number=self.meta_data_run_number,
105
105
  axis_direction=AxisDirection.NEGATIVE
106
- if parameters.features.omega_flip
106
+ if I03Constants.OMEGA_FLIP
107
107
  else AxisDirection.POSITIVE,
108
108
  )