mx-bluesky 1.5.0__py3-none-any.whl → 1.5.1__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 (37) hide show
  1. mx_bluesky/_version.py +2 -2
  2. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +8 -8
  3. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +5 -5
  4. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +4 -4
  5. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +2 -2
  6. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +5 -5
  7. mx_bluesky/beamlines/i24/serial/web_gui_plans/general_plans.py +3 -3
  8. mx_bluesky/common/device_setup_plans/robot_load_unload.py +123 -0
  9. mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +1 -0
  10. mx_bluesky/common/external_interaction/ispyb/exp_eye_store.py +26 -24
  11. mx_bluesky/common/external_interaction/ispyb/ispyb_store.py +0 -1
  12. mx_bluesky/common/external_interaction/nexus/write_nexus.py +2 -2
  13. mx_bluesky/common/parameters/components.py +7 -2
  14. mx_bluesky/common/parameters/constants.py +4 -3
  15. mx_bluesky/common/xrc_result.py +25 -2
  16. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +7 -1
  17. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +26 -8
  18. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +21 -75
  19. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +2 -2
  20. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +9 -4
  21. mx_bluesky/hyperion/external_interaction/agamemnon.py +4 -4
  22. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +1 -1
  23. mx_bluesky/hyperion/external_interaction/callbacks/{robot_load → robot_actions}/ispyb_callback.py +28 -19
  24. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +1 -1
  25. mx_bluesky/hyperion/external_interaction/callbacks/snapshot_callback.py +3 -0
  26. mx_bluesky/hyperion/external_interaction/config_server.py +0 -11
  27. mx_bluesky/hyperion/parameters/constants.py +1 -5
  28. mx_bluesky/hyperion/parameters/gridscan.py +2 -6
  29. mx_bluesky/hyperion/parameters/load_centre_collect.py +15 -0
  30. mx_bluesky/hyperion/parameters/rotation.py +7 -3
  31. {mx_bluesky-1.5.0.dist-info → mx_bluesky-1.5.1.dist-info}/METADATA +4 -3
  32. {mx_bluesky-1.5.0.dist-info → mx_bluesky-1.5.1.dist-info}/RECORD +36 -36
  33. mx_bluesky/hyperion/utils/validation.py +0 -196
  34. {mx_bluesky-1.5.0.dist-info → mx_bluesky-1.5.1.dist-info}/WHEEL +0 -0
  35. {mx_bluesky-1.5.0.dist-info → mx_bluesky-1.5.1.dist-info}/entry_points.txt +0 -0
  36. {mx_bluesky-1.5.0.dist-info → mx_bluesky-1.5.1.dist-info}/licenses/LICENSE +0 -0
  37. {mx_bluesky-1.5.0.dist-info → mx_bluesky-1.5.1.dist-info}/top_level.txt +0 -0
@@ -76,17 +76,29 @@ def load_centre_collect_full(
76
76
  flyscan_event_handler,
77
77
  )
78
78
 
79
- locations_to_collect_um: list[np.ndarray] = []
79
+ locations_to_collect_um: list[np.ndarray]
80
+ samples_to_collect: list[int]
80
81
 
81
82
  if flyscan_event_handler.xray_centre_results:
82
83
  selection_func = flyscan_result.resolve_selection_fn(
83
84
  parameters.selection_params
84
85
  )
85
86
  hits = selection_func(flyscan_event_handler.xray_centre_results)
86
- locations_to_collect_um = [hit.centre_of_mass_mm * 1000 for hit in hits]
87
+ hits_to_collect = []
88
+ for hit in hits:
89
+ if hit.sample_id is None:
90
+ LOGGER.warning(
91
+ f"Diffracting centre {hit} not collected because no sample id was assigned."
92
+ )
93
+ else:
94
+ hits_to_collect.append(hit)
87
95
 
96
+ locations_to_collect_um = [
97
+ hit.centre_of_mass_mm * 1000 for hit in hits_to_collect
98
+ ]
99
+ samples_to_collect = [hit.sample_id for hit in hits_to_collect]
88
100
  LOGGER.info(
89
- f"Selected hits {hits} using {selection_func}, args={parameters.selection_params}"
101
+ f"Selected hits {hits_to_collect} using {selection_func}, args={parameters.selection_params}"
90
102
  )
91
103
  else:
92
104
  # If the xray centring hasn't found a result but has not thrown an error it
@@ -98,6 +110,7 @@ def load_centre_collect_full(
98
110
  locations_to_collect_um = [
99
111
  np.array([initial_x_mm, initial_y_mm, initial_z_mm]) * 1000
100
112
  ]
113
+ samples_to_collect = [parameters.sample_id]
101
114
 
102
115
  multi_rotation = parameters.multi_rotation_scan
103
116
  rotation_template = multi_rotation.rotation_scans.copy()
@@ -108,9 +121,11 @@ def load_centre_collect_full(
108
121
 
109
122
  generator = rotation_scan_generator(is_alternating)
110
123
  next(generator)
111
- for location in locations_to_collect_um:
124
+ for location, sample_id in zip(
125
+ locations_to_collect_um, samples_to_collect, strict=True
126
+ ):
112
127
  for rot in rotation_template:
113
- combination = generator.send((rot, location))
128
+ combination = generator.send((rot, location, sample_id))
114
129
  multi_rotation.rotation_scans.append(combination)
115
130
  multi_rotation = RotationScan.model_validate(multi_rotation)
116
131
 
@@ -125,8 +140,10 @@ def load_centre_collect_full(
125
140
 
126
141
  def rotation_scan_generator(
127
142
  is_alternating: bool,
128
- ) -> Generator[RotationScanPerSweep, tuple[RotationScanPerSweep, np.ndarray], None]:
129
- scan_template, location = yield # type: ignore
143
+ ) -> Generator[
144
+ RotationScanPerSweep, tuple[RotationScanPerSweep, np.ndarray, int], None
145
+ ]:
146
+ scan_template, location, sample_id = yield # type: ignore
130
147
  next_rotation_direction = scan_template.rotation_direction
131
148
  while True:
132
149
  scan = scan_template.model_copy()
@@ -135,6 +152,7 @@ def rotation_scan_generator(
135
152
  scan.y_start_um,
136
153
  scan.z_start_um,
137
154
  ) = location
155
+ scan.sample_id = sample_id
138
156
  if is_alternating:
139
157
  if next_rotation_direction != scan.rotation_direction:
140
158
  # If originally specified direction of the current scan is different
@@ -146,4 +164,4 @@ def rotation_scan_generator(
146
164
  scan.rotation_direction = next_rotation_direction
147
165
  next_rotation_direction = next_rotation_direction.opposite
148
166
 
149
- scan_template, location = yield scan
167
+ scan_template, location, sample_id = yield scan
@@ -10,22 +10,25 @@ import bluesky.preprocessors as bpp
10
10
  import pydantic
11
11
  from blueapi.core import BlueskyContext
12
12
  from bluesky.utils import Msg
13
- from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue
13
+ from dodal.devices.aperturescatterguard import ApertureScatterguard
14
14
  from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
15
15
  from dodal.devices.backlight import Backlight, BacklightPosition
16
16
  from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, MirrorVoltages
17
17
  from dodal.devices.i03.dcm import DCM
18
18
  from dodal.devices.i03.undulator_dcm import UndulatorDCM
19
- from dodal.devices.motors import XYZPositioner
19
+ from dodal.devices.motors import XYZStage
20
20
  from dodal.devices.oav.oav_detector import OAV
21
21
  from dodal.devices.robot import BartRobot, SampleLocation
22
- from dodal.devices.smargon import CombinedMove, Smargon, StubPosition
22
+ from dodal.devices.smargon import Smargon
23
23
  from dodal.devices.thawer import Thawer
24
24
  from dodal.devices.webcam import Webcam
25
25
  from dodal.devices.xbpm_feedback import XBPMFeedback
26
- from dodal.plan_stubs.motor_utils import MoveTooLarge, home_and_reset_wrapper
27
26
 
28
- from mx_bluesky.common.utils.log import LOGGER
27
+ from mx_bluesky.common.device_setup_plans.robot_load_unload import (
28
+ do_plan_while_lower_gonio_at_home,
29
+ prepare_for_robot_load,
30
+ wait_for_smargon_not_disabled,
31
+ )
29
32
  from mx_bluesky.hyperion.experiment_plans.set_energy_plan import (
30
33
  SetEnergyComposite,
31
34
  set_energy_plan,
@@ -47,7 +50,7 @@ class RobotLoadAndEnergyChangeComposite:
47
50
  # RobotLoad fields
48
51
  robot: BartRobot
49
52
  webcam: Webcam
50
- lower_gonio: XYZPositioner
53
+ lower_gonio: XYZStage
51
54
  thawer: Thawer
52
55
  oav: OAV
53
56
  smargon: Smargon
@@ -61,25 +64,6 @@ def create_devices(context: BlueskyContext) -> RobotLoadAndEnergyChangeComposite
61
64
  return device_composite_from_context(context, RobotLoadAndEnergyChangeComposite)
62
65
 
63
66
 
64
- def wait_for_smargon_not_disabled(smargon: Smargon, timeout=60):
65
- """Waits for the smargon disabled flag to go low. The robot hardware is responsible
66
- for setting this to low when it is safe to move. It does this through a physical
67
- connection between the robot and the smargon.
68
- """
69
- LOGGER.info("Waiting for smargon enabled")
70
- SLEEP_PER_CHECK = 0.1
71
- times_to_check = int(timeout / SLEEP_PER_CHECK)
72
- for _ in range(times_to_check):
73
- smargon_disabled = yield from bps.rd(smargon.disabled)
74
- if not smargon_disabled:
75
- LOGGER.info("Smargon now enabled")
76
- return
77
- yield from bps.sleep(SLEEP_PER_CHECK)
78
- raise TimeoutError(
79
- "Timed out waiting for smargon to become enabled after robot load"
80
- )
81
-
82
-
83
67
  def take_robot_snapshots(oav: OAV, webcam: Webcam, directory: Path):
84
68
  time_now = datetime.now()
85
69
  snapshot_format = f"{time_now.strftime('%H%M%S')}_{{device}}_after_load"
@@ -93,28 +77,15 @@ def take_robot_snapshots(oav: OAV, webcam: Webcam, directory: Path):
93
77
  yield from bps.wait("snapshots")
94
78
 
95
79
 
96
- def prepare_for_robot_load(
97
- aperture_scatterguard: ApertureScatterguard, smargon: Smargon
98
- ):
99
- yield from bps.abs_set(
100
- aperture_scatterguard.selected_aperture,
101
- ApertureValue.OUT_OF_BEAM,
102
- group="prepare_robot_load",
103
- )
104
-
105
- yield from bps.mv(smargon.stub_offsets, StubPosition.RESET_TO_ROBOT_LOAD)
106
-
107
- yield from bps.mv(smargon, CombinedMove(x=0, y=0, z=0, chi=0, phi=0, omega=0))
108
-
109
- yield from bps.wait("prepare_robot_load")
110
-
111
-
112
80
  def do_robot_load(
113
81
  composite: RobotLoadAndEnergyChangeComposite,
114
82
  sample_location: SampleLocation,
83
+ sample_id: int,
115
84
  demand_energy_ev: float | None,
116
85
  thawing_time: float,
117
86
  ):
87
+ yield from bps.abs_set(composite.robot.next_sample_id, sample_id, wait=True)
88
+
118
89
  yield from bps.abs_set(
119
90
  composite.robot,
120
91
  sample_location,
@@ -133,16 +104,6 @@ def do_robot_load(
133
104
  yield from wait_for_smargon_not_disabled(composite.smargon)
134
105
 
135
106
 
136
- def raise_exception_if_moved_out_of_cryojet(exception):
137
- yield from bps.null()
138
- if isinstance(exception, MoveTooLarge):
139
- raise Exception(
140
- f"Moving {exception.axis} back to {exception.position} after \
141
- robot load would move it out of the cryojet. The max safe \
142
- distance is {exception.maximum_move}"
143
- )
144
-
145
-
146
107
  def pin_already_loaded(
147
108
  robot: BartRobot, sample_location: SampleLocation
148
109
  ) -> Generator[Msg, None, bool]:
@@ -158,6 +119,7 @@ def robot_load_and_snapshots(
158
119
  composite: RobotLoadAndEnergyChangeComposite,
159
120
  location: SampleLocation,
160
121
  snapshot_directory: Path,
122
+ sample_id: int,
161
123
  thawing_time: float,
162
124
  demand_energy_ev: float | None,
163
125
  ):
@@ -166,37 +128,25 @@ def robot_load_and_snapshots(
166
128
  robot_load_plan = do_robot_load(
167
129
  composite,
168
130
  location,
131
+ sample_id,
169
132
  demand_energy_ev,
170
133
  thawing_time,
171
134
  )
172
135
 
173
- # The lower gonio must be in the correct position for the robot load and we
174
- # want to put it back afterwards. Note we don't wait the robot is interlocked
175
- # to the lower gonio and the move is quicker than the robot takes to get to the
176
- # load position.
177
- yield from bpp.contingency_wrapper(
178
- home_and_reset_wrapper(
179
- robot_load_plan,
180
- composite.lower_gonio,
181
- BartRobot.LOAD_TOLERANCE_MM,
182
- CONST.HARDWARE.CRYOJET_MARGIN_MM,
183
- "lower_gonio",
184
- wait_for_all=False,
185
- ),
186
- except_plan=raise_exception_if_moved_out_of_cryojet,
136
+ gonio_finished = yield from do_plan_while_lower_gonio_at_home(
137
+ robot_load_plan, composite.lower_gonio
187
138
  )
188
-
189
139
  yield from bps.wait(group="snapshot")
190
140
 
191
141
  yield from take_robot_snapshots(composite.oav, composite.webcam, snapshot_directory)
192
142
 
193
- yield from bps.create(name=CONST.DESCRIPTORS.ROBOT_LOAD)
194
- yield from bps.read(composite.robot.barcode)
143
+ yield from bps.create(name=CONST.DESCRIPTORS.ROBOT_UPDATE)
144
+ yield from bps.read(composite.robot)
195
145
  yield from bps.read(composite.oav.snapshot)
196
146
  yield from bps.read(composite.webcam)
197
147
  yield from bps.save()
198
148
 
199
- yield from bps.wait("reset-lower_gonio")
149
+ yield from bps.wait(gonio_finished)
200
150
 
201
151
 
202
152
  def robot_load_and_change_energy_plan(
@@ -218,17 +168,13 @@ def robot_load_and_change_energy_plan(
218
168
  composite,
219
169
  sample_location,
220
170
  params.snapshot_directory,
171
+ params.sample_id,
221
172
  params.thawing_time,
222
173
  params.demand_energy_ev,
223
174
  ),
224
175
  md={
225
176
  "subplan_name": CONST.PLAN.ROBOT_LOAD,
226
- "metadata": {
227
- "visit": params.visit,
228
- "sample_id": params.sample_id,
229
- "sample_puck": sample_location.puck,
230
- "sample_pin": sample_location.pin,
231
- },
177
+ "metadata": {"visit": params.visit, "sample_id": params.sample_id},
232
178
  "activate_callbacks": [
233
179
  "RobotLoadISPyBCallback",
234
180
  ],
@@ -18,7 +18,7 @@ from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, MirrorVolta
18
18
  from dodal.devices.i03 import Beamstop
19
19
  from dodal.devices.i03.dcm import DCM
20
20
  from dodal.devices.i03.undulator_dcm import UndulatorDCM
21
- from dodal.devices.motors import XYZPositioner
21
+ from dodal.devices.motors import XYZStage
22
22
  from dodal.devices.oav.oav_detector import OAV
23
23
  from dodal.devices.oav.pin_image_recognition import PinTipDetection
24
24
  from dodal.devices.robot import BartRobot, SampleLocation
@@ -96,7 +96,7 @@ class RobotLoadThenCentreComposite:
96
96
  # RobotLoad fields
97
97
  robot: BartRobot
98
98
  webcam: Webcam
99
- lower_gonio: XYZPositioner
99
+ lower_gonio: XYZStage
100
100
  beamstop: Beamstop
101
101
 
102
102
 
@@ -342,10 +342,15 @@ def _move_and_rotation(
342
342
 
343
343
  if params.take_snapshots:
344
344
  yield from bps.wait(CONST.WAIT.MOVE_GONIO_TO_START)
345
- yield from setup_beamline_for_OAV(
346
- composite.smargon, composite.backlight, composite.aperture_scatterguard
347
- )
348
- yield from bps.wait(group=CONST.WAIT.READY_FOR_OAV)
345
+
346
+ if not params.use_grid_snapshots:
347
+ yield from setup_beamline_for_OAV(
348
+ composite.smargon,
349
+ composite.backlight,
350
+ composite.aperture_scatterguard,
351
+ wait=True,
352
+ )
353
+
349
354
  if params.selected_aperture:
350
355
  yield from bps.prepare(
351
356
  composite.aperture_scatterguard,
@@ -148,6 +148,7 @@ def populate_parameters_from_agamemnon(agamemnon_params) -> Sequence[LoadCentreC
148
148
  "name": "TopNByMaxCount",
149
149
  "n": pin_type.expected_number_of_crystals,
150
150
  },
151
+ "features": {"use_gpu_results": True},
151
152
  "robot_load_then_centre": {
152
153
  "storage_directory": str(visit_directory) + "/xraycentring",
153
154
  "file_name": file_name,
@@ -156,7 +157,6 @@ def populate_parameters_from_agamemnon(agamemnon_params) -> Sequence[LoadCentreC
156
157
  "omega_start_deg": 0.0,
157
158
  "chi_start_deg": collection["chi"],
158
159
  "transmission_frac": 1.0,
159
- "features": {"use_gpu_results": True},
160
160
  **with_energy_params,
161
161
  },
162
162
  "multi_rotation_scan": {
@@ -228,10 +228,10 @@ def update_params_from_agamemnon(parameters: T) -> T:
228
228
  parameters.robot_load_then_centre.grid_width_um = pin_type.full_width
229
229
  parameters.select_centres.n = pin_type.expected_number_of_crystals
230
230
  if pin_type != SinglePin():
231
- # Snapshots between each collection take a lot of time.
232
- # Before we do https://github.com/DiamondLightSource/mx-bluesky/issues/226
233
- # this will give no snapshots but that's preferable
231
+ # Rotation snapshots will be generated from the gridscan snapshots,
232
+ # no need to specify snapshot omega.
234
233
  parameters.multi_rotation_scan.snapshot_omegas_deg = []
234
+ parameters.multi_rotation_scan.use_grid_snapshots = True
235
235
  except (ValueError, ValidationError) as e:
236
236
  LOGGER.warning(f"Failed to update parameters: {e}")
237
237
  except Exception as e:
@@ -29,7 +29,7 @@ from mx_bluesky.common.utils.log import (
29
29
  _get_logging_dirs,
30
30
  tag_filter,
31
31
  )
32
- from mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback import (
32
+ from mx_bluesky.hyperion.external_interaction.callbacks.robot_actions.ispyb_callback import (
33
33
  RobotLoadISPyBCallback,
34
34
  )
35
35
  from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_callback import (
@@ -12,6 +12,7 @@ from mx_bluesky.common.external_interaction.ispyb.exp_eye_store import (
12
12
  BLSampleStatus,
13
13
  ExpeyeInteraction,
14
14
  RobotActionID,
15
+ create_update_data_from_event_doc,
15
16
  )
16
17
  from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER
17
18
  from mx_bluesky.hyperion.parameters.constants import CONST
@@ -20,11 +21,21 @@ if TYPE_CHECKING:
20
21
  from event_model.documents import Event, EventDescriptor, RunStart, RunStop
21
22
 
22
23
 
24
+ robot_update_mapping = {
25
+ "robot-barcode": "sampleBarcode",
26
+ "robot-current_pin": "containerLocation",
27
+ "robot-current_puck": "dewarLocation",
28
+ # I03 uses webcam/oav snapshots in place of before/after snapshots
29
+ "webcam-last_saved_path": "xtalSnapshotBefore",
30
+ "oav-snapshot-last_saved_path": "xtalSnapshotAfter",
31
+ }
32
+
33
+
23
34
  class RobotLoadISPyBCallback(PlanReactiveCallback):
24
35
  def __init__(self) -> None:
25
36
  ISPYB_ZOCALO_CALLBACK_LOGGER.debug("Initialising ISPyB Robot Load Callback")
26
37
  super().__init__(log=ISPYB_ZOCALO_CALLBACK_LOGGER)
27
- self._metadata: dict | None = None
38
+ self._sample_id: int | None = None
28
39
 
29
40
  self.run_uid: str | None = None
30
41
  self.descriptors: dict[str, EventDescriptor] = {}
@@ -35,22 +46,24 @@ class RobotLoadISPyBCallback(PlanReactiveCallback):
35
46
  ISPYB_ZOCALO_CALLBACK_LOGGER.debug(
36
47
  "ISPyB robot load callback received start document."
37
48
  )
38
- if doc.get("subplan_name") == CONST.PLAN.ROBOT_LOAD:
49
+ subplan = doc.get("subplan_name")
50
+ if subplan == CONST.PLAN.ROBOT_LOAD or subplan == CONST.PLAN.ROBOT_UNLOAD:
39
51
  ISPYB_ZOCALO_CALLBACK_LOGGER.debug(
40
52
  f"ISPyB robot load callback received: {doc}"
41
53
  )
42
54
  self.run_uid = doc.get("uid")
43
- self._metadata = doc.get("metadata")
44
- assert isinstance(self._metadata, dict)
55
+ metadata = doc.get("metadata")
56
+ assert isinstance(metadata, dict)
57
+ self._sample_id = metadata["sample_id"]
58
+ assert isinstance(self._sample_id, int)
45
59
  proposal, session = get_proposal_and_session_from_visit_string(
46
- self._metadata["visit"]
60
+ metadata["visit"]
47
61
  )
48
- self.action_id = self.expeye.start_load(
62
+ self.action_id = self.expeye.start_robot_action(
63
+ "LOAD" if subplan == CONST.PLAN.ROBOT_LOAD else "UNLOAD",
49
64
  proposal,
50
65
  session,
51
- self._metadata["sample_id"],
52
- self._metadata["sample_puck"],
53
- self._metadata["sample_pin"],
66
+ self._sample_id,
54
67
  )
55
68
  return super().activity_gated_start(doc)
56
69
 
@@ -62,18 +75,14 @@ class RobotLoadISPyBCallback(PlanReactiveCallback):
62
75
  event_descriptor = self.descriptors.get(doc["descriptor"])
63
76
  if (
64
77
  event_descriptor
65
- and event_descriptor.get("name") == CONST.DESCRIPTORS.ROBOT_LOAD
78
+ and event_descriptor.get("name") == CONST.DESCRIPTORS.ROBOT_UPDATE
66
79
  ):
67
80
  assert self.action_id is not None, (
68
81
  "ISPyB Robot load callback event called unexpectedly"
69
82
  )
70
- barcode = doc["data"]["robot-barcode"]
71
- oav_snapshot = doc["data"]["oav-snapshot-last_saved_path"]
72
- webcam_snapshot = doc["data"]["webcam-last_saved_path"]
73
83
  # I03 uses webcam/oav snapshots in place of before/after snapshots
74
- self.expeye.update_barcode_and_snapshots(
75
- self.action_id, barcode, webcam_snapshot, oav_snapshot
76
- )
84
+ update_data = create_update_data_from_event_doc(robot_update_mapping, doc)
85
+ self.expeye.update_robot_action(self.action_id, update_data)
77
86
 
78
87
  return super().activity_gated_event(doc)
79
88
 
@@ -87,12 +96,12 @@ class RobotLoadISPyBCallback(PlanReactiveCallback):
87
96
  )
88
97
  exit_status = doc.get("exit_status")
89
98
  assert exit_status, "Exit status not available in stop document!"
90
- assert self._metadata, "Metadata not received before stop document."
99
+ assert self._sample_id is not None, "Stop called before start"
91
100
  reason = doc.get("reason") or "OK"
92
101
 
93
- self.expeye.end_load(self.action_id, exit_status, reason)
102
+ self.expeye.end_robot_action(self.action_id, exit_status, reason)
94
103
  self.expeye.update_sample_status(
95
- self._metadata["sample_id"],
104
+ self._sample_id,
96
105
  BLSampleStatus.LOADED
97
106
  if exit_status == "success"
98
107
  else BLSampleStatus.ERROR_BEAMLINE,
@@ -54,6 +54,7 @@ class RotationISPyBCallback(BaseISPyBCallback):
54
54
  super().__init__(emit=emit)
55
55
  self.last_sample_id: int | None = None
56
56
  self.ispyb_ids: IspybIds = IspybIds()
57
+ self.ispyb = StoreInIspyb(self.ispyb_config)
57
58
 
58
59
  def activity_gated_start(self, doc: RunStart):
59
60
  if doc.get("subplan_name") == CONST.PLAN.ROTATION_OUTER:
@@ -82,7 +83,6 @@ class RotationISPyBCallback(BaseISPyBCallback):
82
83
  f"Collection is {self.params.ispyb_experiment_type} - storing sampleID to bundle images"
83
84
  )
84
85
  self.last_sample_id = self.params.sample_id
85
- self.ispyb = StoreInIspyb(self.ispyb_config)
86
86
  ISPYB_ZOCALO_CALLBACK_LOGGER.info("Beginning ispyb deposition")
87
87
  data_collection_group_info = populate_data_collection_group(self.params)
88
88
  data_collection_info = populate_data_collection_info_for_rotation(
@@ -1,4 +1,5 @@
1
1
  import dataclasses
2
+ import os
2
3
  import re
3
4
  from collections.abc import Iterator
4
5
  from datetime import datetime
@@ -172,6 +173,8 @@ class BeamDrawingCallback(PlanReactiveCallback):
172
173
  f"Generating snapshot at {current_sample_pos_mm} from base snapshot {snapshot_info}"
173
174
  )
174
175
  output_snapshot_directory = data["oav-snapshot-directory"]
176
+ if not os.path.exists(output_snapshot_directory):
177
+ os.mkdir(output_snapshot_directory)
175
178
  base_file_stem = Path(snapshot_info.snapshot_path).stem
176
179
  output_snapshot_filename = _snapshot_filename(base_file_stem)
177
180
  output_snapshot_path = (
@@ -1,7 +1,6 @@
1
1
  from functools import cache
2
2
 
3
3
  from daq_config_server.client import ConfigServer
4
- from pydantic import model_validator
5
4
 
6
5
  from mx_bluesky.common.external_interaction.config_server import FeatureFlags
7
6
  from mx_bluesky.common.utils.log import LOGGER
@@ -14,8 +13,6 @@ class HyperionFeatureFlags(FeatureFlags):
14
13
 
15
14
  Attributes:
16
15
  use_panda_for_gridscan: If True then the PandA is used for gridscans, otherwise the zebra is used
17
- compare_cpu_and_gpu_zocalo: If True then GPU result processing is enabled
18
- alongside CPU and the results are compared. The CPU result is still take.n
19
16
  use_gpu_results: If True then GPU result processing is enabled
20
17
  and the GPU result is taken.
21
18
  set_stub_offsets: If True then set the stub offsets after moving to the crystal (ignored for
@@ -31,15 +28,7 @@ class HyperionFeatureFlags(FeatureFlags):
31
28
  def get_config_server() -> ConfigServer:
32
29
  return ConfigServer(CONST.CONFIG_SERVER_URL, LOGGER)
33
30
 
34
- @model_validator(mode="after")
35
- def use_gpu_and_compare_cannot_both_be_true(self):
36
- assert not (self.use_gpu_results and self.compare_cpu_and_gpu_zocalo), (
37
- "Cannot both use GPU results and compare them to CPU"
38
- )
39
- return self
40
-
41
31
  use_panda_for_gridscan: bool = CONST.I03.USE_PANDA_FOR_GRIDSCAN
42
- compare_cpu_and_gpu_zocalo: bool = CONST.I03.COMPARE_CPU_AND_GPU_ZOCALO
43
32
  use_gpu_results: bool = CONST.I03.USE_GPU_RESULTS
44
33
  set_stub_offsets: bool = CONST.I03.SET_STUB_OFFSETS
45
34
  omega_flip: bool = CONST.I03.OMEGA_FLIP
@@ -29,12 +29,8 @@ class I03Constants:
29
29
  OMEGA_FLIP = True
30
30
  ALTERNATE_ROTATION_DIRECTION = True
31
31
 
32
- # Turns on GPU processing for zocalo and logs a comparison between GPU and CPU-
33
- # processed results.
34
- COMPARE_CPU_AND_GPU_ZOCALO = False
35
-
36
32
  # Turns on GPU processing for zocalo and uses the results that come back
37
- USE_GPU_RESULTS = False
33
+ USE_GPU_RESULTS = True
38
34
 
39
35
 
40
36
  @dataclass(frozen=True)
@@ -20,9 +20,7 @@ class GridCommonWithHyperionDetectorParams(GridCommon, WithHyperionUDCFeatures):
20
20
  @property
21
21
  def detector_params(self):
22
22
  params = super().detector_params
23
- params.enable_dev_shm = (
24
- self.features.compare_cpu_and_gpu_zocalo or self.features.use_gpu_results
25
- )
23
+ params.enable_dev_shm = self.features.use_gpu_results
26
24
  return params
27
25
 
28
26
 
@@ -35,9 +33,7 @@ class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGr
35
33
  @property
36
34
  def detector_params(self):
37
35
  params = super().detector_params
38
- params.enable_dev_shm = (
39
- self.features.compare_cpu_and_gpu_zocalo or self.features.use_gpu_results
40
- )
36
+ params.enable_dev_shm = self.features.use_gpu_results
41
37
  return params
42
38
 
43
39
  # Relative to common grid scan, stub offsets are defined by config server
@@ -50,6 +50,12 @@ class LoadCentreCollect(
50
50
  f"Unexpected fields found in LoadCentreCollect {disallowed_keys}"
51
51
  )
52
52
 
53
+ assert "features" not in values["robot_load_then_centre"], (
54
+ "Features flags must be specified at top-level in LoadCentreCollect"
55
+ )
56
+ assert "features" not in values["multi_rotation_scan"], (
57
+ "Features flags must be specified at top-level in LoadCentreCollect"
58
+ )
53
59
  keys_from_outer_load_centre_collect = (
54
60
  MxBlueskyParameters.model_fields.keys()
55
61
  | WithSample.model_fields.keys()
@@ -70,6 +76,9 @@ class LoadCentreCollect(
70
76
  f"Unexpected keys in multi_rotation_scan: {', '.join(duplicated_multi_rotation_scan_keys)}"
71
77
  )
72
78
 
79
+ for rotation in values["multi_rotation_scan"]["rotation_scans"]:
80
+ rotation["sample_id"] = values["sample_id"]
81
+
73
82
  new_robot_load_then_centre_params = construct_from_values(
74
83
  values, values["robot_load_then_centre"], RobotLoadThenCentre
75
84
  )
@@ -80,6 +89,12 @@ class LoadCentreCollect(
80
89
  values["robot_load_then_centre"] = new_robot_load_then_centre_params
81
90
  return values
82
91
 
92
+ @model_validator(mode="after")
93
+ def _ensure_features_are_internally_consistent(self) -> Self:
94
+ self.robot_load_then_centre.features = self.features
95
+ self.multi_rotation_scan.features = self.features
96
+ return self
97
+
83
98
  @model_validator(mode="after")
84
99
  def _check_rotation_start_xyz_is_not_specified(self) -> Self:
85
100
  for scan in self.multi_rotation_scan.single_rotation_scans:
@@ -18,12 +18,14 @@ from scanspec.core import Path as ScanPath
18
18
  from scanspec.specs import Line
19
19
 
20
20
  from mx_bluesky.common.parameters.components import (
21
+ DiffractionExperiment,
21
22
  DiffractionExperimentWithSample,
22
23
  IspybExperimentType,
23
24
  OptionalGonioAngleStarts,
24
25
  OptionalXyzStarts,
25
26
  RotationAxis,
26
27
  SplitScan,
28
+ WithSample,
27
29
  WithScan,
28
30
  )
29
31
  from mx_bluesky.hyperion.parameters.components import WithHyperionUDCFeatures
@@ -33,7 +35,7 @@ from mx_bluesky.hyperion.parameters.constants import (
33
35
  )
34
36
 
35
37
 
36
- class RotationScanPerSweep(OptionalGonioAngleStarts, OptionalXyzStarts):
38
+ class RotationScanPerSweep(OptionalGonioAngleStarts, OptionalXyzStarts, WithSample):
37
39
  """
38
40
  Describes a rotation scan about the specified axis.
39
41
 
@@ -54,7 +56,7 @@ class RotationScanPerSweep(OptionalGonioAngleStarts, OptionalXyzStarts):
54
56
  nexus_vds_start_img: int = Field(default=0, ge=0)
55
57
 
56
58
 
57
- class RotationExperiment(DiffractionExperimentWithSample, WithHyperionUDCFeatures):
59
+ class RotationExperiment(DiffractionExperiment, WithHyperionUDCFeatures):
58
60
  shutter_opening_time_s: float = Field(default=CONST.I03.SHUTTER_TIME_S)
59
61
  rotation_increment_deg: float = Field(default=0.1, gt=0)
60
62
  ispyb_experiment_type: IspybExperimentType = Field(
@@ -105,7 +107,9 @@ class RotationExperiment(DiffractionExperimentWithSample, WithHyperionUDCFeature
105
107
  return aperture_position
106
108
 
107
109
 
108
- class SingleRotationScan(WithScan, RotationScanPerSweep, RotationExperiment):
110
+ class SingleRotationScan(
111
+ WithScan, RotationScanPerSweep, RotationExperiment, DiffractionExperimentWithSample
112
+ ):
109
113
  @property
110
114
  def detector_params(self):
111
115
  return self._detector_params(self.omega_start_deg)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mx-bluesky
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: Bluesky tools for MX Beamlines at DLS
5
5
  Author-email: Dominic Oram <dominic.oram@diamond.ac.uk>
6
6
  License: Apache License
@@ -221,7 +221,7 @@ Requires-Dist: ispyb
221
221
  Requires-Dist: jupyterlab
222
222
  Requires-Dist: matplotlib
223
223
  Requires-Dist: nexgen>=0.11.0
224
- Requires-Dist: numpy
224
+ Requires-Dist: numpy==2.2.6
225
225
  Requires-Dist: opencv-python
226
226
  Requires-Dist: opentelemetry-distro
227
227
  Requires-Dist: opentelemetry-exporter-otlp
@@ -233,13 +233,14 @@ Requires-Dist: requests
233
233
  Requires-Dist: scanspec
234
234
  Requires-Dist: scipy
235
235
  Requires-Dist: semver
236
+ Requires-Dist: deepdiff
236
237
  Requires-Dist: matplotlib
237
238
  Requires-Dist: blueapi>=0.11.1
238
239
  Requires-Dist: daq-config-server==0.1.1
239
240
  Requires-Dist: ophyd>=1.10.5
240
241
  Requires-Dist: ophyd-async>=0.10.0a2
241
242
  Requires-Dist: bluesky>=1.13.1
242
- Requires-Dist: dls-dodal==1.49.0
243
+ Requires-Dist: dls-dodal==1.51.0
243
244
  Provides-Extra: dev
244
245
  Requires-Dist: black; extra == "dev"
245
246
  Requires-Dist: build; extra == "dev"