mx-bluesky 1.5.2__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 (40) hide show
  1. mx_bluesky/_version.py +16 -3
  2. mx_bluesky/beamlines/i04/__init__.py +7 -3
  3. mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +3 -0
  4. mx_bluesky/beamlines/i24/serial/blueapi_config.yaml +2 -2
  5. mx_bluesky/common/experiment_plans/oav_grid_detection_plan.py +12 -2
  6. mx_bluesky/common/external_interaction/alerting/__init__.py +13 -0
  7. mx_bluesky/common/external_interaction/alerting/_service.py +82 -0
  8. mx_bluesky/common/external_interaction/alerting/log_based_service.py +57 -0
  9. mx_bluesky/common/external_interaction/callbacks/sample_handling/sample_handling_callback.py +28 -4
  10. mx_bluesky/common/external_interaction/config_server.py +151 -54
  11. mx_bluesky/common/parameters/constants.py +26 -8
  12. mx_bluesky/common/parameters/gridscan.py +1 -1
  13. mx_bluesky/hyperion/__main__.py +50 -178
  14. mx_bluesky/hyperion/baton_handler.py +125 -69
  15. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +29 -24
  16. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +4 -1
  17. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +12 -4
  18. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +1 -1
  19. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +2 -3
  20. mx_bluesky/hyperion/external_interaction/agamemnon.py +128 -73
  21. mx_bluesky/hyperion/external_interaction/alerting/__init__.py +0 -0
  22. mx_bluesky/hyperion/external_interaction/alerting/constants.py +12 -0
  23. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +5 -0
  24. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +2 -2
  25. mx_bluesky/hyperion/external_interaction/config_server.py +12 -31
  26. mx_bluesky/hyperion/parameters/cli.py +15 -3
  27. mx_bluesky/hyperion/parameters/components.py +7 -5
  28. mx_bluesky/hyperion/parameters/constants.py +20 -4
  29. mx_bluesky/hyperion/parameters/gridscan.py +22 -14
  30. mx_bluesky/hyperion/parameters/load_centre_collect.py +1 -14
  31. mx_bluesky/hyperion/parameters/robot_load.py +1 -4
  32. mx_bluesky/hyperion/parameters/rotation.py +1 -2
  33. mx_bluesky/hyperion/plan_runner.py +78 -0
  34. mx_bluesky/hyperion/runner.py +189 -0
  35. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/METADATA +4 -3
  36. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/RECORD +40 -33
  37. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/entry_points.txt +0 -2
  38. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/WHEEL +0 -0
  39. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/licenses/LICENSE +0 -0
  40. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/top_level.txt +0 -0
@@ -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
@@ -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
  )
@@ -1,35 +1,16 @@
1
1
  from functools import cache
2
2
 
3
- from daq_config_server.client import ConfigServer
3
+ from mx_bluesky.common.external_interaction.config_server import MXConfigClient
4
+ from mx_bluesky.hyperion.parameters.constants import (
5
+ HyperionFeatureSetting,
6
+ HyperionFeatureSettingources,
7
+ )
4
8
 
5
- from mx_bluesky.common.external_interaction.config_server import FeatureFlags
6
- from mx_bluesky.common.utils.log import LOGGER
7
- from mx_bluesky.hyperion.parameters.constants import CONST
8
9
 
9
-
10
- class HyperionFeatureFlags(FeatureFlags):
11
- """
12
- Feature flags specific to Hyperion.
13
-
14
- Attributes:
15
- use_panda_for_gridscan: If True then the PandA is used for gridscans, otherwise the zebra is used
16
- use_gpu_results: If True then GPU result processing is enabled
17
- and the GPU result is taken.
18
- set_stub_offsets: If True then set the stub offsets after moving to the crystal (ignored for
19
- multi-centre)
20
- omega_flip: If True then invert the smargon omega motor rotation commands with respect to
21
- the hyperion request. See "Hyperion Coordinate Systems" in the documentation.
22
- alternate_rotation_direction: If True then the for multi-sample pins the rotation direction of
23
- successive rotation scans is alternated between positive and negative.
24
- """
25
-
26
- @staticmethod
27
- @cache
28
- def get_config_server() -> ConfigServer:
29
- return ConfigServer(CONST.CONFIG_SERVER_URL, LOGGER)
30
-
31
- use_panda_for_gridscan: bool = CONST.I03.USE_PANDA_FOR_GRIDSCAN
32
- use_gpu_results: bool = CONST.I03.USE_GPU_RESULTS
33
- set_stub_offsets: bool = CONST.I03.SET_STUB_OFFSETS
34
- omega_flip: bool = CONST.I03.OMEGA_FLIP
35
- alternate_rotation_direction: bool = CONST.I03.ALTERNATE_ROTATION_DIRECTION
10
+ @cache
11
+ def get_hyperion_config_client() -> MXConfigClient[HyperionFeatureSetting]:
12
+ return MXConfigClient(
13
+ feature_sources=HyperionFeatureSettingources,
14
+ feature_dc=HyperionFeatureSetting,
15
+ url="https://daq-config.diamond.ac.uk",
16
+ )
@@ -1,12 +1,19 @@
1
1
  import argparse
2
+ from enum import StrEnum
2
3
 
3
4
  from pydantic.dataclasses import dataclass
4
5
 
5
6
  from mx_bluesky._version import version
6
7
 
7
8
 
9
+ class HyperionMode(StrEnum):
10
+ GDA = "gda"
11
+ UDC = "udc"
12
+
13
+
8
14
  @dataclass
9
15
  class HyperionArgs:
16
+ mode: HyperionMode
10
17
  dev_mode: bool = False
11
18
 
12
19
 
@@ -39,7 +46,12 @@ def parse_cli_args() -> HyperionArgs:
39
46
  action="version",
40
47
  version=version,
41
48
  )
42
- args = parser.parse_args()
43
- return HyperionArgs(
44
- dev_mode=args.dev or False,
49
+ parser.add_argument(
50
+ "--mode",
51
+ help="Launch in the specified mode (default is 'gda')",
52
+ default=HyperionMode.GDA,
53
+ type=HyperionMode,
54
+ choices=HyperionMode.__members__.values(),
45
55
  )
56
+ args = parser.parse_args()
57
+ return HyperionArgs(dev_mode=args.dev or False, mode=args.mode)
@@ -1,8 +1,10 @@
1
- from pydantic import Field
1
+ from mx_bluesky.common.parameters.components import MxBlueskyParameters
2
2
 
3
- from mx_bluesky.common.parameters.components import WithPandaGridScan
4
- from mx_bluesky.hyperion.external_interaction.config_server import HyperionFeatureFlags
5
3
 
4
+ class Wait(MxBlueskyParameters):
5
+ """Represents an instruction from Agamemnon for Hyperion to wait for a specified time
6
+ Attributes:
7
+ duration_s: duration to wait in seconds
8
+ """
6
9
 
7
- class WithHyperionUDCFeatures(WithPandaGridScan):
8
- features: HyperionFeatureFlags = Field(default=HyperionFeatureFlags())
10
+ duration_s: float
@@ -8,6 +8,8 @@ from mx_bluesky.common.parameters.constants import (
8
8
  DocDescriptorNames,
9
9
  EnvironmentConstants,
10
10
  ExperimentParamConstants,
11
+ FeatureSetting,
12
+ FeatureSettingources,
11
13
  HardwareConstants,
12
14
  OavConstants,
13
15
  PlanGroupCheckpointConstants,
@@ -24,13 +26,26 @@ class I03Constants:
24
26
  INSERTION_PREFIX = "SR03S" if TEST_MODE else "SR03I"
25
27
  OAV_CENTRING_FILE = OavConstants.OAV_CONFIG_JSON
26
28
  SHUTTER_TIME_S = 0.06
27
- USE_PANDA_FOR_GRIDSCAN = False
28
- SET_STUB_OFFSETS = False
29
+ USE_GPU_RESULTS = True
29
30
  OMEGA_FLIP = True
30
31
  ALTERNATE_ROTATION_DIRECTION = True
31
32
 
32
- # Turns on GPU processing for zocalo and uses the results that come back
33
- USE_GPU_RESULTS = True
33
+
34
+ # These currently exist in GDA domain.properties
35
+ class HyperionFeatureSettingources(FeatureSettingources):
36
+ USE_GPU_RESULTS = "gda.mx.hyperion.xrc.use_gpu_results"
37
+ USE_PANDA_FOR_GRIDSCAN = "gda.mx.hyperion.use_panda_for_gridscans"
38
+ SET_STUB_OFFSETS = "gda.mx.hyperion.do_stub_offsets"
39
+ PANDA_RUNUP_DISTANCE_MM = "gda.mx.hyperion.panda_runup_distance_mm"
40
+
41
+
42
+ # Use these defaults if we can't read from the config server
43
+ @dataclass
44
+ class HyperionFeatureSetting(FeatureSetting):
45
+ USE_GPU_RESULTS: bool = True
46
+ USE_PANDA_FOR_GRIDSCAN: bool = False
47
+ SET_STUB_OFFSETS: bool = False
48
+ PANDA_RUNUP_DISTANCE_MM: float = 0.16
34
49
 
35
50
 
36
51
  @dataclass(frozen=True)
@@ -49,6 +64,7 @@ class HyperionConstants:
49
64
  else "https://daq-config.diamond.ac.uk/api"
50
65
  )
51
66
  GRAYLOG_PORT = 12232 # Hyperion stream
67
+ GRAYLOG_STREAM_ID = "66264f5519ccca6d1c9e4e03"
52
68
  PARAMETER_SCHEMA_DIRECTORY = "src/hyperion/parameters/schemas/"
53
69
  LOG_FILE_NAME = "hyperion.log"
54
70
  DEVICE_SETTINGS_CONSTANTS = DeviceSettingsConstants()
@@ -9,10 +9,12 @@ from mx_bluesky.common.parameters.gridscan import (
9
9
  GridCommon,
10
10
  SpecifiedThreeDGridScan,
11
11
  )
12
- from mx_bluesky.hyperion.parameters.components import WithHyperionUDCFeatures
12
+ from mx_bluesky.hyperion.external_interaction.config_server import (
13
+ get_hyperion_config_client,
14
+ )
13
15
 
14
16
 
15
- class GridCommonWithHyperionDetectorParams(GridCommon, WithHyperionUDCFeatures):
17
+ class GridCommonWithHyperionDetectorParams(GridCommon):
16
18
  """Used by models which require detector parameters but have no specifications of the grid"""
17
19
 
18
20
  # These detector params only exist so that we can properly select enable_dev_shm. Remove in
@@ -20,11 +22,13 @@ class GridCommonWithHyperionDetectorParams(GridCommon, WithHyperionUDCFeatures):
20
22
  @property
21
23
  def detector_params(self):
22
24
  params = super().detector_params
23
- params.enable_dev_shm = self.features.use_gpu_results
25
+ params.enable_dev_shm = (
26
+ get_hyperion_config_client().get_feature_flags().USE_GPU_RESULTS
27
+ )
24
28
  return params
25
29
 
26
30
 
27
- class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGridScan):
31
+ class HyperionSpecifiedThreeDGridScan(SpecifiedThreeDGridScan):
28
32
  """Hyperion's 3D grid scan deviates from the common class due to: optionally using a PandA, optionally using dev_shm for GPU analysis, and using a config server for features"""
29
33
 
30
34
  # These detector params only exist so that we can properly select enable_dev_shm. Remove in
@@ -33,7 +37,9 @@ class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGr
33
37
  @property
34
38
  def detector_params(self):
35
39
  params = super().detector_params
36
- params.enable_dev_shm = self.features.use_gpu_results
40
+ params.enable_dev_shm = (
41
+ get_hyperion_config_client().get_feature_flags().USE_GPU_RESULTS
42
+ )
37
43
  return params
38
44
 
39
45
  # Relative to common grid scan, stub offsets are defined by config server
@@ -51,7 +57,9 @@ class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGr
51
57
  z1_start_mm=self.z_start_um / 1000,
52
58
  y2_start_mm=self.y2_start_um / 1000,
53
59
  z2_start_mm=self.z2_start_um / 1000,
54
- set_stub_offsets=self.features.set_stub_offsets,
60
+ set_stub_offsets=get_hyperion_config_client()
61
+ .get_feature_flags()
62
+ .SET_STUB_OFFSETS,
55
63
  dwell_time_ms=self.exposure_time_s * 1000,
56
64
  transmission_fraction=self.transmission_frac,
57
65
  )
@@ -75,8 +83,12 @@ class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGr
75
83
  z1_start_mm=self.z_start_um / 1000,
76
84
  y2_start_mm=self.y2_start_um / 1000,
77
85
  z2_start_mm=self.z2_start_um / 1000,
78
- set_stub_offsets=self.features.set_stub_offsets,
79
- run_up_distance_mm=self.panda_runup_distance_mm,
86
+ set_stub_offsets=get_hyperion_config_client()
87
+ .get_feature_flags()
88
+ .SET_STUB_OFFSETS,
89
+ run_up_distance_mm=get_hyperion_config_client()
90
+ .get_feature_flags()
91
+ .PANDA_RUNUP_DISTANCE_MM,
80
92
  transmission_fraction=self.transmission_frac,
81
93
  )
82
94
 
@@ -84,13 +96,9 @@ class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGr
84
96
  class OddYStepsException(Exception): ...
85
97
 
86
98
 
87
- class PinTipCentreThenXrayCentre(
88
- GridCommonWithHyperionDetectorParams, WithHyperionUDCFeatures
89
- ):
99
+ class PinTipCentreThenXrayCentre(GridCommonWithHyperionDetectorParams):
90
100
  tip_offset_um: float = 0
91
101
 
92
102
 
93
- class GridScanWithEdgeDetect(
94
- GridCommonWithHyperionDetectorParams, WithHyperionUDCFeatures
95
- ):
103
+ class GridScanWithEdgeDetect(GridCommonWithHyperionDetectorParams):
96
104
  pass