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
mx_bluesky/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '1.5.0'
21
- __version_tuple__ = version_tuple = (1, 5, 0)
20
+ __version__ = version = '1.5.1'
21
+ __version_tuple__ = version_tuple = (1, 5, 1)
@@ -23,8 +23,8 @@ from dodal.devices.i24.beamstop import Beamstop
23
23
  from dodal.devices.i24.dcm import DCM
24
24
  from dodal.devices.i24.dual_backlight import DualBacklight
25
25
  from dodal.devices.i24.focus_mirrors import FocusMirrorsMode
26
- from dodal.devices.i24.i24_detector_motion import DetectorMotion
27
26
  from dodal.devices.i24.pilatus_metadata import PilatusMetadata
27
+ from dodal.devices.motors import YZStage
28
28
  from dodal.devices.zebra.zebra import Zebra
29
29
 
30
30
  from mx_bluesky.beamlines.i24.serial.dcid import (
@@ -70,7 +70,7 @@ def flush_print(text):
70
70
 
71
71
  @log_on_entry
72
72
  def initialise_extruder(
73
- detector_stage: DetectorMotion = inject("detector_motion"),
73
+ detector_stage: YZStage = inject("detector_motion"),
74
74
  ) -> MsgGenerator:
75
75
  SSX_LOGGER.info("Initialise Parameters for extruder data collection on I24.")
76
76
 
@@ -98,7 +98,7 @@ def initialise_extruder(
98
98
  def laser_check(
99
99
  mode: str,
100
100
  zebra: Zebra = inject("zebra"),
101
- detector_stage: DetectorMotion = inject("detector_motion"),
101
+ detector_stage: YZStage = inject("detector_motion"),
102
102
  ) -> MsgGenerator:
103
103
  """Plan to open the shutter and check the laser beam from the viewer by pressing \
104
104
  'Laser On' and 'Laser Off' buttons on the edm.
@@ -138,7 +138,7 @@ def laser_check(
138
138
 
139
139
  @log_on_entry
140
140
  def enter_hutch(
141
- detector_stage: DetectorMotion = inject("detector_motion"),
141
+ detector_stage: YZStage = inject("detector_motion"),
142
142
  ) -> MsgGenerator:
143
143
  """Move the detector stage before entering hutch."""
144
144
  yield from bps.mv(detector_stage.z, SAFE_DET_Z)
@@ -146,12 +146,12 @@ def enter_hutch(
146
146
 
147
147
 
148
148
  @log_on_entry
149
- def read_parameters(detector_stage: DetectorMotion, attenuator: ReadOnlyAttenuator):
149
+ def read_parameters(detector_stage: YZStage, attenuator: ReadOnlyAttenuator):
150
150
  """ Read the parameters from user input and create the parameter model for an \
151
151
  extruder collection.
152
152
 
153
153
  Args:
154
- detector_stage (DetectorMotion): The detector stage device.
154
+ detector_stage (YZStage): The detector stage device.
155
155
  attenuator (ReadOnlyAttenuator): A read-only attenuator device to get the \
156
156
  transmission value.
157
157
 
@@ -208,7 +208,7 @@ def main_extruder_plan(
208
208
  aperture: Aperture,
209
209
  backlight: DualBacklight,
210
210
  beamstop: Beamstop,
211
- detector_stage: DetectorMotion,
211
+ detector_stage: YZStage,
212
212
  shutter: HutchShutter,
213
213
  dcm: DCM,
214
214
  mirrors: FocusMirrorsMode,
@@ -500,7 +500,7 @@ def run_extruder_plan(
500
500
  aperture: Aperture = inject("aperture"),
501
501
  backlight: DualBacklight = inject("backlight"),
502
502
  beamstop: Beamstop = inject("beamstop"),
503
- detector_stage: DetectorMotion = inject("detector_motion"),
503
+ detector_stage: YZStage = inject("detector_motion"),
504
504
  shutter: HutchShutter = inject("shutter"),
505
505
  dcm: DCM = inject("dcm"),
506
506
  mirrors: FocusMirrorsMode = inject("focus_mirrors"),
@@ -18,9 +18,9 @@ from dodal.devices.i24.beamstop import Beamstop
18
18
  from dodal.devices.i24.dcm import DCM
19
19
  from dodal.devices.i24.dual_backlight import DualBacklight
20
20
  from dodal.devices.i24.focus_mirrors import FocusMirrorsMode
21
- from dodal.devices.i24.i24_detector_motion import DetectorMotion
22
21
  from dodal.devices.i24.pilatus_metadata import PilatusMetadata
23
22
  from dodal.devices.i24.pmac import PMAC
23
+ from dodal.devices.motors import YZStage
24
24
  from dodal.devices.zebra.zebra import Zebra
25
25
 
26
26
  from mx_bluesky.beamlines.i24.serial.dcid import (
@@ -269,7 +269,7 @@ def start_i24(
269
269
  aperture: Aperture,
270
270
  backlight: DualBacklight,
271
271
  beamstop: Beamstop,
272
- detector_stage: DetectorMotion,
272
+ detector_stage: YZStage,
273
273
  shutter: HutchShutter,
274
274
  parameters: FixedTargetParameters,
275
275
  dcm: DCM,
@@ -504,7 +504,7 @@ def main_fixed_target_plan(
504
504
  aperture: Aperture,
505
505
  backlight: DualBacklight,
506
506
  beamstop: Beamstop,
507
- detector_stage: DetectorMotion,
507
+ detector_stage: YZStage,
508
508
  shutter: HutchShutter,
509
509
  dcm: DCM,
510
510
  mirrors: FocusMirrorsMode,
@@ -655,7 +655,7 @@ def run_fixed_target_plan(
655
655
  aperture: Aperture = inject("aperture"),
656
656
  backlight: DualBacklight = inject("backlight"),
657
657
  beamstop: Beamstop = inject("beamstop"),
658
- detector_stage: DetectorMotion = inject("detector_motion"),
658
+ detector_stage: YZStage = inject("detector_motion"),
659
659
  shutter: HutchShutter = inject("shutter"),
660
660
  dcm: DCM = inject("dcm"),
661
661
  mirrors: FocusMirrorsMode = inject("focus_mirrors"),
@@ -707,7 +707,7 @@ def run_plan_in_wrapper(
707
707
  aperture: Aperture,
708
708
  backlight: DualBacklight,
709
709
  beamstop: Beamstop,
710
- detector_stage: DetectorMotion,
710
+ detector_stage: YZStage,
711
711
  shutter: HutchShutter,
712
712
  dcm: DCM,
713
713
  mirrors: FocusMirrorsMode,
@@ -16,8 +16,8 @@ from dodal.common import inject
16
16
  from dodal.devices.attenuator.attenuator import ReadOnlyAttenuator
17
17
  from dodal.devices.i24.beamstop import Beamstop, BeamstopPositions
18
18
  from dodal.devices.i24.dual_backlight import BacklightPositions, DualBacklight
19
- from dodal.devices.i24.i24_detector_motion import DetectorMotion
20
19
  from dodal.devices.i24.pmac import PMAC, EncReset, LaserSettings
20
+ from dodal.devices.motors import YZStage
21
21
 
22
22
  from mx_bluesky.beamlines.i24.serial.fixed_target.ft_utils import (
23
23
  ChipType,
@@ -121,14 +121,14 @@ def _is_checker_pattern() -> bool:
121
121
 
122
122
  @log_on_entry
123
123
  def read_parameters(
124
- detector_stage: DetectorMotion,
124
+ detector_stage: YZStage,
125
125
  attenuator: ReadOnlyAttenuator,
126
126
  ) -> MsgGenerator:
127
127
  """ Read the parameters from user input and create the parameter model for a fixed \
128
128
  target collection.
129
129
 
130
130
  Args:
131
- detector_stage (DetectorMotion): The detector stage device.
131
+ detector_stage (YZStage): The detector stage device.
132
132
  attenuator (ReadOnlyAttenuator): A read-only attenuator device to get the \
133
133
  transmission value.
134
134
 
@@ -566,7 +566,7 @@ def moveto_preset(
566
566
  pmac: PMAC = inject("pmac"),
567
567
  beamstop: Beamstop = inject("beamstop"),
568
568
  backlight: DualBacklight = inject("backlight"),
569
- det_stage: DetectorMotion = inject("detector_motion"),
569
+ det_stage: YZStage = inject("detector_motion"),
570
570
  ) -> MsgGenerator:
571
571
  # Non Chip Specific Move
572
572
  if place == "zero":
@@ -7,7 +7,7 @@ from dodal.devices.i24.beam_center import DetectorBeamCenter
7
7
  from dodal.devices.i24.beamstop import Beamstop, BeamstopPositions
8
8
  from dodal.devices.i24.dcm import DCM
9
9
  from dodal.devices.i24.dual_backlight import BacklightPositions, DualBacklight
10
- from dodal.devices.i24.i24_detector_motion import DetectorMotion
10
+ from dodal.devices.motors import YZStage
11
11
  from dodal.devices.util.lookup_tables import (
12
12
  linear_interpolation_lut,
13
13
  parse_lookup_table,
@@ -69,7 +69,7 @@ def setup_beamline_for_collection_plan(
69
69
 
70
70
 
71
71
  def move_detector_stage_to_position_plan(
72
- detector_stage: DetectorMotion,
72
+ detector_stage: YZStage,
73
73
  detector_distance: float,
74
74
  ):
75
75
  SSX_LOGGER.debug("Setup beamline: moving detector stage.")
@@ -8,7 +8,7 @@ from enum import IntEnum
8
8
  import bluesky.plan_stubs as bps
9
9
  from bluesky.utils import Msg, MsgGenerator
10
10
  from dodal.common import inject
11
- from dodal.devices.i24.i24_detector_motion import DetectorMotion
11
+ from dodal.devices.motors import YZStage
12
12
 
13
13
  from mx_bluesky.beamlines.i24.serial.log import SSX_LOGGER
14
14
  from mx_bluesky.beamlines.i24.serial.parameters import SSXType
@@ -38,9 +38,9 @@ class UnknownDetectorType(Exception):
38
38
  pass
39
39
 
40
40
 
41
- def get_detector_type(detector_stage: DetectorMotion) -> Generator[Msg, None, Detector]:
41
+ def get_detector_type(detector_stage: YZStage) -> Generator[Msg, None, Detector]:
42
42
  det_y = yield from bps.rd(detector_stage.y)
43
- # DetectorMotion should also be used for this.
43
+ # YZStage should also be used for this.
44
44
  # This should be part of https://github.com/DiamondLightSource/mx_bluesky/issues/51
45
45
  if float(det_y) < Eiger.det_y_threshold:
46
46
  SSX_LOGGER.info("Eiger detector in use.")
@@ -53,7 +53,7 @@ def get_detector_type(detector_stage: DetectorMotion) -> Generator[Msg, None, De
53
53
  raise UnknownDetectorType("Detector not found.")
54
54
 
55
55
 
56
- def _move_detector_stage(detector_stage: DetectorMotion, target: float) -> MsgGenerator:
56
+ def _move_detector_stage(detector_stage: YZStage, target: float) -> MsgGenerator:
57
57
  SSX_LOGGER.info(f"Moving detector stage to target position: {target}.")
58
58
  yield from bps.mv(detector_stage.y, target)
59
59
 
@@ -82,7 +82,7 @@ def _get_requested_detector(det_type_pv: str) -> str:
82
82
 
83
83
 
84
84
  def setup_detector_stage(
85
- expt_type: SSXType, detector_stage: DetectorMotion = inject("detector_motion")
85
+ expt_type: SSXType, detector_stage: YZStage = inject("detector_motion")
86
86
  ) -> MsgGenerator:
87
87
  # Grab the correct PV depending on experiment
88
88
  # Its value is set with MUX on edm screen
@@ -13,9 +13,9 @@ from dodal.devices.i24.beamstop import Beamstop
13
13
  from dodal.devices.i24.dcm import DCM
14
14
  from dodal.devices.i24.dual_backlight import BacklightPositions, DualBacklight
15
15
  from dodal.devices.i24.focus_mirrors import FocusMirrorsMode
16
- from dodal.devices.i24.i24_detector_motion import DetectorMotion
17
16
  from dodal.devices.i24.pilatus_metadata import PilatusMetadata
18
17
  from dodal.devices.i24.pmac import PMAC
18
+ from dodal.devices.motors import YZStage
19
19
  from dodal.devices.oav.oav_detector import OAVBeamCentreFile
20
20
  from dodal.devices.zebra.zebra import Zebra
21
21
 
@@ -104,7 +104,7 @@ def gui_sleep(sec: int) -> MsgGenerator:
104
104
  @bpp.run_decorator()
105
105
  def gui_move_detector(
106
106
  det: Literal["eiger", "pilatus"],
107
- detector_stage: DetectorMotion = inject("detector_motion"),
107
+ detector_stage: YZStage = inject("detector_motion"),
108
108
  ) -> MsgGenerator:
109
109
  det_y_target = Eiger.det_y_target if det == "eiger" else Pilatus.det_y_target
110
110
  yield from _move_detector_stage(detector_stage, det_y_target)
@@ -134,7 +134,7 @@ def gui_run_chip_collection(
134
134
  aperture: Aperture = inject("aperture"),
135
135
  backlight: DualBacklight = inject("backlight"),
136
136
  beamstop: Beamstop = inject("beamstop"),
137
- detector_stage: DetectorMotion = inject("detector_motion"),
137
+ detector_stage: YZStage = inject("detector_motion"),
138
138
  shutter: HutchShutter = inject("shutter"),
139
139
  dcm: DCM = inject("dcm"),
140
140
  mirrors: FocusMirrorsMode = inject("focus_mirrors"),
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ import bluesky.plan_stubs as bps
4
+ import bluesky.preprocessors as bpp
5
+ from bluesky.utils import MsgGenerator
6
+ from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue
7
+ from dodal.devices.motors import XYZStage
8
+ from dodal.devices.robot import BartRobot
9
+ from dodal.devices.smargon import CombinedMove, Smargon, StubPosition
10
+ from dodal.plan_stubs.motor_utils import MoveTooLarge, home_and_reset_wrapper
11
+
12
+ from mx_bluesky.common.utils.log import LOGGER
13
+ from mx_bluesky.hyperion.parameters.constants import CONST
14
+
15
+
16
+ def wait_for_smargon_not_disabled(smargon: Smargon, timeout=60):
17
+ """Waits for the smargon disabled flag to go low. The robot hardware is responsible
18
+ for setting this to low when it is safe to move. It does this through a physical
19
+ connection between the robot and the smargon.
20
+ """
21
+ LOGGER.info("Waiting for smargon enabled")
22
+ SLEEP_PER_CHECK = 0.1
23
+ times_to_check = int(timeout / SLEEP_PER_CHECK)
24
+ for _ in range(times_to_check):
25
+ smargon_disabled = yield from bps.rd(smargon.disabled)
26
+ if not smargon_disabled:
27
+ LOGGER.info("Smargon now enabled")
28
+ return
29
+ yield from bps.sleep(SLEEP_PER_CHECK)
30
+ raise TimeoutError(
31
+ "Timed out waiting for smargon to become enabled after robot load"
32
+ )
33
+
34
+
35
+ def _raise_exception_if_moved_out_of_cryojet(exception):
36
+ yield from bps.null()
37
+ if isinstance(exception, MoveTooLarge):
38
+ raise Exception(
39
+ f"Moving {exception.axis} back to {exception.position} after \
40
+ robot load would move it out of the cryojet. The max safe \
41
+ distance is {exception.maximum_move}"
42
+ )
43
+
44
+
45
+ def do_plan_while_lower_gonio_at_home(plan: MsgGenerator, lower_gonio: XYZStage):
46
+ """Moves the lower gonio to home then performs the provided plan and moves it back.
47
+
48
+ The lower gonio must be in the correct position for the robot load and we
49
+ want to put it back afterwards. Note we don't need to wait for the move as the robot
50
+ is interlocked to the lower gonio and the move is quicker than the robot takes to
51
+ get to the load position.
52
+
53
+ Args:
54
+ plan (MsgGenerator): The plan to run while the lower gonio is at home.
55
+ lower_gonio (XYZStage): The lower gonio to home.
56
+ """
57
+ yield from bpp.contingency_wrapper(
58
+ home_and_reset_wrapper(
59
+ plan,
60
+ lower_gonio,
61
+ BartRobot.LOAD_TOLERANCE_MM,
62
+ CONST.HARDWARE.CRYOJET_MARGIN_MM,
63
+ "lower_gonio",
64
+ wait_for_all=False,
65
+ ),
66
+ except_plan=_raise_exception_if_moved_out_of_cryojet,
67
+ )
68
+ return "reset-lower_gonio"
69
+
70
+
71
+ def prepare_for_robot_load(
72
+ aperture_scatterguard: ApertureScatterguard, smargon: Smargon
73
+ ):
74
+ yield from bps.abs_set(
75
+ aperture_scatterguard.selected_aperture,
76
+ ApertureValue.OUT_OF_BEAM,
77
+ group="prepare_robot_load",
78
+ )
79
+
80
+ yield from bps.mv(smargon.stub_offsets, StubPosition.RESET_TO_ROBOT_LOAD)
81
+
82
+ yield from bps.mv(smargon, CombinedMove(x=0, y=0, z=0, chi=0, phi=0, omega=0))
83
+
84
+ yield from bps.wait("prepare_robot_load")
85
+
86
+
87
+ def robot_unload(
88
+ robot: BartRobot,
89
+ smargon: Smargon,
90
+ aperture_scatterguard: ApertureScatterguard,
91
+ lower_gonio: XYZStage,
92
+ visit: str,
93
+ ):
94
+ """Unloads the currently mounted pin into the location that it was loaded from. The
95
+ loaded location is stored on the robot and so need not be provided.
96
+ """
97
+ yield from prepare_for_robot_load(aperture_scatterguard, smargon)
98
+ sample_id = yield from bps.rd(robot.sample_id)
99
+
100
+ @bpp.run_decorator(
101
+ md={
102
+ "subplan_name": CONST.PLAN.ROBOT_UNLOAD,
103
+ "metadata": {"visit": visit, "sample_id": sample_id},
104
+ "activate_callbacks": [
105
+ "RobotLoadISPyBCallback",
106
+ ],
107
+ },
108
+ )
109
+ def do_robot_unload_and_send_to_ispyb():
110
+ yield from bps.create(name=CONST.DESCRIPTORS.ROBOT_UPDATE)
111
+ yield from bps.read(robot)
112
+ yield from bps.save()
113
+
114
+ def _unload():
115
+ yield from bps.trigger(robot.unload, wait=True)
116
+ yield from wait_for_smargon_not_disabled(smargon)
117
+
118
+ gonio_finished = yield from do_plan_while_lower_gonio_at_home(
119
+ _unload(), lower_gonio
120
+ )
121
+ yield from bps.wait(gonio_finished)
122
+
123
+ yield from do_robot_unload_and_send_to_ispyb()
@@ -288,6 +288,7 @@ def _xrc_result_in_boxes_to_result_in_mm(
288
288
  ),
289
289
  max_count=xrc_result["max_count"],
290
290
  total_count=xrc_result["total_count"],
291
+ sample_id=xrc_result["sample_id"],
291
292
  )
292
293
 
293
294
 
@@ -1,7 +1,9 @@
1
1
  import configparser
2
2
  from dataclasses import dataclass
3
3
  from enum import StrEnum
4
+ from typing import Any, Literal
4
5
 
6
+ from event_model.documents import Event
5
7
  from requests import JSONDecodeError, patch, post
6
8
  from requests.auth import AuthBase
7
9
 
@@ -63,6 +65,19 @@ assert all(len(value) <= 20 for value in BLSampleStatus), (
63
65
  )
64
66
 
65
67
 
68
+ def create_update_data_from_event_doc(
69
+ mapping: dict[str, str], event: Event
70
+ ) -> dict[str, Any]:
71
+ """Given a mapping between bluesky event data and an event itself this function will
72
+ create a dict that can be used to update exp-eye."""
73
+ event_data = event["data"]
74
+ return {
75
+ target_key: event_data[source_key]
76
+ for source_key, target_key in mapping.items()
77
+ if source_key in event_data
78
+ }
79
+
80
+
66
81
  class ExpeyeInteraction:
67
82
  """Exposes functionality from the Expeye core API"""
68
83
 
@@ -74,24 +89,22 @@ class ExpeyeInteraction:
74
89
  self._base_url = url
75
90
  self._auth = BearerAuth(token)
76
91
 
77
- def start_load(
92
+ def start_robot_action(
78
93
  self,
94
+ action_type: Literal["LOAD", "UNLOAD"],
79
95
  proposal_reference: str,
80
96
  visit_number: int,
81
97
  sample_id: int,
82
- dewar_location: int,
83
- container_location: int,
84
98
  ) -> RobotActionID:
85
- """Create a robot load entry in ispyb.
99
+ """Create a robot action entry in ispyb.
86
100
 
87
101
  Args:
102
+ action_type ("LOAD" | "UNLOAD"): The robot action being performed
88
103
  proposal_reference (str): The proposal of the experiment e.g. cm37235
89
104
  visit_number (int): The visit number for the proposal, usually this can be
90
105
  found added to the end of the proposal e.g. the data for
91
106
  visit number 2 of proposal cm37235 is in cm37235-2
92
107
  sample_id (int): The id of the sample in the database
93
- dewar_location (int): Which puck in the dewar the sample is in
94
- container_location (int): Which pin in that puck has the sample
95
108
 
96
109
  Returns:
97
110
  RobotActionID: The id of the robot load action that is created
@@ -102,39 +115,28 @@ class ExpeyeInteraction:
102
115
 
103
116
  data = {
104
117
  "startTimestamp": get_current_time_string(),
118
+ "actionType": action_type,
105
119
  "sampleId": sample_id,
106
- "actionType": "LOAD",
107
- "containerLocation": container_location,
108
- "dewarLocation": dewar_location,
109
120
  }
110
121
  response = _send_and_get_response(self._auth, url, data, post)
111
122
  return response["robotActionId"]
112
123
 
113
- def update_barcode_and_snapshots(
124
+ def update_robot_action(
114
125
  self,
115
126
  action_id: RobotActionID,
116
- barcode: str,
117
- snapshot_before_path: str,
118
- snapshot_after_path: str,
127
+ data: dict[str, Any],
119
128
  ):
120
- """Update the barcode and snapshots of an existing robot action.
129
+ """Update an existing robot action to contain additional info.
121
130
 
122
131
  Args:
123
132
  action_id (RobotActionID): The id of the action to update
124
- barcode (str): The barcode to give the action
125
- snapshot_before_path (str): Path to the snapshot before robot load
126
- snapshot_after_path (str): Path to the snapshot after robot load
133
+ data (dict): The data to update with, where the keys match those expected
134
+ by exp-eye.
127
135
  """
128
136
  url = self._base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id)
129
-
130
- data = {
131
- "sampleBarcode": barcode,
132
- "xtalSnapshotBefore": snapshot_before_path,
133
- "xtalSnapshotAfter": snapshot_after_path,
134
- }
135
137
  _send_and_get_response(self._auth, url, data, patch)
136
138
 
137
- def end_load(self, action_id: RobotActionID, status: str, reason: str):
139
+ def end_robot_action(self, action_id: RobotActionID, status: str, reason: str):
138
140
  """Finish an existing robot action, providing final information about how it went
139
141
 
140
142
  Args:
@@ -41,7 +41,6 @@ class IspybIds(BaseModel):
41
41
  class StoreInIspyb:
42
42
  def __init__(self, ispyb_config: str) -> None:
43
43
  self.ISPYB_CONFIG_PATH: str = ispyb_config
44
- self._data_collection_group_id: int | None
45
44
 
46
45
  def begin_deposition(
47
46
  self,
@@ -20,13 +20,13 @@ from mx_bluesky.common.external_interaction.nexus.nexus_utils import (
20
20
  create_goniometer_axes,
21
21
  get_start_and_predicted_end_time,
22
22
  )
23
- from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample
23
+ from mx_bluesky.common.parameters.components import DiffractionExperiment
24
24
 
25
25
 
26
26
  class NexusWriter:
27
27
  def __init__(
28
28
  self,
29
- parameters: DiffractionExperimentWithSample,
29
+ parameters: DiffractionExperiment,
30
30
  data_shape: tuple[int, int, int],
31
31
  scan_points: AxesPoints,
32
32
  *,
@@ -238,9 +238,14 @@ class TopNByMaxCountSelection(MultiXtalSelection):
238
238
  n: int
239
239
 
240
240
 
241
+ class TopNByMaxCountForEachSampleSelection(MultiXtalSelection):
242
+ name: Literal["TopNByMaxCountForEachSample"] = "TopNByMaxCountForEachSample" # pyright: ignore [reportIncompatibleVariableOverride]
243
+ n: int
244
+
245
+
241
246
  class WithCentreSelection(BaseModel):
242
- select_centres: TopNByMaxCountSelection = Field(
243
- discriminator="name", default=TopNByMaxCountSelection(n=1)
247
+ select_centres: TopNByMaxCountSelection | TopNByMaxCountForEachSampleSelection = (
248
+ Field(discriminator="name", default=TopNByMaxCountSelection(n=1))
244
249
  )
245
250
 
246
251
  @property
@@ -15,8 +15,8 @@ TEST_MODE = BEAMLINE == "test"
15
15
 
16
16
  @dataclass(frozen=True)
17
17
  class DocDescriptorNames:
18
- # Robot load event descriptor
19
- ROBOT_LOAD = "robot_load"
18
+ # Robot load/unload event descriptor
19
+ ROBOT_UPDATE = "robot_update"
20
20
  # For callbacks to use
21
21
  OAV_ROTATION_SNAPSHOT_TRIGGERED = "rotation_snapshot_triggered"
22
22
  OAV_GRID_SNAPSHOT_TRIGGERED = "snapshot_to_ispyb"
@@ -41,8 +41,9 @@ class OavConstants:
41
41
  @dataclass(frozen=True)
42
42
  class PlanNameConstants:
43
43
  LOAD_CENTRE_COLLECT = "load_centre_collect"
44
- # Robot load subplan
44
+ # Robot subplans
45
45
  ROBOT_LOAD = "robot_load"
46
+ ROBOT_UNLOAD = "robot_unload"
46
47
  # Gridscan
47
48
  GRID_DETECT_AND_DO_GRIDSCAN = "grid_detect_and_do_gridscan"
48
49
  GRID_DETECT_INNER = "grid_detect"
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import dataclasses
4
+ from collections import defaultdict
4
5
  from collections.abc import Callable, Sequence
5
6
  from functools import partial
6
7
 
@@ -10,6 +11,7 @@ from event_model import RunStart
10
11
 
11
12
  from mx_bluesky.common.parameters.components import (
12
13
  MultiXtalSelection,
14
+ TopNByMaxCountForEachSampleSelection,
13
15
  TopNByMaxCountSelection,
14
16
  )
15
17
 
@@ -39,18 +41,21 @@ class XRayCentreResult:
39
41
  containing the crystal
40
42
  max_count: The maximum spot count encountered in any one grid box in the crystal
41
43
  total_count: The total count across all boxes in the crystal.
44
+ sample_id: The sample id associated with the centre.
42
45
  """
43
46
 
44
47
  centre_of_mass_mm: np.ndarray
45
48
  bounding_box_mm: tuple[np.ndarray, np.ndarray]
46
49
  max_count: int
47
50
  total_count: int
51
+ sample_id: int | None
48
52
 
49
53
  def __eq__(self, o):
50
54
  return (
51
55
  isinstance(o, XRayCentreResult)
52
56
  and o.max_count == self.max_count
53
57
  and o.total_count == self.total_count
58
+ and o.sample_id == self.sample_id
54
59
  and all(o.centre_of_mass_mm == self.centre_of_mass_mm)
55
60
  and all(o.bounding_box_mm[0] == self.bounding_box_mm[0])
56
61
  and all(o.bounding_box_mm[1] == self.bounding_box_mm[1])
@@ -64,9 +69,27 @@ def top_n_by_max_count(
64
69
  return sorted_hits[:n]
65
70
 
66
71
 
72
+ def top_n_by_max_count_for_each_sample(
73
+ unfiltered: Sequence[XRayCentreResult], n: int
74
+ ) -> Sequence[XRayCentreResult]:
75
+ xrc_results_by_sample_id: dict[int | None, list[XRayCentreResult]] = defaultdict(
76
+ list[XRayCentreResult]
77
+ )
78
+ for result in unfiltered:
79
+ xrc_results_by_sample_id[result.sample_id].append(result)
80
+ return [
81
+ result
82
+ for results in xrc_results_by_sample_id.values()
83
+ for result in sorted(results, key=lambda x: x.max_count, reverse=True)[:n]
84
+ ]
85
+
86
+
67
87
  def resolve_selection_fn(
68
88
  params: MultiXtalSelection,
69
89
  ) -> Callable[[Sequence[XRayCentreResult]], Sequence[XRayCentreResult]]:
70
- if isinstance(params, TopNByMaxCountSelection):
71
- return partial(top_n_by_max_count, n=params.n)
90
+ match params:
91
+ case TopNByMaxCountSelection():
92
+ return partial(top_n_by_max_count, n=params.n)
93
+ case TopNByMaxCountForEachSampleSelection():
94
+ return partial(top_n_by_max_count_for_each_sample, n=params.n)
72
95
  raise ValueError(f"Invalid selection function {params.name}")
@@ -102,7 +102,13 @@ def _generic_tidy(
102
102
 
103
103
  # Turn off dev/shm streaming to avoid filling disk, see https://github.com/DiamondLightSource/hyperion/issues/1395
104
104
  LOGGER.info("Turning off Eiger dev/shm streaming")
105
- yield from bps.abs_set(xrc_composite.eiger.odin.fan.dev_shm_enable, 0) # type: ignore # Fix types in ophyd-async (https://github.com/DiamondLightSource/mx-bluesky/issues/855)
105
+ # Fix types in ophyd-async (https://github.com/DiamondLightSource/mx-bluesky/issues/855)
106
+ yield from bps.abs_set(
107
+ xrc_composite.eiger.odin.fan.dev_shm_enable, # type: ignore
108
+ 0,
109
+ group=group,
110
+ wait=wait,
111
+ )
106
112
 
107
113
 
108
114
  def _panda_tidy(xrc_composite: HyperionFlyScanXRayCentreComposite):