mx-bluesky 1.4.6__py3-none-any.whl → 1.4.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 (62) hide show
  1. mx_bluesky/_version.py +2 -2
  2. mx_bluesky/beamlines/aithre_lasershaping/__init__.py +13 -0
  3. mx_bluesky/beamlines/aithre_lasershaping/check_goniometer_performance.py +29 -0
  4. mx_bluesky/beamlines/aithre_lasershaping/goniometer_controls.py +18 -0
  5. mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +31 -25
  6. mx_bluesky/beamlines/i04/thawing_plan.py +10 -1
  7. mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +12 -12
  8. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +30 -29
  9. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +10 -11
  10. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +8 -10
  11. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +10 -3
  12. mx_bluesky/beamlines/i24/serial/log.py +1 -0
  13. mx_bluesky/beamlines/i24/serial/set_visit_directory.sh +1 -1
  14. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +16 -16
  15. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +47 -48
  16. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +1 -1
  17. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +9 -7
  18. mx_bluesky/beamlines/i24/serial/write_nexus.py +3 -2
  19. mx_bluesky/common/device_setup_plans/xbpm_feedback.py +45 -0
  20. mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +2 -4
  21. mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +1 -1
  22. mx_bluesky/common/external_interaction/callbacks/common/zocalo_callback.py +18 -15
  23. mx_bluesky/{hyperion → common}/external_interaction/callbacks/sample_handling/sample_handling_callback.py +16 -4
  24. mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py +41 -5
  25. mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py +2 -1
  26. mx_bluesky/common/external_interaction/ispyb/data_model.py +1 -0
  27. mx_bluesky/common/external_interaction/ispyb/ispyb_store.py +14 -1
  28. mx_bluesky/common/external_interaction/nexus/nexus_utils.py +1 -1
  29. mx_bluesky/common/parameters/constants.py +2 -0
  30. mx_bluesky/common/parameters/gridscan.py +1 -1
  31. mx_bluesky/common/plans/write_sample_status.py +46 -0
  32. mx_bluesky/common/preprocessors/__init__.py +0 -0
  33. mx_bluesky/common/preprocessors/preprocessors.py +105 -0
  34. mx_bluesky/common/protocols/__init__.py +0 -0
  35. mx_bluesky/common/protocols/protocols.py +10 -0
  36. mx_bluesky/hyperion/__main__.py +3 -9
  37. mx_bluesky/hyperion/baton_handler.py +84 -0
  38. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +5 -1
  39. mx_bluesky/hyperion/experiment_plans/__init__.py +0 -4
  40. mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py +10 -25
  41. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +0 -7
  42. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +11 -10
  43. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +5 -1
  44. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +2 -2
  45. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +5 -3
  46. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +0 -26
  47. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +23 -18
  48. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +25 -6
  49. mx_bluesky/hyperion/external_interaction/agamemnon.py +148 -10
  50. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +12 -6
  51. mx_bluesky/hyperion/external_interaction/callbacks/snapshot_callback.py +107 -0
  52. mx_bluesky/hyperion/parameters/gridscan.py +2 -2
  53. mx_bluesky/hyperion/parameters/rotation.py +1 -1
  54. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.7.dist-info}/METADATA +7 -7
  55. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.7.dist-info}/RECORD +60 -51
  56. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.7.dist-info}/WHEEL +1 -1
  57. mx_bluesky/common/external_interaction/callbacks/common/aperture_change_callback.py +0 -22
  58. mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +0 -103
  59. /mx_bluesky/{hyperion → common}/external_interaction/callbacks/sample_handling/__init__.py +0 -0
  60. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.7.dist-info}/entry_points.txt +0 -0
  61. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.7.dist-info/licenses}/LICENSE +0 -0
  62. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.7.dist-info}/top_level.txt +0 -0
@@ -29,6 +29,7 @@ from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping
29
29
  )
30
30
  from mx_bluesky.common.external_interaction.ispyb.data_model import (
31
31
  DataCollectionGridInfo,
32
+ DataCollectionGroupInfo,
32
33
  DataCollectionInfo,
33
34
  DataCollectionPositionInfo,
34
35
  Orientation,
@@ -52,6 +53,9 @@ from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, set_dcgid_
52
53
  if TYPE_CHECKING:
53
54
  from event_model import Event, RunStart, RunStop
54
55
 
56
+ T = TypeVar("T", bound="GridCommon")
57
+ ASSERT_START_BEFORE_EVENT_DOC_MESSAGE = f"No data collection group info - event document has been emitted before a {PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN} start document"
58
+
55
59
 
56
60
  def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters):
57
61
  return bpp.set_run_key_wrapper(
@@ -67,9 +71,6 @@ def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters):
67
71
  )
68
72
 
69
73
 
70
- T = TypeVar("T", bound="GridCommon")
71
-
72
-
73
74
  class GridscanISPyBCallback(BaseISPyBCallback):
74
75
  """Callback class to handle the deposition of experiment parameters into the ISPyB
75
76
  database. Listens for 'event' and 'descriptor' documents. Creates the ISpyB entry on
@@ -97,6 +98,7 @@ class GridscanISPyBCallback(BaseISPyBCallback):
97
98
  self.param_type = param_type
98
99
  self._start_of_fgs_uid: str | None = None
99
100
  self._processing_start_time: float | None = None
101
+ self.data_collection_group_info: DataCollectionGroupInfo | None
100
102
 
101
103
  def activity_gated_start(self, doc: RunStart):
102
104
  if doc.get("subplan_name") == PlanNameConstants.DO_FGS:
@@ -111,7 +113,9 @@ class GridscanISPyBCallback(BaseISPyBCallback):
111
113
  assert isinstance(mx_bluesky_parameters, str)
112
114
  self.params = self.param_type.model_validate_json(mx_bluesky_parameters)
113
115
  self.ispyb = StoreInIspyb(self.ispyb_config)
114
- data_collection_group_info = populate_data_collection_group(self.params)
116
+ self.data_collection_group_info = populate_data_collection_group(
117
+ self.params
118
+ )
115
119
 
116
120
  scan_data_infos = [
117
121
  ScanDataInfo(
@@ -135,12 +139,13 @@ class GridscanISPyBCallback(BaseISPyBCallback):
135
139
  ]
136
140
 
137
141
  self.ispyb_ids = self.ispyb.begin_deposition(
138
- data_collection_group_info, scan_data_infos
142
+ self.data_collection_group_info, scan_data_infos
139
143
  )
140
144
  set_dcgid_tag(self.ispyb_ids.data_collection_group_id)
141
145
  return super().activity_gated_start(doc)
142
146
 
143
147
  def activity_gated_event(self, doc: Event):
148
+ assert self.data_collection_group_info, ASSERT_START_BEFORE_EVENT_DOC_MESSAGE
144
149
  doc = super().activity_gated_event(doc)
145
150
 
146
151
  descriptor_name = self.descriptors[doc["descriptor"]].get("name")
@@ -151,10 +156,14 @@ class GridscanISPyBCallback(BaseISPyBCallback):
151
156
  self.ispyb_ids = self.ispyb.update_deposition(
152
157
  self.ispyb_ids, scan_data_infos
153
158
  )
159
+ self.ispyb.update_data_collection_group_table(
160
+ self.data_collection_group_info, self.ispyb_ids.data_collection_group_id
161
+ )
154
162
 
155
163
  return doc
156
164
 
157
165
  def _handle_zocalo_read_event(self, doc):
166
+ assert self.data_collection_group_info, ASSERT_START_BEFORE_EVENT_DOC_MESSAGE
158
167
  crystal_summary = ""
159
168
  if self._processing_start_time is not None:
160
169
  proc_time = time() - self._processing_start_time
@@ -185,6 +194,11 @@ class GridscanISPyBCallback(BaseISPyBCallback):
185
194
  assert self.ispyb_ids.data_collection_ids, (
186
195
  "No data collection to add results to"
187
196
  )
197
+
198
+ self.data_collection_group_info.comments = (
199
+ self.data_collection_group_info.comments or ""
200
+ ) + crystal_summary
201
+
188
202
  self.ispyb.append_to_comment(
189
203
  self.ispyb_ids.data_collection_ids[0], crystal_summary
190
204
  )
@@ -192,6 +206,7 @@ class GridscanISPyBCallback(BaseISPyBCallback):
192
206
  def _handle_oav_grid_snapshot_triggered(self, doc) -> Sequence[ScanDataInfo]:
193
207
  assert self.ispyb_ids.data_collection_ids, "No current data collection"
194
208
  assert self.params, "ISPyB handler didn't receive parameters!"
209
+ assert self.data_collection_group_info, "No data collection group"
195
210
  data = doc["data"]
196
211
  data_collection_id = None
197
212
  data_collection_info = DataCollectionInfo(
@@ -220,6 +235,18 @@ class GridscanISPyBCallback(BaseISPyBCallback):
220
235
  data_collection_info.comments = construct_comment_for_gridscan(
221
236
  data_collection_grid_info
222
237
  )
238
+
239
+ if self.data_collection_group_info.comments:
240
+ self.data_collection_group_info.comments += (
241
+ f"by {data_collection_grid_info.steps_y}."
242
+ )
243
+ else:
244
+ self.data_collection_group_info.comments = (
245
+ f"Diffraction grid scan of "
246
+ f"{data_collection_grid_info.steps_x} "
247
+ f"by {data_collection_grid_info.steps_y} "
248
+ )
249
+
223
250
  if len(self.ispyb_ids.data_collection_ids) > self._oav_snapshot_event_idx:
224
251
  data_collection_id = self.ispyb_ids.data_collection_ids[
225
252
  self._oav_snapshot_event_idx
@@ -275,6 +302,9 @@ class GridscanISPyBCallback(BaseISPyBCallback):
275
302
  return scan_data_infos
276
303
 
277
304
  def activity_gated_stop(self, doc: RunStop) -> RunStop:
305
+ assert self.data_collection_group_info, (
306
+ f"No data collection group info - stop document has been emitted before a {PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN} start document"
307
+ )
278
308
  if doc.get("run_start") == self._start_of_fgs_uid:
279
309
  self._processing_start_time = time()
280
310
  if doc.get("run_start") == self.uid_to_finalize_on:
@@ -289,5 +319,11 @@ class GridscanISPyBCallback(BaseISPyBCallback):
289
319
  )
290
320
  if exception_type:
291
321
  doc["reason"] = message
322
+ self.data_collection_group_info.comments = message
323
+ self.ispyb.update_data_collection_group_table(
324
+ self.data_collection_group_info,
325
+ self.ispyb_ids.data_collection_group_id,
326
+ )
327
+ self.data_collection_group_info = None
292
328
  return super().activity_gated_stop(doc)
293
329
  return self._tag_doc(doc)
@@ -79,7 +79,8 @@ class GridscanNexusFileCallback(PlanReactiveCallback):
79
79
  self.descriptors[doc["uid"]] = doc
80
80
 
81
81
  def activity_gated_event(self, doc: Event) -> Event | None:
82
- assert (event_descriptor := self.descriptors.get(doc["descriptor"])) is not None
82
+ event_descriptor = self.descriptors.get(doc["descriptor"])
83
+ assert event_descriptor is not None
83
84
  if event_descriptor.get("name") == DocDescriptorNames.HARDWARE_READ_DURING:
84
85
  data = doc["data"]
85
86
  for nexus_writer in [self.nexus_writer_1, self.nexus_writer_2]:
@@ -13,6 +13,7 @@ class DataCollectionGroupInfo:
13
13
  experiment_type: str
14
14
  sample_id: int | None
15
15
  sample_barcode: str | None = None
16
+ comments: str | None = None
16
17
 
17
18
 
18
19
  @dataclass(kw_only=True)
@@ -77,7 +77,7 @@ class StoreInIspyb:
77
77
  scan_data_infos,
78
78
  ) -> IspybIds:
79
79
  with ispyb.open(self.ISPYB_CONFIG_PATH) as conn:
80
- assert conn is not None, "Failed to connect to ISPyB"
80
+ assert conn, "Failed to connect to ISPyB"
81
81
  if data_collection_group_info:
82
82
  ispyb_ids.data_collection_group_id = (
83
83
  self._store_data_collection_group_table(
@@ -152,6 +152,19 @@ class StoreInIspyb:
152
152
  data_collection_id, comment, delimiter
153
153
  )
154
154
 
155
+ def update_data_collection_group_table(
156
+ self,
157
+ dcg_info: DataCollectionGroupInfo,
158
+ data_collection_group_id: int | None = None,
159
+ ) -> None:
160
+ with ispyb.open(self.ISPYB_CONFIG_PATH) as conn:
161
+ assert conn is not None, "Failed to connect to ISPyB!"
162
+ self._store_data_collection_group_table(
163
+ conn,
164
+ dcg_info,
165
+ data_collection_group_id,
166
+ )
167
+
155
168
  def _update_scan_with_end_time_and_status(
156
169
  self,
157
170
  end_time: str,
@@ -144,7 +144,7 @@ def create_detector_parameters(detector_params: DetectorParams) -> Detector:
144
144
  list(
145
145
  detector_params.get_beam_position_pixels(detector_params.detector_distance)
146
146
  ),
147
- detector_params.exposure_time,
147
+ detector_params.exposure_time_s,
148
148
  [(-1.0, 0.0, 0.0), (0.0, -1.0, 0.0)],
149
149
  )
150
150
 
@@ -58,6 +58,8 @@ class PlanNameConstants:
58
58
  ROTATION_OUTER = "rotation_scan_with_cleanup"
59
59
  ROTATION_MAIN = "rotation_scan_main"
60
60
  FLYSCAN_RESULTS = "xray_centre_results"
61
+ SET_ENERGY = "set_energy"
62
+ UNNAMED_RUN = "unnamed_run"
61
63
 
62
64
 
63
65
  @dataclass(frozen=True)
@@ -168,7 +168,7 @@ class SpecifiedThreeDGridScan(
168
168
  get_beamline_name("dev")
169
169
  ],
170
170
  expected_energy_ev=self.demand_energy_ev,
171
- exposure_time=self.exposure_time_s,
171
+ exposure_time_s=self.exposure_time_s,
172
172
  directory=self.storage_directory,
173
173
  prefix=self.file_name,
174
174
  detector_distance=self.detector_distance_mm,
@@ -0,0 +1,46 @@
1
+ from enum import StrEnum
2
+
3
+ import bluesky.plan_stubs as bps
4
+ import bluesky.preprocessors as bpp
5
+
6
+ from mx_bluesky.common.external_interaction.callbacks.sample_handling.sample_handling_callback import (
7
+ SampleHandlingCallback,
8
+ )
9
+ from mx_bluesky.common.utils.exceptions import SampleException
10
+
11
+
12
+ class SampleStatusExceptionType(StrEnum):
13
+ BEAMLINE = "Beamline"
14
+ SAMPLE = "Sample"
15
+
16
+
17
+ @bpp.subs_decorator(SampleHandlingCallback())
18
+ def deposit_sample_error(exception_type: SampleStatusExceptionType, sample_id: int):
19
+ @bpp.run_decorator(
20
+ md={
21
+ "metadata": {"sample_id": sample_id},
22
+ "activate_callbacks": ["SampleHandlingCallback"],
23
+ }
24
+ )
25
+ def _inner():
26
+ yield from bps.null()
27
+ if exception_type == SampleStatusExceptionType.BEAMLINE:
28
+ raise AssertionError()
29
+ elif exception_type == SampleStatusExceptionType.SAMPLE:
30
+ raise SampleException
31
+
32
+ yield from _inner()
33
+
34
+
35
+ @bpp.subs_decorator(SampleHandlingCallback(record_loaded_on_success=True))
36
+ def deposit_loaded_sample(sample_id: int):
37
+ @bpp.run_decorator(
38
+ md={
39
+ "metadata": {"sample_id": sample_id},
40
+ "activate_callbacks": ["SampleHandlingCallback"],
41
+ }
42
+ )
43
+ def _inner():
44
+ yield from bps.null()
45
+
46
+ yield from _inner()
File without changes
@@ -0,0 +1,105 @@
1
+ from bluesky import preprocessors as bpp
2
+ from bluesky.preprocessors import plan_mutator
3
+ from bluesky.utils import Msg, MsgGenerator, make_decorator
4
+
5
+ from mx_bluesky.common.device_setup_plans.xbpm_feedback import (
6
+ check_and_pause_feedback,
7
+ unpause_xbpm_feedback_and_set_transmission_to_1,
8
+ )
9
+ from mx_bluesky.common.parameters.constants import PlanNameConstants
10
+ from mx_bluesky.common.protocols.protocols import (
11
+ XBPMPauseDevices,
12
+ )
13
+
14
+
15
+ def transmission_and_xbpm_feedback_for_collection_wrapper(
16
+ plan: MsgGenerator,
17
+ devices: XBPMPauseDevices,
18
+ desired_transmission_fraction: float,
19
+ run_key_to_wrap: PlanNameConstants | None = None,
20
+ ):
21
+ """
22
+ Sets the transmission for the data collection, ensuring the xbpm feedback is valid, then resets it immediately after
23
+ the run has finished.
24
+
25
+ This wrapper should be attached to the entry point of any beamline-specific plan that may disrupt the XBPM feedback,
26
+ such as a data collection or an x-ray center grid scan.
27
+ This wrapper will do nothing if no runs are seen.
28
+
29
+ XBPM feedback isn't reliable during collections due to:
30
+ * Objects (e.g. attenuator) crossing the beam can cause large (incorrect) feedback movements
31
+ * Lower transmissions/higher energies are less reliable for the xbpm
32
+
33
+ So we need to keep the transmission at 100% and the feedback on when not collecting
34
+ and then turn it off and set the correct transmission for collection. The feedback
35
+ mostly accounts for slow thermal drift so it is safe to assume that the beam is
36
+ stable during a collection.
37
+
38
+ Args:
39
+ plan: The plan performing the data collection.
40
+ devices (XBPMPauseDevices): Composite device including The XBPM device that is responsible for keeping
41
+ the beam in position, and attenuator
42
+ desired_transmission_fraction (float): The desired transmission for the collection
43
+ run_key_to_wrap: (str | None): Pausing XBPM and setting transmission is inserted after the 'open_run' message is seen with
44
+ the matching run key, and unpausing and resetting transmission is inserted after the corresponding 'close_run' message is
45
+ seen. If not specified, instead wrap the first run encountered.
46
+ """
47
+
48
+ _wrapped_run_name: None | str = None
49
+
50
+ def head(msg: Msg):
51
+ yield from check_and_pause_feedback(
52
+ devices.xbpm_feedback,
53
+ devices.attenuator,
54
+ desired_transmission_fraction,
55
+ )
56
+
57
+ # Allow 'open_run' message to pass through
58
+ yield msg
59
+
60
+ def tail():
61
+ yield from unpause_xbpm_feedback_and_set_transmission_to_1(
62
+ devices.xbpm_feedback, devices.attenuator
63
+ )
64
+
65
+ def insert_plans(msg: Msg):
66
+ # Wrap the specified run, or, if none specified, wrap the first run encountered
67
+ nonlocal _wrapped_run_name
68
+
69
+ match msg.command:
70
+ case "open_run":
71
+ # If we specified a run key, did we encounter it
72
+ # If we didn't specify, then insert the plans and track the name of the run
73
+ if (
74
+ not (run_key_to_wrap or _wrapped_run_name)
75
+ or run_key_to_wrap is msg.run
76
+ ):
77
+ _wrapped_run_name = msg.run if msg.run else "unnamed_run"
78
+ return head(msg), None
79
+ case "close_run":
80
+ # Check if the run tracked from above was closed
81
+ # An exception is raised in the RunEngine if two unnamed runs are opened
82
+ # at the same time, so we are safe from unpausing on the wrong run
83
+ if (_wrapped_run_name == "unnamed_run" and not msg.run) or (
84
+ msg.run and _wrapped_run_name and _wrapped_run_name is msg.run
85
+ ):
86
+ return None, tail()
87
+
88
+ return None, None
89
+
90
+ # Contingency wrapper can cause unpausing to occur on exception and again on close_run.
91
+ # Not needed after https://github.com/bluesky/bluesky/issues/1891
92
+ return (
93
+ yield from bpp.contingency_wrapper(
94
+ plan_mutator(plan, insert_plans),
95
+ except_plan=lambda _: unpause_xbpm_feedback_and_set_transmission_to_1(
96
+ devices.xbpm_feedback,
97
+ devices.attenuator,
98
+ ),
99
+ )
100
+ )
101
+
102
+
103
+ transmission_and_xbpm_feedback_for_collection_decorator = make_decorator(
104
+ transmission_and_xbpm_feedback_for_collection_wrapper
105
+ )
File without changes
@@ -0,0 +1,10 @@
1
+ from typing import Protocol, runtime_checkable
2
+
3
+ from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
4
+ from dodal.devices.xbpm_feedback import XBPMFeedback
5
+
6
+
7
+ @runtime_checkable
8
+ class XBPMPauseDevices(Protocol):
9
+ xbpm_feedback: XBPMFeedback
10
+ attenuator: BinaryFilterAttenuator
@@ -16,9 +16,6 @@ from flask import Flask, request
16
16
  from flask_restful import Api, Resource
17
17
  from pydantic.dataclasses import dataclass
18
18
 
19
- from mx_bluesky.common.external_interaction.callbacks.common.aperture_change_callback import (
20
- ApertureChangeCallback,
21
- )
22
19
  from mx_bluesky.common.external_interaction.callbacks.common.log_uid_tag_callback import (
23
20
  LogUidTaggingCallback,
24
21
  )
@@ -39,6 +36,7 @@ from mx_bluesky.hyperion.experiment_plans.experiment_registry import (
39
36
  PlanNotFound,
40
37
  )
41
38
  from mx_bluesky.hyperion.external_interaction.agamemnon import (
39
+ compare_params,
42
40
  update_params_from_agamemnon,
43
41
  )
44
42
  from mx_bluesky.hyperion.parameters.cli import parse_cli_args
@@ -89,13 +87,11 @@ class BlueskyRunner:
89
87
  self.command_queue: Queue[Command] = Queue()
90
88
  self.current_status: StatusAndMessage = StatusAndMessage(Status.IDLE)
91
89
  self.last_run_aborted: bool = False
92
- self.aperture_change_callback = ApertureChangeCallback()
93
90
  self.logging_uid_tag_callback = LogUidTaggingCallback()
94
91
  self.context: BlueskyContext
95
92
 
96
93
  self.RE = RE
97
94
  self.context = context
98
- RE.subscribe(self.aperture_change_callback)
99
95
  RE.subscribe(self.logging_uid_tag_callback)
100
96
 
101
97
  LOGGER.info("Connecting to external callback ZMQ proxy...")
@@ -175,10 +171,7 @@ class BlueskyRunner:
175
171
  with TRACER.start_span("do_run"):
176
172
  self.RE(command.experiment(command.devices, command.parameters))
177
173
 
178
- self.current_status = StatusAndMessage(
179
- Status.IDLE,
180
- self.aperture_change_callback.last_selected_aperture,
181
- )
174
+ self.current_status = StatusAndMessage(Status.IDLE)
182
175
 
183
176
  self.last_run_aborted = False
184
177
  except WarningException as exception:
@@ -212,6 +205,7 @@ def compose_start_args(context: BlueskyContext, plan_name: str, action: Actions)
212
205
  try:
213
206
  parameters = experiment_internal_param_type(**json.loads(request.data))
214
207
  parameters = update_params_from_agamemnon(parameters)
208
+ compare_params(parameters)
215
209
  if parameters.model_extra:
216
210
  raise ValueError(f"Extra fields not allowed {parameters.model_extra}")
217
211
  except Exception as e:
@@ -0,0 +1,84 @@
1
+ from bluesky import plan_stubs as bps
2
+ from bluesky import preprocessors as bpp
3
+ from dodal.devices.baton import Baton
4
+
5
+ from mx_bluesky.common.utils.exceptions import WarningException
6
+ from mx_bluesky.hyperion.experiment_plans.load_centre_collect_full_plan import (
7
+ LoadCentreCollectComposite,
8
+ load_centre_collect_full,
9
+ )
10
+ from mx_bluesky.hyperion.external_interaction.agamemnon import (
11
+ create_parameters_from_agamemnon,
12
+ )
13
+ from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
14
+
15
+ HYPERION_USER = "Hyperion"
16
+ NO_USER = "None"
17
+
18
+
19
+ def wait_for_hyperion_requested(baton: Baton):
20
+ SLEEP_PER_CHECK = 0.1
21
+ while True:
22
+ requested_user = yield from bps.rd(baton.requested_user)
23
+ if requested_user == HYPERION_USER:
24
+ break
25
+ yield from bps.sleep(SLEEP_PER_CHECK)
26
+
27
+
28
+ def ignore_sample_errors(exception: Exception):
29
+ yield from bps.null()
30
+ # For sample errors we want to continue the loop
31
+ if not isinstance(exception, WarningException):
32
+ raise exception
33
+
34
+
35
+ def main_hyperion_loop(baton: Baton, composite: LoadCentreCollectComposite):
36
+ requested_user = yield from bps.rd(baton.requested_user)
37
+ while requested_user == HYPERION_USER:
38
+
39
+ def inner_loop():
40
+ parameters: LoadCentreCollect | None = create_parameters_from_agamemnon() # type: ignore # not complete until https://github.com/DiamondLightSource/mx-bluesky/issues/773
41
+ if parameters:
42
+ yield from load_centre_collect_full(composite, parameters)
43
+ else:
44
+ yield from bps.mv(baton.requested_user, NO_USER)
45
+
46
+ yield from bpp.contingency_wrapper(
47
+ inner_loop(), except_plan=ignore_sample_errors, auto_raise=False
48
+ )
49
+ requested_user = yield from bps.rd(baton.requested_user)
50
+
51
+
52
+ def move_to_default_state():
53
+ # To be filled in in https://github.com/DiamondLightSource/mx-bluesky/issues/396
54
+ yield from bps.null()
55
+
56
+
57
+ def run_udc_when_requested(baton: Baton, composite: LoadCentreCollectComposite):
58
+ """This will wait for the baton to be handed to hyperion and then run through the
59
+ UDC queue from agamemnon until:
60
+ 1. There are no more instructions from agamemnon
61
+ 2. There is an error on the beamline
62
+ 3. The baton is requested by another party
63
+
64
+ In the case of 1. or 2. hyperion will immediately release the baton. In the case of
65
+ 3. the baton will be released after the next collection has finished."""
66
+
67
+ yield from wait_for_hyperion_requested(baton)
68
+ yield from bps.abs_set(baton.current_user, HYPERION_USER)
69
+
70
+ def default_state_then_collect():
71
+ yield from move_to_default_state()
72
+ yield from main_hyperion_loop(baton, composite)
73
+
74
+ def release_baton():
75
+ # If hyperion has given up the baton itself we need to also release requested
76
+ # user so that hyperion doesn't think we're requested again
77
+ requested_user = yield from bps.rd(baton.requested_user)
78
+ if requested_user == HYPERION_USER:
79
+ yield from bps.abs_set(baton.requested_user, NO_USER)
80
+ yield from bps.abs_set(baton.current_user, NO_USER)
81
+
82
+ yield from bpp.contingency_wrapper(
83
+ default_state_then_collect(), final_plan=release_baton
84
+ )
@@ -1,10 +1,12 @@
1
1
  from datetime import datetime
2
2
  from enum import Enum
3
3
  from pathlib import Path
4
+ from typing import cast
4
5
 
5
6
  import bluesky.plan_stubs as bps
6
7
  from bluesky.utils import MsgGenerator
7
8
  from dodal.common.beamlines.beamline_utils import get_path_provider
9
+ from dodal.common.types import UpdatingPathProvider
8
10
  from dodal.devices.fast_grid_scan import PandAGridScanParams
9
11
  from dodal.devices.smargon import Smargon
10
12
  from ophyd_async.fastcs.panda import (
@@ -222,6 +224,8 @@ def set_panda_directory(panda_directory: Path) -> MsgGenerator:
222
224
  suffix = datetime.now().strftime("_%Y%m%d%H%M%S")
223
225
 
224
226
  async def set_panda_dir():
225
- await get_path_provider().update(directory=panda_directory, suffix=suffix)
227
+ await cast(UpdatingPathProvider, get_path_provider()).update(
228
+ directory=panda_directory, suffix=suffix
229
+ )
226
230
 
227
231
  yield from bps.wait_for([set_panda_dir])
@@ -15,9 +15,6 @@ from mx_bluesky.hyperion.experiment_plans.load_centre_collect_full_plan import (
15
15
  from mx_bluesky.hyperion.experiment_plans.pin_centre_then_xray_centre_plan import (
16
16
  pin_tip_centre_then_xray_centre,
17
17
  )
18
- from mx_bluesky.hyperion.experiment_plans.robot_load_then_centre_plan import (
19
- robot_load_then_centre,
20
- )
21
18
  from mx_bluesky.hyperion.experiment_plans.rotation_scan_plan import (
22
19
  multi_rotation_scan,
23
20
  rotation_scan,
@@ -29,6 +26,5 @@ __all__ = [
29
26
  "rotation_scan",
30
27
  "pin_tip_centre_then_xray_centre",
31
28
  "multi_rotation_scan",
32
- "robot_load_then_centre",
33
29
  "load_centre_collect_full",
34
30
  ]
@@ -1,5 +1,4 @@
1
1
  import bluesky.plan_stubs as bps
2
- import bluesky.preprocessors as bpp
3
2
  import numpy
4
3
  from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue
5
4
  from dodal.devices.smargon import Smargon, StubPosition
@@ -21,17 +20,13 @@ def change_aperture_then_move_to_xtal(
21
20
  * Change the aperture so that the beam size is comparable to the crystal size
22
21
  * Centre on the centre-of-mass
23
22
  * Reset the stub offsets if specified by params"""
24
- if best_hit.bounding_box_mm is not None:
25
- bounding_box_size = numpy.abs(
26
- best_hit.bounding_box_mm[1] - best_hit.bounding_box_mm[0]
27
- )
28
- with TRACER.start_span("change_aperture"):
29
- yield from set_aperture_for_bbox_mm(
30
- aperture_scatterguard,
31
- bounding_box_size,
32
- )
33
- else:
34
- LOGGER.warning("No bounding box size received")
23
+ bounding_box_size = numpy.abs(
24
+ best_hit.bounding_box_mm[1] - best_hit.bounding_box_mm[0]
25
+ )
26
+ yield from set_aperture_for_bbox_mm(
27
+ aperture_scatterguard,
28
+ bounding_box_size,
29
+ )
35
30
 
36
31
  # once we have the results, go to the appropriate position
37
32
  LOGGER.info("Moving to centre of mass.")
@@ -52,12 +47,12 @@ def set_aperture_for_bbox_mm(
52
47
  ):
53
48
  """Sets aperture size based on bbox_size.
54
49
 
55
- This function determines the aperture size needed to accomodate the bounding box
50
+ This function determines the aperture size needed to accommodate the bounding box
56
51
  of a crystal. The x-axis length of the bounding box is used, setting the aperture
57
52
  to Medium if this is less than 50um, and Large otherwise.
58
53
 
59
54
  Args:
60
- aperture_device: The aperture scatter gaurd device we are controlling.
55
+ aperture_device: The aperture scatter guard device we are controlling.
61
56
  bbox_size_mm: The [x,y,z] lengths, in mm, of a bounding box
62
57
  containing a crystal. This describes (in no particular order):
63
58
  * The maximum width a crystal occupies
@@ -77,14 +72,4 @@ def set_aperture_for_bbox_mm(
77
72
  f"Setting aperture to {new_selected_aperture} based on bounding box size {bbox_size_mm}."
78
73
  )
79
74
 
80
- @bpp.set_run_key_decorator("change_aperture")
81
- @bpp.run_decorator(
82
- md={
83
- "subplan_name": "change_aperture",
84
- "aperture_size": new_selected_aperture.value,
85
- }
86
- )
87
- def set_aperture():
88
- yield from bps.abs_set(aperture_device, new_selected_aperture)
89
-
90
- yield from set_aperture()
75
+ yield from bps.abs_set(aperture_device, new_selected_aperture)
@@ -9,7 +9,6 @@ from mx_bluesky.hyperion.experiment_plans import (
9
9
  grid_detect_then_xray_centre_plan,
10
10
  load_centre_collect_full_plan,
11
11
  pin_centre_then_xray_centre_plan,
12
- robot_load_then_centre_plan,
13
12
  )
14
13
  from mx_bluesky.hyperion.parameters.gridscan import (
15
14
  GridScanWithEdgeDetect,
@@ -17,7 +16,6 @@ from mx_bluesky.hyperion.parameters.gridscan import (
17
16
  PinTipCentreThenXrayCentre,
18
17
  )
19
18
  from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
20
- from mx_bluesky.hyperion.parameters.robot_load import RobotLoadThenCentre
21
19
  from mx_bluesky.hyperion.parameters.rotation import MultiRotationScan, RotationScan
22
20
 
23
21
 
@@ -38,7 +36,6 @@ class ExperimentRegistryEntry(TypedDict):
38
36
  | MultiRotationScan
39
37
  | PinTipCentreThenXrayCentre
40
38
  | LoadCentreCollect
41
- | RobotLoadThenCentre
42
39
  ]
43
40
 
44
41
 
@@ -59,10 +56,6 @@ PLAN_REGISTRY: dict[str, ExperimentRegistryEntry] = {
59
56
  "setup": pin_centre_then_xray_centre_plan.create_devices,
60
57
  "param_type": PinTipCentreThenXrayCentre,
61
58
  },
62
- "robot_load_then_centre": {
63
- "setup": robot_load_then_centre_plan.create_devices,
64
- "param_type": RobotLoadThenCentre,
65
- },
66
59
  "multi_rotation_scan": {
67
60
  "setup": rotation_scan_plan.create_devices,
68
61
  "param_type": MultiRotationScan,