mx-bluesky 1.5.5__py3-none-any.whl → 1.5.7__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 (38) hide show
  1. mx_bluesky/_version.py +2 -2
  2. mx_bluesky/beamlines/i02_1/parameters/__init__.py +0 -0
  3. mx_bluesky/beamlines/i02_1/parameters/gridscan.py +35 -0
  4. mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +6 -3
  5. mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +3 -1
  6. mx_bluesky/beamlines/i04/thawing_plan.py +15 -8
  7. mx_bluesky/beamlines/i24/jungfrau_commissioning/do_external_acquisition.py +44 -0
  8. mx_bluesky/beamlines/i24/jungfrau_commissioning/do_internal_acquisition.py +46 -0
  9. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_utils.py +73 -0
  10. mx_bluesky/common/device_setup_plans/robot_load_unload.py +2 -1
  11. mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +29 -5
  12. mx_bluesky/common/experiment_plans/inner_plans/do_fgs.py +7 -8
  13. mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +6 -6
  14. mx_bluesky/common/external_interaction/callbacks/common/zocalo_callback.py +30 -22
  15. mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py +73 -15
  16. mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_mapping.py +0 -20
  17. mx_bluesky/common/parameters/components.py +1 -0
  18. mx_bluesky/common/parameters/device_composites.py +2 -2
  19. mx_bluesky/common/parameters/gridscan.py +67 -49
  20. mx_bluesky/hyperion/__main__.py +16 -3
  21. mx_bluesky/hyperion/baton_handler.py +39 -9
  22. mx_bluesky/hyperion/device_setup_plans/smargon.py +13 -8
  23. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +19 -8
  24. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +2 -2
  25. mx_bluesky/hyperion/external_interaction/agamemnon.py +6 -2
  26. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +10 -2
  27. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +37 -1
  28. mx_bluesky/hyperion/parameters/constants.py +1 -0
  29. mx_bluesky/hyperion/parameters/device_composites.py +2 -2
  30. mx_bluesky/hyperion/parameters/gridscan.py +3 -3
  31. mx_bluesky/hyperion/plan_runner.py +2 -4
  32. mx_bluesky/hyperion/plan_runner_api.py +43 -0
  33. {mx_bluesky-1.5.5.dist-info → mx_bluesky-1.5.7.dist-info}/METADATA +2 -2
  34. {mx_bluesky-1.5.5.dist-info → mx_bluesky-1.5.7.dist-info}/RECORD +38 -32
  35. {mx_bluesky-1.5.5.dist-info → mx_bluesky-1.5.7.dist-info}/WHEEL +0 -0
  36. {mx_bluesky-1.5.5.dist-info → mx_bluesky-1.5.7.dist-info}/entry_points.txt +0 -0
  37. {mx_bluesky-1.5.5.dist-info → mx_bluesky-1.5.7.dist-info}/licenses/LICENSE +0 -0
  38. {mx_bluesky-1.5.5.dist-info → mx_bluesky-1.5.7.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,28 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable, Sequence
4
+ from enum import StrEnum
5
+ from math import isclose
4
6
  from time import time
5
7
  from typing import TYPE_CHECKING, Any, TypeVar
6
8
 
7
9
  from bluesky import preprocessors as bpp
8
10
  from bluesky.utils import MsgGenerator, make_decorator
11
+ from dodal.devices.zocalo import ZocaloStartInfo
9
12
 
10
13
  from mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base import (
11
14
  BaseISPyBCallback,
15
+ D,
12
16
  )
13
17
  from mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping import (
14
18
  populate_data_collection_group,
15
19
  populate_remaining_data_collection_info,
16
20
  )
21
+ from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import (
22
+ ZocaloInfoGenerator,
23
+ )
17
24
  from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping import (
18
25
  construct_comment_for_gridscan,
19
- populate_xy_data_collection_info,
20
- populate_xz_data_collection_info,
21
26
  )
22
27
  from mx_bluesky.common.external_interaction.ispyb.data_model import (
23
28
  DataCollectionGridInfo,
@@ -33,14 +38,21 @@ from mx_bluesky.common.external_interaction.ispyb.ispyb_store import (
33
38
  )
34
39
  from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample
35
40
  from mx_bluesky.common.parameters.constants import DocDescriptorNames, PlanNameConstants
36
- from mx_bluesky.common.parameters.gridscan import (
37
- GridCommon,
38
- )
41
+ from mx_bluesky.common.parameters.gridscan import GridCommon
39
42
  from mx_bluesky.common.utils.exceptions import (
40
43
  ISPyBDepositionNotMade,
41
44
  SampleException,
42
45
  )
43
46
  from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, set_dcgid_tag
47
+ from mx_bluesky.common.utils.utils import number_of_frames_from_scan_spec
48
+
49
+ OMEGA_TOLERANCE = 1
50
+
51
+
52
+ class GridscanPlane(StrEnum):
53
+ OMEGA_XY = "0"
54
+ OMEGA_XZ = "90"
55
+
44
56
 
45
57
  if TYPE_CHECKING:
46
58
  from event_model import Event, RunStart, RunStop
@@ -89,10 +101,10 @@ class GridscanISPyBCallback(BaseISPyBCallback):
89
101
  ) -> None:
90
102
  super().__init__(emit=emit)
91
103
  self.ispyb: StoreInIspyb
92
- self.ispyb_ids: IspybIds = IspybIds()
93
104
  self.param_type = param_type
94
105
  self._start_of_fgs_uid: str | None = None
95
106
  self._processing_start_time: float | None = None
107
+ self._grid_plane_to_id_map: dict[GridscanPlane, int] = {}
96
108
  self.data_collection_group_info: DataCollectionGroupInfo | None
97
109
 
98
110
  def activity_gated_start(self, doc: RunStart):
@@ -108,6 +120,7 @@ class GridscanISPyBCallback(BaseISPyBCallback):
108
120
  mx_bluesky_parameters = doc.get("mx_bluesky_parameters")
109
121
  assert isinstance(mx_bluesky_parameters, str)
110
122
  self.params = self.param_type.model_validate_json(mx_bluesky_parameters)
123
+ assert isinstance(self.params, DiffractionExperimentWithSample)
111
124
  self.ispyb = StoreInIspyb(self.ispyb_config)
112
125
  self.data_collection_group_info = populate_data_collection_group(
113
126
  self.params
@@ -118,8 +131,8 @@ class GridscanISPyBCallback(BaseISPyBCallback):
118
131
  data_collection_info=populate_remaining_data_collection_info(
119
132
  "MX-Bluesky: Xray centring 1 -",
120
133
  None,
121
- populate_xy_data_collection_info(
122
- self.params.detector_params,
134
+ DataCollectionInfo(
135
+ data_collection_number=self.params.detector_params.run_number,
123
136
  ),
124
137
  self.params,
125
138
  ),
@@ -128,7 +141,11 @@ class GridscanISPyBCallback(BaseISPyBCallback):
128
141
  data_collection_info=populate_remaining_data_collection_info(
129
142
  "MX-Bluesky: Xray centring 2 -",
130
143
  None,
131
- populate_xz_data_collection_info(self.params.detector_params),
144
+ DataCollectionInfo(
145
+ data_collection_number=(
146
+ self.params.detector_params.run_number + 1
147
+ ),
148
+ ),
132
149
  self.params,
133
150
  )
134
151
  ),
@@ -175,7 +192,6 @@ class GridscanISPyBCallback(BaseISPyBCallback):
175
192
  assert self.params, "ISPyB handler didn't receive parameters!"
176
193
  assert self.data_collection_group_info, "No data collection group"
177
194
  data = doc["data"]
178
- data_collection_id = None
179
195
  data_collection_info = DataCollectionInfo(
180
196
  xtal_snapshot1=data.get("oav-grid_snapshot-last_path_full_overlay"),
181
197
  xtal_snapshot2=data.get("oav-grid_snapshot-last_path_outer"),
@@ -214,10 +230,9 @@ class GridscanISPyBCallback(BaseISPyBCallback):
214
230
  f"by {data_collection_grid_info.steps_y} "
215
231
  )
216
232
 
217
- if len(self.ispyb_ids.data_collection_ids) > self._oav_snapshot_event_idx:
218
- data_collection_id = self.ispyb_ids.data_collection_ids[
219
- self._oav_snapshot_event_idx
220
- ]
233
+ data_collection_id = self.ispyb_ids.data_collection_ids[
234
+ self._oav_snapshot_event_idx
235
+ ]
221
236
  self._populate_axis_info(data_collection_info, doc["data"]["smargon-omega"])
222
237
 
223
238
  scan_data_info = ScanDataInfo(
@@ -228,6 +243,11 @@ class GridscanISPyBCallback(BaseISPyBCallback):
228
243
  ISPYB_ZOCALO_CALLBACK_LOGGER.info(
229
244
  "Updating ispyb data collection after oav snapshot."
230
245
  )
246
+ grid_plane = _smargon_omega_to_xyxz_plane(doc["data"]["smargon-omega"])
247
+ # Snapshots may be triggered in a different order to gridscans, so save
248
+ # the mapping to the data collection id in order to trigger Zocalo correctly.
249
+ self._grid_plane_to_id_map[grid_plane] = data_collection_id
250
+
231
251
  self._oav_snapshot_event_idx += 1
232
252
  return [scan_data_info]
233
253
 
@@ -294,5 +314,43 @@ class GridscanISPyBCallback(BaseISPyBCallback):
294
314
  self.ispyb_ids.data_collection_group_id,
295
315
  )
296
316
  self.data_collection_group_info = None
317
+ self._grid_plane_to_id_map.clear()
297
318
  return super().activity_gated_stop(doc)
298
- return self._tag_doc(doc)
319
+ return self.tag_doc(doc)
320
+
321
+ def tag_doc(self, doc: D) -> D:
322
+ doc = super().tag_doc(doc)
323
+ assert isinstance(doc, dict)
324
+ if self._grid_plane_to_id_map:
325
+ doc["grid_plane_to_id_map"] = self._grid_plane_to_id_map
326
+ return doc # type: ignore
327
+
328
+
329
+ def generate_start_info_from_omega_map() -> ZocaloInfoGenerator:
330
+ """
331
+ Generate the zocalo trigger info from bluesky runs where the frame number is
332
+ computed using metadata added to the document by the ISPyB callback and the
333
+ run start which together can be used to determine the correct frame numbering.
334
+ """
335
+ doc = yield []
336
+ omega_to_scan_spec = doc["omega_to_scan_spec"]
337
+ start_frame = 0
338
+ infos = []
339
+ for i, omega in enumerate([GridscanPlane.OMEGA_XY, GridscanPlane.OMEGA_XZ]):
340
+ frames = number_of_frames_from_scan_spec(omega_to_scan_spec[omega])
341
+ infos.append(
342
+ ZocaloStartInfo(
343
+ doc["grid_plane_to_id_map"][omega], None, start_frame, frames, i
344
+ )
345
+ )
346
+ start_frame += frames
347
+ yield infos
348
+
349
+
350
+ def _smargon_omega_to_xyxz_plane(smargon_omega: float) -> GridscanPlane:
351
+ modulo_180 = abs(smargon_omega) % 180
352
+ is_xy = isclose(modulo_180, 0, abs_tol=OMEGA_TOLERANCE)
353
+ assert is_xy or isclose(modulo_180, 90, abs_tol=OMEGA_TOLERANCE), (
354
+ f"Smargon snapshot omega not in tolerance of compass point {smargon_omega}"
355
+ )
356
+ return GridscanPlane.OMEGA_XY if is_xy else GridscanPlane.OMEGA_XZ
@@ -1,33 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import numpy
4
- from dodal.devices.detector import DetectorParams
5
4
  from dodal.devices.oav import utils as oav_utils
6
5
 
7
6
  from mx_bluesky.common.external_interaction.ispyb.data_model import (
8
7
  DataCollectionGridInfo,
9
- DataCollectionInfo,
10
8
  )
11
9
 
12
10
 
13
- def populate_xz_data_collection_info(detector_params: DetectorParams):
14
- assert (
15
- detector_params.omega_start is not None
16
- and detector_params.run_number is not None
17
- ), "StoreGridscanInIspyb failed to get parameters"
18
- run_number = detector_params.run_number + 1
19
- info = DataCollectionInfo(
20
- data_collection_number=run_number,
21
- )
22
- return info
23
-
24
-
25
- def populate_xy_data_collection_info(detector_params: DetectorParams):
26
- return DataCollectionInfo(
27
- data_collection_number=detector_params.run_number,
28
- )
29
-
30
-
31
11
  def construct_comment_for_gridscan(grid_info: DataCollectionGridInfo) -> str:
32
12
  assert grid_info is not None, "StoreGridScanInIspyb failed to get parameters"
33
13
 
@@ -231,6 +231,7 @@ class DiffractionExperimentWithSample(DiffractionExperiment, WithSample): ...
231
231
 
232
232
  class MultiXtalSelection(BaseModel):
233
233
  name: str
234
+ ignore_xtal_not_found: bool = False
234
235
 
235
236
 
236
237
  class TopNByMaxCountSelection(MultiXtalSelection):
@@ -8,7 +8,7 @@ from dodal.devices.common_dcm import BaseDCM
8
8
  from dodal.devices.detector.detector_motion import DetectorMotion
9
9
  from dodal.devices.eiger import EigerDetector
10
10
  from dodal.devices.fast_grid_scan import (
11
- ZebraFastGridScan,
11
+ ZebraFastGridScanThreeD,
12
12
  )
13
13
  from dodal.devices.flux import Flux
14
14
  from dodal.devices.mx_phase1.beamstop import Beamstop
@@ -53,7 +53,7 @@ class GridDetectThenXRayCentreComposite(FlyScanEssentialDevices):
53
53
  beamstop: Beamstop
54
54
  dcm: BaseDCM
55
55
  detector_motion: DetectorMotion
56
- zebra_fast_grid_scan: ZebraFastGridScan
56
+ zebra_fast_grid_scan: ZebraFastGridScanThreeD
57
57
  flux: Flux
58
58
  oav: OAV
59
59
  pin_tip_detection: PinTipDetection
@@ -1,15 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from abc import abstractmethod
4
+ from typing import Generic, TypeVar
5
+
3
6
  from dodal.devices.aperturescatterguard import ApertureValue
4
7
  from dodal.devices.detector.det_dim_constants import EIGER2_X_9M_SIZE, EIGER2_X_16M_SIZE
5
8
  from dodal.devices.detector.detector import DetectorParams
6
9
  from dodal.devices.fast_grid_scan import (
7
- ZebraGridScanParams,
10
+ GridScanParamsCommon,
11
+ ZebraGridScanParamsThreeD,
8
12
  )
9
13
  from dodal.utils import get_beamline_name
10
14
  from pydantic import Field, PrivateAttr
11
15
  from scanspec.core import Path as ScanPath
12
- from scanspec.specs import Line, Static
16
+ from scanspec.specs import Concat, Line, Product, Static
13
17
 
14
18
  from mx_bluesky.common.parameters.components import (
15
19
  DiffractionExperimentWithSample,
@@ -33,6 +37,10 @@ DETECTOR_SIZE_PER_BEAMLINE = {
33
37
  "i04": EIGER2_X_16M_SIZE,
34
38
  }
35
39
 
40
+ GridScanParamType = TypeVar(
41
+ "GridScanParamType", bound=GridScanParamsCommon, covariant=True
42
+ )
43
+
36
44
 
37
45
  class GridCommon(
38
46
  DiffractionExperimentWithSample,
@@ -87,36 +95,80 @@ class GridCommon(
87
95
  )
88
96
 
89
97
 
90
- class SpecifiedGrid(XyzStarts, WithScan):
98
+ class SpecifiedGrid(GridCommon, XyzStarts, WithScan, Generic[GridScanParamType]):
91
99
  """A specified grid is one which has defined values for the start position,
92
100
  grid and box sizes, etc., as opposed to parameters for a plan which will create
93
101
  those parameters at some point (e.g. through optical pin detection)."""
94
102
 
103
+ grid1_omega_deg: float = Field(default=GridscanParamConstants.OMEGA_1)
104
+ x_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)
105
+ y_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)
106
+ x_steps: int = Field(gt=0)
107
+ y_steps: int = Field(gt=0)
108
+ _set_stub_offsets: bool = PrivateAttr(default_factory=lambda: False)
109
+
110
+ @property
111
+ @abstractmethod
112
+ def FGS_params(self) -> GridScanParamType: ...
113
+
114
+ def do_set_stub_offsets(self, value: bool):
115
+ self._set_stub_offsets = value
116
+
117
+ @property
118
+ def grid_1_spec(self):
119
+ x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1)
120
+ y1_end = self.y_start_um + self.y_step_size_um * (self.y_steps - 1)
121
+ grid_1_x = Line("sam_x", self.x_start_um, x_end, self.x_steps)
122
+ grid_1_y = Line("sam_y", self.y_start_um, y1_end, self.y_steps)
123
+ grid_1_z = Static("sam_z", self.z_start_um)
124
+ return grid_1_y.zip(grid_1_z) * ~grid_1_x
125
+
126
+ @property
127
+ def scan_indices(self) -> list[int]:
128
+ """The first index of each gridscan, useful for writing nexus files/VDS"""
129
+ return [
130
+ 0,
131
+ len(ScanPath(self.grid_1_spec.calculate()).consume().midpoints["sam_x"]),
132
+ ]
133
+
134
+ @property
135
+ @abstractmethod
136
+ def scan_spec(self) -> Product[str] | Concat[str]:
137
+ """A fully specified ScanSpec object representing all grids, with x, y, z and
138
+ omega positions."""
139
+
140
+ @property
141
+ def scan_points(self):
142
+ """A list of all the points in the scan_spec."""
143
+ return ScanPath(self.scan_spec.calculate()).consume().midpoints
144
+
145
+ @property
146
+ def scan_points_first_grid(self):
147
+ """A list of all the points in the first grid scan."""
148
+ return ScanPath(self.grid_1_spec.calculate()).consume().midpoints
149
+
150
+ @property
151
+ def num_images(self) -> int:
152
+ return len(self.scan_points["sam_x"])
153
+
95
154
 
96
155
  class SpecifiedThreeDGridScan(
97
- GridCommon,
98
- SpecifiedGrid,
156
+ SpecifiedGrid[ZebraGridScanParamsThreeD],
99
157
  SplitScan,
100
158
  WithOptionalEnergyChange,
101
159
  ):
102
160
  """Parameters representing a so-called 3D grid scan, which consists of doing a
103
161
  gridscan in X and Y, followed by one in X and Z."""
104
162
 
105
- grid1_omega_deg: float = Field(default=GridscanParamConstants.OMEGA_1)
106
- grid2_omega_deg: float = Field(default=GridscanParamConstants.OMEGA_2)
107
- x_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)
108
- y_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)
163
+ z_steps: int = Field(gt=0)
109
164
  z_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)
110
165
  y2_start_um: float
111
166
  z2_start_um: float
112
- x_steps: int = Field(gt=0)
113
- y_steps: int = Field(gt=0)
114
- z_steps: int = Field(gt=0)
115
- _set_stub_offsets: bool = PrivateAttr(default_factory=lambda: False)
167
+ grid2_omega_deg: float = Field(default=GridscanParamConstants.OMEGA_2)
116
168
 
117
169
  @property
118
- def FGS_params(self) -> ZebraGridScanParams:
119
- return ZebraGridScanParams(
170
+ def FGS_params(self) -> ZebraGridScanParamsThreeD:
171
+ return ZebraGridScanParamsThreeD(
120
172
  x_steps=self.x_steps,
121
173
  y_steps=self.y_steps,
122
174
  z_steps=self.z_steps,
@@ -133,18 +185,6 @@ class SpecifiedThreeDGridScan(
133
185
  transmission_fraction=self.transmission_frac,
134
186
  )
135
187
 
136
- def do_set_stub_offsets(self, value: bool):
137
- self._set_stub_offsets = value
138
-
139
- @property
140
- def grid_1_spec(self):
141
- x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1)
142
- y1_end = self.y_start_um + self.y_step_size_um * (self.y_steps - 1)
143
- grid_1_x = Line("sam_x", self.x_start_um, x_end, self.x_steps)
144
- grid_1_y = Line("sam_y", self.y_start_um, y1_end, self.y_steps)
145
- grid_1_z = Static("sam_z", self.z_start_um)
146
- return grid_1_y.zip(grid_1_z) * ~grid_1_x
147
-
148
188
  @property
149
189
  def grid_2_spec(self):
150
190
  x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1)
@@ -154,35 +194,13 @@ class SpecifiedThreeDGridScan(
154
194
  grid_2_y = Static("sam_y", self.y2_start_um)
155
195
  return grid_2_z.zip(grid_2_y) * ~grid_2_x
156
196
 
157
- @property
158
- def scan_indices(self):
159
- """The first index of each gridscan, useful for writing nexus files/VDS"""
160
- return [
161
- 0,
162
- len(ScanPath(self.grid_1_spec.calculate()).consume().midpoints["sam_x"]),
163
- ]
164
-
165
197
  @property
166
198
  def scan_spec(self):
167
199
  """A fully specified ScanSpec object representing both grids, with x, y, z and
168
200
  omega positions."""
169
201
  return self.grid_1_spec.concat(self.grid_2_spec)
170
202
 
171
- @property
172
- def scan_points(self):
173
- """A list of all the points in the scan_spec."""
174
- return ScanPath(self.scan_spec.calculate()).consume().midpoints
175
-
176
- @property
177
- def scan_points_first_grid(self):
178
- """A list of all the points in the first grid scan."""
179
- return ScanPath(self.grid_1_spec.calculate()).consume().midpoints
180
-
181
203
  @property
182
204
  def scan_points_second_grid(self):
183
205
  """A list of all the points in the second grid scan."""
184
206
  return ScanPath(self.grid_2_spec.calculate()).consume().midpoints
185
-
186
- @property
187
- def num_images(self) -> int:
188
- return len(self.scan_points["sam_x"])
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import signal
2
3
  import threading
3
4
  from dataclasses import asdict
4
5
  from sys import argv
@@ -32,9 +33,10 @@ from mx_bluesky.hyperion.parameters.cli import (
32
33
  HyperionMode,
33
34
  parse_cli_args,
34
35
  )
35
- from mx_bluesky.hyperion.parameters.constants import CONST
36
+ from mx_bluesky.hyperion.parameters.constants import CONST, HyperionConstants
36
37
  from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
37
38
  from mx_bluesky.hyperion.plan_runner import PlanRunner
39
+ from mx_bluesky.hyperion.plan_runner_api import create_server_for_udc
38
40
  from mx_bluesky.hyperion.runner import (
39
41
  GDARunner,
40
42
  StatusAndMessage,
@@ -170,7 +172,7 @@ def main():
170
172
  """Main application entry point."""
171
173
  args = parse_cli_args()
172
174
  initialise_globals(args)
173
- hyperion_port = 5005
175
+ hyperion_port = HyperionConstants.HYPERION_PORT
174
176
  context = setup_context(dev_mode=args.dev_mode)
175
177
 
176
178
  if args.mode == HyperionMode.GDA:
@@ -188,7 +190,18 @@ def main():
188
190
  )
189
191
  runner.wait_on_queue()
190
192
  else:
191
- run_forever(PlanRunner(context))
193
+ plan_runner = PlanRunner(context, args.dev_mode)
194
+ create_server_for_udc(plan_runner)
195
+ _register_sigterm_handler(plan_runner)
196
+ run_forever(plan_runner)
197
+
198
+
199
+ def _register_sigterm_handler(runner: PlanRunner):
200
+ def shutdown_on_sigterm(sig_num, frame):
201
+ LOGGER.info("Received SIGTERM, shutting down...")
202
+ runner.shutdown()
203
+
204
+ signal.signal(signal.SIGTERM, shutdown_on_sigterm)
192
205
 
193
206
 
194
207
  if __name__ == "__main__":
@@ -6,8 +6,14 @@ from blueapi.core.context import BlueskyContext
6
6
  from bluesky import plan_stubs as bps
7
7
  from bluesky import preprocessors as bpp
8
8
  from bluesky.utils import MsgGenerator, RunEngineInterrupted
9
+ from dodal.common.beamlines.commissioning_mode import set_commissioning_signal
10
+ from dodal.devices.aperturescatterguard import ApertureScatterguard
9
11
  from dodal.devices.baton import Baton
12
+ from dodal.devices.motors import XYZStage
13
+ from dodal.devices.robot import BartRobot
14
+ from dodal.devices.smargon import Smargon
10
15
 
16
+ from mx_bluesky.common.device_setup_plans.robot_load_unload import robot_unload
11
17
  from mx_bluesky.common.experiment_plans.inner_plans.udc_default_state import (
12
18
  UDCDefaultDevices,
13
19
  move_to_udc_default_state,
@@ -77,6 +83,7 @@ def run_udc_when_requested(context: BlueskyContext, runner: PlanRunner):
77
83
 
78
84
  def acquire_baton() -> MsgGenerator:
79
85
  yield from _wait_for_hyperion_requested(baton)
86
+ LOGGER.debug("Hyperion is now current baton holder.")
80
87
  yield from bps.abs_set(baton.current_user, HYPERION_USER)
81
88
 
82
89
  def collect() -> MsgGenerator:
@@ -96,14 +103,20 @@ def run_udc_when_requested(context: BlueskyContext, runner: PlanRunner):
96
103
 
97
104
  # re-fetch the baton because the device has been reinstantiated
98
105
  baton = _get_baton(context)
106
+ current_visit: str | None = None
99
107
  while (yield from _is_requesting_baton(baton)):
100
- yield from _fetch_and_process_agamemnon_instruction(baton, runner)
108
+ current_visit = yield from _fetch_and_process_agamemnon_instruction(
109
+ baton, runner, current_visit
110
+ )
111
+ if current_visit:
112
+ yield from _perform_robot_unload(runner.context, current_visit)
101
113
 
102
114
  def release_baton() -> MsgGenerator:
103
115
  # If hyperion has given up the baton itself we need to also release requested
104
116
  # user so that hyperion doesn't think we're requested again
105
117
  baton = _get_baton(context)
106
- previous_requested_user = yield from _safely_release_baton(baton)
118
+ previous_requested_user = yield from _unrequest_baton(baton)
119
+ LOGGER.debug("Hyperion no longer current baton holder.")
107
120
  yield from bps.abs_set(baton.current_user, NO_USER, wait=True)
108
121
  _raise_baton_released_alert(get_alerting_service(), previous_requested_user)
109
122
 
@@ -111,11 +124,11 @@ def run_udc_when_requested(context: BlueskyContext, runner: PlanRunner):
111
124
  yield from bpp.contingency_wrapper(collect(), final_plan=release_baton)
112
125
 
113
126
  context.run_engine(acquire_baton())
114
- _initialise_udc(context)
127
+ _initialise_udc(context, runner.is_dev_mode)
115
128
  context.run_engine(collect_then_release())
116
129
 
117
130
 
118
- def _initialise_udc(context: BlueskyContext):
131
+ def _initialise_udc(context: BlueskyContext, dev_mode: bool):
119
132
  """
120
133
  Perform all initialisation that happens at the start of UDC just after the
121
134
  baton is acquired, but before we execute any plans or move hardware.
@@ -125,21 +138,25 @@ def _initialise_udc(context: BlueskyContext):
125
138
  """
126
139
  LOGGER.info("Initialising mx-bluesky for UDC start...")
127
140
  clear_all_device_caches(context)
128
- setup_devices(context, False)
141
+ LOGGER.debug("Reinitialising beamline devices")
142
+ setup_devices(context, dev_mode)
143
+ set_commissioning_signal(_get_baton(context).commissioning)
129
144
 
130
145
 
131
146
  def _wait_for_hyperion_requested(baton: Baton):
147
+ LOGGER.debug("Hyperion waiting for baton...")
132
148
  SLEEP_PER_CHECK = 0.1
133
149
  while True:
134
150
  requested_user = yield from bps.rd(baton.requested_user)
135
151
  if requested_user == HYPERION_USER:
152
+ LOGGER.debug("Baton requested for Hyperion")
136
153
  break
137
154
  yield from bps.sleep(SLEEP_PER_CHECK)
138
155
 
139
156
 
140
157
  def _fetch_and_process_agamemnon_instruction(
141
- baton: Baton, runner: PlanRunner
142
- ) -> MsgGenerator:
158
+ baton: Baton, runner: PlanRunner, current_visit: str | None
159
+ ) -> MsgGenerator[str | None]:
143
160
  parameter_list: Sequence[MxBlueskyParameters] = create_parameters_from_agamemnon()
144
161
  if parameter_list:
145
162
  for parameters in parameter_list:
@@ -148,6 +165,7 @@ def _fetch_and_process_agamemnon_instruction(
148
165
  )
149
166
  match parameters:
150
167
  case LoadCentreCollect():
168
+ current_visit = parameters.visit
151
169
  devices: Any = create_devices(runner.context)
152
170
  yield from runner.execute_plan(
153
171
  partial(load_centre_collect_full, devices, parameters)
@@ -161,7 +179,8 @@ def _fetch_and_process_agamemnon_instruction(
161
179
  else:
162
180
  _raise_udc_completed_alert(get_alerting_service())
163
181
  # Release the baton for orderly exit from the instruction loop
164
- yield from _safely_release_baton(baton)
182
+ yield from _unrequest_baton(baton)
183
+ return current_visit
165
184
 
166
185
 
167
186
  def _raise_udc_start_alert(alert_service: AlertService):
@@ -205,7 +224,7 @@ def _get_baton(context: BlueskyContext) -> Baton:
205
224
  return find_device_in_context(context, "baton", Baton)
206
225
 
207
226
 
208
- def _safely_release_baton(baton: Baton) -> MsgGenerator[str]:
227
+ def _unrequest_baton(baton: Baton) -> MsgGenerator[str]:
209
228
  """Relinquish the requested user of the baton if it is not already requested
210
229
  by another user.
211
230
 
@@ -214,6 +233,17 @@ def _safely_release_baton(baton: Baton) -> MsgGenerator[str]:
214
233
  """
215
234
  requested_user = yield from bps.rd(baton.requested_user)
216
235
  if requested_user == HYPERION_USER:
236
+ LOGGER.debug("Hyperion no longer requesting baton")
217
237
  yield from bps.abs_set(baton.requested_user, NO_USER)
218
238
  return NO_USER
219
239
  return requested_user
240
+
241
+
242
+ def _perform_robot_unload(context: BlueskyContext, visit: str) -> MsgGenerator:
243
+ robot = find_device_in_context(context, "robot", BartRobot)
244
+ smargon = find_device_in_context(context, "smargon", Smargon)
245
+ aperture_scatterguard = find_device_in_context(
246
+ context, "aperture_scatterguard", ApertureScatterguard
247
+ )
248
+ lower_gonio = find_device_in_context(context, "lower_gonio", XYZStage)
249
+ yield from robot_unload(robot, smargon, aperture_scatterguard, lower_gonio, visit)
@@ -1,6 +1,8 @@
1
1
  import numpy as np
2
2
  from bluesky import plan_stubs as bps
3
+ from bluesky.utils import FailedStatus
3
4
  from dodal.devices.smargon import CombinedMove, Smargon
5
+ from ophyd_async.epics.motor import MotorLimitsException
4
6
 
5
7
  from mx_bluesky.common.utils.exceptions import SampleException
6
8
 
@@ -9,12 +11,15 @@ def move_smargon_warn_on_out_of_range(
9
11
  smargon: Smargon, position: np.ndarray | list[float] | tuple[float, float, float]
10
12
  ):
11
13
  """Throws a SampleException if the specified position is out of range for the
12
- smargon. Otherwise moves to that position."""
13
- limits = yield from smargon.get_xyz_limits()
14
- if not limits.position_valid(position):
15
- raise SampleException(
16
- "Pin tip centring failed - pin too long/short/bent and out of range"
14
+ smargon. Otherwise moves to that position. The check is from ophyd-async"""
15
+ try:
16
+ yield from bps.mv(
17
+ smargon, CombinedMove(x=position[0], y=position[1], z=position[2])
17
18
  )
18
- yield from bps.mv(
19
- smargon, CombinedMove(x=position[0], y=position[1], z=position[2])
20
- )
19
+ except FailedStatus as fs:
20
+ if isinstance(fs.__cause__, MotorLimitsException):
21
+ raise SampleException(
22
+ "Pin tip centring failed - pin too long/short/bent and out of range"
23
+ ) from fs.__cause__
24
+ else:
25
+ raise fs