mx-bluesky 1.2.0__py3-none-any.whl → 1.4.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 (105) hide show
  1. mx_bluesky/__init__.py +8 -3
  2. mx_bluesky/__main__.py +12 -7
  3. mx_bluesky/_version.py +2 -2
  4. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +14 -4
  5. mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +178 -0
  6. mx_bluesky/beamlines/i04/thawing_plan.py +49 -11
  7. mx_bluesky/beamlines/i24/serial/__init__.py +3 -0
  8. mx_bluesky/beamlines/i24/serial/dcid.py +143 -171
  9. mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +1 -1
  10. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +121 -110
  11. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +3 -6
  12. mx_bluesky/beamlines/i24/serial/fixed_target/ft_utils.py +0 -1
  13. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +164 -169
  14. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +149 -225
  15. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +7 -216
  16. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +18 -17
  17. mx_bluesky/beamlines/i24/serial/log.py +58 -49
  18. mx_bluesky/beamlines/i24/serial/parameters/__init__.py +4 -0
  19. mx_bluesky/beamlines/i24/serial/parameters/constants.py +6 -1
  20. mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py +42 -15
  21. mx_bluesky/beamlines/i24/serial/parameters/fixed_target/cs/cs_maker.json +3 -3
  22. mx_bluesky/beamlines/i24/serial/run_extruder.sh +30 -5
  23. mx_bluesky/beamlines/i24/serial/run_fixed_target.sh +30 -5
  24. mx_bluesky/beamlines/i24/serial/run_serial.py +24 -8
  25. mx_bluesky/beamlines/i24/serial/setup_beamline/ca.py +0 -2
  26. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +2 -0
  27. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +104 -82
  28. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +9 -20
  29. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +26 -28
  30. mx_bluesky/beamlines/i24/serial/write_nexus.py +74 -72
  31. mx_bluesky/common/__init__.py +0 -0
  32. mx_bluesky/common/device_setup_plans/read_hardware_for_setup.py +14 -0
  33. mx_bluesky/common/external_interaction/config_server.py +46 -0
  34. mx_bluesky/common/parameters/components.py +258 -0
  35. mx_bluesky/common/parameters/constants.py +143 -0
  36. mx_bluesky/common/parameters/gridscan.py +94 -0
  37. mx_bluesky/common/parameters/robot_load.py +16 -0
  38. mx_bluesky/common/plans/__init__.py +1 -0
  39. mx_bluesky/common/plans/do_fgs.py +121 -0
  40. mx_bluesky/common/utils/log.py +118 -0
  41. mx_bluesky/{hyperion → common/utils}/tracing.py +2 -2
  42. mx_bluesky/hyperion/__main__.py +13 -10
  43. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +47 -52
  44. mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py +6 -12
  45. mx_bluesky/hyperion/device_setup_plans/setup_oav.py +6 -12
  46. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +5 -6
  47. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +49 -18
  48. mx_bluesky/hyperion/device_setup_plans/smargon.py +9 -9
  49. mx_bluesky/hyperion/device_setup_plans/utils.py +2 -2
  50. mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +4 -4
  51. mx_bluesky/hyperion/exceptions.py +13 -1
  52. mx_bluesky/hyperion/experiment_plans/__init__.py +4 -0
  53. mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py +83 -0
  54. mx_bluesky/hyperion/experiment_plans/common/xrc_result.py +47 -0
  55. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +9 -9
  56. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +147 -169
  57. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +48 -22
  58. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +75 -9
  59. mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +21 -20
  60. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +9 -6
  61. mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +2 -2
  62. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +40 -21
  63. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +22 -22
  64. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +43 -39
  65. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +69 -18
  66. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +17 -7
  67. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +13 -13
  68. mx_bluesky/hyperion/external_interaction/callbacks/__init__.py +0 -4
  69. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +5 -2
  70. mx_bluesky/hyperion/external_interaction/callbacks/common/abstract_event.py +66 -0
  71. mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py +5 -0
  72. mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py +1 -1
  73. mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py +30 -25
  74. mx_bluesky/hyperion/external_interaction/callbacks/ispyb_callback_base.py +29 -12
  75. mx_bluesky/hyperion/external_interaction/callbacks/log_uid_tag_callback.py +1 -1
  76. mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +19 -11
  77. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +7 -4
  78. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +5 -3
  79. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/__init__.py +0 -0
  80. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/sample_handling_callback.py +84 -0
  81. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +38 -27
  82. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py +5 -4
  83. mx_bluesky/hyperion/external_interaction/config_server.py +11 -28
  84. mx_bluesky/hyperion/external_interaction/exceptions.py +0 -9
  85. mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +65 -15
  86. mx_bluesky/hyperion/external_interaction/ispyb/ispyb_store.py +1 -1
  87. mx_bluesky/hyperion/external_interaction/nexus/nexus_utils.py +2 -2
  88. mx_bluesky/hyperion/external_interaction/nexus/write_nexus.py +1 -1
  89. mx_bluesky/hyperion/log.py +0 -84
  90. mx_bluesky/hyperion/parameters/components.py +4 -251
  91. mx_bluesky/hyperion/parameters/constants.py +22 -119
  92. mx_bluesky/hyperion/parameters/gridscan.py +35 -74
  93. mx_bluesky/hyperion/parameters/load_centre_collect.py +16 -11
  94. mx_bluesky/hyperion/parameters/rotation.py +23 -10
  95. mx_bluesky/hyperion/utils/utils.py +17 -0
  96. mx_bluesky/hyperion/utils/validation.py +5 -6
  97. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/METADATA +36 -33
  98. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/RECORD +102 -89
  99. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/WHEEL +1 -1
  100. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Mapping_py3v1.py +0 -161
  101. mx_bluesky/example.py +0 -19
  102. mx_bluesky/hyperion/parameters/robot_load.py +0 -16
  103. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/LICENSE +0 -0
  104. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/entry_points.txt +0 -0
  105. {mx_bluesky-1.2.0.dist-info → mx_bluesky-1.4.1.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,6 @@ from typing import TypedDict
2
2
 
3
3
  import numpy as np
4
4
  from bluesky.callbacks import CallbackBase
5
- from dodal.devices.oav.oav_detector import OAVConfigParams
6
5
  from dodal.devices.oav.utils import calculate_x_y_z_of_pixel
7
6
  from event_model.documents import Event
8
7
 
@@ -26,21 +25,19 @@ class GridParamUpdate(TypedDict):
26
25
  class GridDetectionCallback(CallbackBase):
27
26
  def __init__(
28
27
  self,
29
- oav_params: OAVConfigParams,
30
28
  *args,
31
29
  ) -> None:
32
30
  super().__init__(*args)
33
- self.oav_params = oav_params
34
- self.start_positions: list = []
31
+ self.start_positions_mm: list = []
35
32
  self.box_numbers: list = []
36
33
 
37
34
  def event(self, doc: Event):
38
35
  data = doc.get("data")
39
- top_left_x_px = data["oav_grid_snapshot_top_left_x"]
40
- box_width_px = data["oav_grid_snapshot_box_width"]
36
+ top_left_x_px = data["oav-grid_snapshot-top_left_x"]
37
+ box_width_px = data["oav-grid_snapshot-box_width"]
41
38
  x_of_centre_of_first_box_px = top_left_x_px + box_width_px / 2
42
39
 
43
- top_left_y_px = data["oav_grid_snapshot_top_left_y"]
40
+ top_left_y_px = data["oav-grid_snapshot-top_left_y"]
44
41
  y_of_centre_of_first_box_px = top_left_y_px + box_width_px / 2
45
42
 
46
43
  smargon_omega = data["smargon-omega"]
@@ -53,36 +50,44 @@ class GridDetectionCallback(CallbackBase):
53
50
  y_of_centre_of_first_box_px,
54
51
  )
55
52
 
56
- position_grid_start = calculate_x_y_z_of_pixel(
57
- current_xyz, smargon_omega, centre_of_first_box, self.oav_params
58
- )
53
+ microns_per_pixel_x = data["oav-microns_per_pixel_x"]
54
+ microns_per_pixel_y = data["oav-microns_per_pixel_y"]
55
+ beam_x = data["oav-beam_centre_i"]
56
+ beam_y = data["oav-beam_centre_j"]
59
57
 
60
- LOGGER.info(f"Calculated start position {position_grid_start}")
58
+ position_grid_start_mm = calculate_x_y_z_of_pixel(
59
+ current_xyz,
60
+ smargon_omega,
61
+ centre_of_first_box,
62
+ (beam_x, beam_y),
63
+ (microns_per_pixel_x, microns_per_pixel_y),
64
+ )
65
+ LOGGER.info(f"Calculated start position {position_grid_start_mm}")
61
66
 
62
- self.start_positions.append(position_grid_start)
67
+ self.start_positions_mm.append(position_grid_start_mm)
63
68
  self.box_numbers.append(
64
69
  (
65
- data["oav_grid_snapshot_num_boxes_x"],
66
- data["oav_grid_snapshot_num_boxes_y"],
70
+ data["oav-grid_snapshot-num_boxes_x"],
71
+ data["oav-grid_snapshot-num_boxes_y"],
67
72
  )
68
73
  )
69
74
 
70
- self.x_step_size_mm = box_width_px * self.oav_params.micronsPerXPixel / 1000
71
- self.y_step_size_mm = box_width_px * self.oav_params.micronsPerYPixel / 1000
72
- self.z_step_size_mm = box_width_px * self.oav_params.micronsPerYPixel / 1000
75
+ self.x_step_size_um = box_width_px * microns_per_pixel_x
76
+ self.y_step_size_um = box_width_px * microns_per_pixel_y
77
+ self.z_step_size_um = box_width_px * microns_per_pixel_y
73
78
  return doc
74
79
 
75
80
  def get_grid_parameters(self) -> GridParamUpdate:
76
81
  return {
77
- "x_start_um": self.start_positions[0][0],
78
- "y_start_um": self.start_positions[0][1],
79
- "y2_start_um": self.start_positions[0][1],
80
- "z_start_um": self.start_positions[1][2],
81
- "z2_start_um": self.start_positions[1][2],
82
+ "x_start_um": self.start_positions_mm[0][0] * 1000,
83
+ "y_start_um": self.start_positions_mm[0][1] * 1000,
84
+ "y2_start_um": self.start_positions_mm[0][1] * 1000,
85
+ "z_start_um": self.start_positions_mm[1][2] * 1000,
86
+ "z2_start_um": self.start_positions_mm[1][2] * 1000,
82
87
  "x_steps": self.box_numbers[0][0],
83
88
  "y_steps": self.box_numbers[0][1],
84
89
  "z_steps": self.box_numbers[1][1],
85
- "x_step_size_um": self.x_step_size_mm,
86
- "y_step_size_um": self.y_step_size_mm,
87
- "z_step_size_um": self.z_step_size_mm,
90
+ "x_step_size_um": self.x_step_size_um,
91
+ "y_step_size_um": self.y_step_size_um,
92
+ "z_step_size_um": self.z_step_size_um,
88
93
  }
@@ -5,9 +5,12 @@ from collections.abc import Callable, Sequence
5
5
  from typing import TYPE_CHECKING, Any, TypeVar, cast
6
6
 
7
7
  from dodal.beamline_specific_utils.i03 import beam_size_from_aperture
8
+ from dodal.devices.detector import DetectorParams
8
9
  from dodal.devices.detector.det_resolution import resolution
9
10
  from dodal.devices.synchrotron import SynchrotronMode
10
11
 
12
+ from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample
13
+ from mx_bluesky.common.utils.log import set_dcgid_tag
11
14
  from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import (
12
15
  PlanReactiveCallback,
13
16
  )
@@ -21,8 +24,7 @@ from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import (
21
24
  StoreInIspyb,
22
25
  )
23
26
  from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_utils import get_ispyb_config
24
- from mx_bluesky.hyperion.log import ISPYB_LOGGER, set_dcgid_tag
25
- from mx_bluesky.hyperion.parameters.components import DiffractionExperimentWithSample
27
+ from mx_bluesky.hyperion.log import ISPYB_LOGGER
26
28
  from mx_bluesky.hyperion.parameters.constants import CONST
27
29
  from mx_bluesky.hyperion.utils.utils import convert_eV_to_angstrom
28
30
 
@@ -33,6 +35,25 @@ if TYPE_CHECKING:
33
35
  from event_model.documents import Event, EventDescriptor, RunStart, RunStop
34
36
 
35
37
 
38
+ def _update_based_on_energy(
39
+ doc: Event,
40
+ detector_params: DetectorParams,
41
+ data_collection_info: DataCollectionInfo,
42
+ ):
43
+ """If energy has been read as part of this reading then add it into the data
44
+ collection info along with the other fields that depend on it."""
45
+ if energy_kev := doc["data"].get("dcm-energy_in_kev", None):
46
+ energy_ev = energy_kev * 1000
47
+ wavelength_angstroms = convert_eV_to_angstrom(energy_ev)
48
+ data_collection_info.wavelength = wavelength_angstroms
49
+ data_collection_info.resolution = resolution(
50
+ detector_params,
51
+ wavelength_angstroms,
52
+ detector_params.detector_distance,
53
+ )
54
+ return data_collection_info
55
+
56
+
36
57
  class BaseISPyBCallback(PlanReactiveCallback):
37
58
  def __init__(
38
59
  self,
@@ -109,6 +130,9 @@ class BaseISPyBCallback(PlanReactiveCallback):
109
130
  slitgap_horizontal=doc["data"]["s4_slit_gaps_xgap"],
110
131
  slitgap_vertical=doc["data"]["s4_slit_gaps_ygap"],
111
132
  )
133
+ hwscan_data_collection_info = _update_based_on_energy(
134
+ doc, self.params.detector_params, hwscan_data_collection_info
135
+ )
112
136
  hwscan_position_info = DataCollectionPositionInfo(
113
137
  pos_x=float(doc["data"]["smargon-x"]),
114
138
  pos_y=float(doc["data"]["smargon-y"]),
@@ -137,16 +161,9 @@ class BaseISPyBCallback(PlanReactiveCallback):
137
161
  if transmission := doc["data"]["attenuator-actual_transmission"]:
138
162
  # Ispyb wants the transmission in a percentage, we use fractions
139
163
  hwscan_data_collection_info.transmission = transmission * 100
140
- event_energy = doc["data"]["dcm-energy_in_kev"]
141
- if event_energy:
142
- energy_ev = event_energy * 1000
143
- wavelength_angstroms = convert_eV_to_angstrom(energy_ev)
144
- hwscan_data_collection_info.wavelength = wavelength_angstroms
145
- hwscan_data_collection_info.resolution = resolution(
146
- self.params.detector_params,
147
- wavelength_angstroms,
148
- self.params.detector_params.detector_distance,
149
- )
164
+ hwscan_data_collection_info = _update_based_on_energy(
165
+ doc, self.params.detector_params, hwscan_data_collection_info
166
+ )
150
167
  scan_data_infos = self.populate_info_for_update(
151
168
  hwscan_data_collection_info, None, self.params
152
169
  )
@@ -1,7 +1,7 @@
1
1
  from bluesky.callbacks import CallbackBase
2
2
  from event_model import RunStart, RunStop
3
3
 
4
- from mx_bluesky.hyperion.log import set_uid_tag
4
+ from mx_bluesky.common.utils.log import set_uid_tag
5
5
 
6
6
 
7
7
  class LogUidTaggingCallback(CallbackBase):
@@ -2,8 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
- from event_model.documents import EventDescriptor
6
-
7
5
  from mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping import (
8
6
  get_proposal_and_session_from_visit_string,
9
7
  )
@@ -11,6 +9,7 @@ from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback i
11
9
  PlanReactiveCallback,
12
10
  )
13
11
  from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import (
12
+ BLSampleStatus,
14
13
  ExpeyeInteraction,
15
14
  RobotActionID,
16
15
  )
@@ -25,6 +24,7 @@ class RobotLoadISPyBCallback(PlanReactiveCallback):
25
24
  def __init__(self) -> None:
26
25
  ISPYB_LOGGER.debug("Initialising ISPyB Robot Load Callback")
27
26
  super().__init__(log=ISPYB_LOGGER)
27
+ self._metadata: dict | None = None
28
28
  self.run_uid: str | None = None
29
29
  self.descriptors: dict[str, EventDescriptor] = {}
30
30
  self.action_id: RobotActionID | None = None
@@ -35,16 +35,17 @@ class RobotLoadISPyBCallback(PlanReactiveCallback):
35
35
  if doc.get("subplan_name") == CONST.PLAN.ROBOT_LOAD:
36
36
  ISPYB_LOGGER.debug(f"ISPyB robot load callback received: {doc}")
37
37
  self.run_uid = doc.get("uid")
38
- assert isinstance(metadata := doc.get("metadata"), dict)
38
+ self._metadata = doc.get("metadata")
39
+ assert isinstance(self._metadata, dict)
39
40
  proposal, session = get_proposal_and_session_from_visit_string(
40
- metadata["visit"]
41
+ self._metadata["visit"]
41
42
  )
42
43
  self.action_id = self.expeye.start_load(
43
44
  proposal,
44
45
  session,
45
- metadata["sample_id"],
46
- metadata["sample_puck"],
47
- metadata["sample_pin"],
46
+ self._metadata["sample_id"],
47
+ self._metadata["sample_puck"],
48
+ self._metadata["sample_pin"],
48
49
  )
49
50
  return super().activity_gated_start(doc)
50
51
 
@@ -62,7 +63,7 @@ class RobotLoadISPyBCallback(PlanReactiveCallback):
62
63
  self.action_id is not None
63
64
  ), "ISPyB Robot load callback event called unexpectedly"
64
65
  barcode = doc["data"]["robot-barcode"]
65
- oav_snapshot = doc["data"]["oav_snapshot_last_saved_path"]
66
+ oav_snapshot = doc["data"]["oav-snapshot-last_saved_path"]
66
67
  webcam_snapshot = doc["data"]["webcam-last_saved_path"]
67
68
  # I03 uses webcam/oav snapshots in place of before/after snapshots
68
69
  self.expeye.update_barcode_and_snapshots(
@@ -77,10 +78,17 @@ class RobotLoadISPyBCallback(PlanReactiveCallback):
77
78
  assert (
78
79
  self.action_id is not None
79
80
  ), "ISPyB Robot load callback stop called unexpectedly"
80
- exit_status = (
81
- doc.get("exit_status") or "Exit status not available in stop document!"
82
- )
81
+ exit_status = doc.get("exit_status")
82
+ assert exit_status, "Exit status not available in stop document!"
83
+ assert self._metadata, "Metadata not received before stop document."
83
84
  reason = doc.get("reason") or "OK"
85
+
84
86
  self.expeye.end_load(self.action_id, exit_status, reason)
87
+ self.expeye.update_sample_status(
88
+ self._metadata["sample_id"],
89
+ BLSampleStatus.LOADED
90
+ if exit_status == "success"
91
+ else BLSampleStatus.ERROR_BEAMLINE,
92
+ )
85
93
  self.action_id = None
86
94
  return super().activity_gated_stop(doc)
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  from collections.abc import Callable, Sequence
4
4
  from typing import TYPE_CHECKING, Any, cast
5
5
 
6
+ from mx_bluesky.common.parameters.components import IspybExperimentType
7
+ from mx_bluesky.common.utils.log import set_dcgid_tag
6
8
  from mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping import (
7
9
  populate_data_collection_group,
8
10
  populate_remaining_data_collection_info,
@@ -22,8 +24,7 @@ from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import (
22
24
  IspybIds,
23
25
  StoreInIspyb,
24
26
  )
25
- from mx_bluesky.hyperion.log import ISPYB_LOGGER, set_dcgid_tag
26
- from mx_bluesky.hyperion.parameters.components import IspybExperimentType
27
+ from mx_bluesky.hyperion.log import ISPYB_LOGGER
27
28
  from mx_bluesky.hyperion.parameters.constants import CONST
28
29
  from mx_bluesky.hyperion.parameters.rotation import RotationScan
29
30
 
@@ -60,7 +61,9 @@ class RotationISPyBCallback(BaseISPyBCallback):
60
61
  ISPYB_LOGGER.info(
61
62
  "ISPyB callback received start document with experiment parameters."
62
63
  )
63
- self.params = RotationScan.from_json(doc.get("hyperion_parameters"))
64
+ hyperion_params = doc.get("hyperion_parameters")
65
+ assert isinstance(hyperion_params, str)
66
+ self.params = RotationScan.model_validate_json(hyperion_params)
64
67
  dcgid = (
65
68
  self.ispyb_ids.data_collection_group_id
66
69
  if (self.params.sample_id == self.last_sample_id)
@@ -157,7 +160,7 @@ class RotationISPyBCallback(BaseISPyBCallback):
157
160
  data_collection_info = DataCollectionInfo(
158
161
  **{
159
162
  f"xtal_snapshot{self._oav_snapshot_event_idx}": data.get(
160
- "oav_snapshot_last_saved_path"
163
+ "oav-snapshot-last_saved_path"
161
164
  )
162
165
  }
163
166
  )
@@ -78,12 +78,14 @@ class RotationNexusFileCallback(PlanReactiveCallback):
78
78
  self.meta_data_run_number = doc.get("meta_data_run_number")
79
79
  if doc.get("subplan_name") == CONST.PLAN.ROTATION_OUTER:
80
80
  self.run_uid = doc.get("uid")
81
- json_params = doc.get("hyperion_parameters")
81
+ hyperion_params = doc.get("hyperion_parameters")
82
+ assert isinstance(hyperion_params, str)
82
83
  NEXUS_LOGGER.info(
83
- f"Nexus writer received start document with experiment parameters {json_params}"
84
+ f"Nexus writer received start document with experiment parameters {hyperion_params}"
84
85
  )
85
- parameters = RotationScan.from_json(json_params)
86
+ parameters = RotationScan.model_validate_json(hyperion_params)
86
87
  NEXUS_LOGGER.info("Setting up nexus file...")
88
+
87
89
  det_size = (
88
90
  parameters.detector_params.detector_size_constants.det_size_pixels
89
91
  )
@@ -0,0 +1,84 @@
1
+ import dataclasses
2
+ from collections.abc import Generator
3
+ from functools import partial
4
+ from typing import Any
5
+
6
+ import bluesky.plan_stubs as bps
7
+ from bluesky.preprocessors import contingency_wrapper
8
+ from bluesky.utils import Msg, make_decorator
9
+ from event_model import Event, EventDescriptor, RunStart
10
+
11
+ from mx_bluesky.hyperion.exceptions import CrystalNotFoundException, SampleException
12
+ from mx_bluesky.hyperion.external_interaction.callbacks.common.abstract_event import (
13
+ AbstractEvent,
14
+ )
15
+ from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import (
16
+ PlanReactiveCallback,
17
+ )
18
+ from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import (
19
+ BLSampleStatus,
20
+ ExpeyeInteraction,
21
+ )
22
+ from mx_bluesky.hyperion.log import ISPYB_LOGGER
23
+ from mx_bluesky.hyperion.parameters.constants import CONST
24
+
25
+ # TODO remove this event-raising shenanigans once
26
+ # https://github.com/bluesky/bluesky/issues/1829 is addressed
27
+
28
+
29
+ @dataclasses.dataclass(frozen=True)
30
+ class _ExceptionEvent(AbstractEvent):
31
+ exception_type: str
32
+
33
+
34
+ def _exception_interceptor(exception: Exception) -> Generator[Msg, Any, Any]:
35
+ yield from bps.create(CONST.DESCRIPTORS.SAMPLE_HANDLING_EXCEPTION)
36
+ yield from bps.read(_ExceptionEvent(type(exception).__name__))
37
+ yield from bps.save()
38
+
39
+
40
+ sample_handling_callback_decorator = make_decorator(
41
+ partial(contingency_wrapper, except_plan=_exception_interceptor)
42
+ )
43
+
44
+
45
+ class SampleHandlingCallback(PlanReactiveCallback):
46
+ """Intercepts exceptions from experiment plans and updates the ISPyB BLSampleStatus
47
+ field according to the type of exception raised."""
48
+
49
+ def __init__(self):
50
+ super().__init__(log=ISPYB_LOGGER)
51
+ self._sample_id: int | None = None
52
+ self._descriptor: str | None = None
53
+
54
+ def activity_gated_start(self, doc: RunStart):
55
+ if not self._sample_id:
56
+ sample_id = doc.get("metadata", {}).get("sample_id")
57
+ self.log.info(f"Recording sample ID at run start {sample_id}")
58
+ self._sample_id = sample_id
59
+
60
+ def activity_gated_descriptor(self, doc: EventDescriptor) -> EventDescriptor | None:
61
+ if doc.get("name") == CONST.DESCRIPTORS.SAMPLE_HANDLING_EXCEPTION:
62
+ self._descriptor = doc["uid"]
63
+ return super().activity_gated_descriptor(doc)
64
+
65
+ def activity_gated_event(self, doc: Event) -> Event | None:
66
+ if doc["descriptor"] == self._descriptor:
67
+ exception_type = doc["data"]["exception_type"]
68
+ self.log.info(
69
+ f"Sample handling callback intercepted exception of type {exception_type}"
70
+ )
71
+ self._record_exception(exception_type)
72
+ return doc
73
+
74
+ def _record_exception(self, exception_type: str):
75
+ expeye = ExpeyeInteraction()
76
+ assert self._sample_id, "Unable to record exception due to no sample ID"
77
+ sample_status = self._decode_sample_status(exception_type)
78
+ expeye.update_sample_status(self._sample_id, sample_status)
79
+
80
+ def _decode_sample_status(self, exception_type: str) -> BLSampleStatus:
81
+ match exception_type:
82
+ case SampleException.__name__ | CrystalNotFoundException.__name__:
83
+ return BLSampleStatus.ERROR_SAMPLE
84
+ return BLSampleStatus.ERROR_BEAMLINE
@@ -5,10 +5,16 @@ from time import time
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
7
  import numpy as np
8
- from blueapi.core import MsgGenerator
9
8
  from bluesky import preprocessors as bpp
10
- from dodal.devices.zocalo.zocalo_results import ZOCALO_READING_PLAN_NAME
9
+ from bluesky.utils import MsgGenerator
10
+ from dodal.devices.zocalo.zocalo_results import (
11
+ ZOCALO_READING_PLAN_NAME,
12
+ get_processing_results_from_event,
13
+ )
11
14
 
15
+ from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample
16
+ from mx_bluesky.common.parameters.constants import PlanNameConstants
17
+ from mx_bluesky.common.utils.log import set_dcgid_tag
12
18
  from mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping import (
13
19
  populate_data_collection_group,
14
20
  populate_remaining_data_collection_info,
@@ -36,8 +42,7 @@ from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import (
36
42
  IspybIds,
37
43
  StoreInIspyb,
38
44
  )
39
- from mx_bluesky.hyperion.log import ISPYB_LOGGER, set_dcgid_tag
40
- from mx_bluesky.hyperion.parameters.components import DiffractionExperimentWithSample
45
+ from mx_bluesky.hyperion.log import ISPYB_LOGGER
41
46
  from mx_bluesky.hyperion.parameters.constants import CONST
42
47
  from mx_bluesky.hyperion.parameters.gridscan import (
43
48
  GridCommon,
@@ -48,13 +53,16 @@ if TYPE_CHECKING:
48
53
 
49
54
 
50
55
  def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters):
51
- return bpp.run_wrapper(
52
- plan_generator,
53
- md={
54
- "activate_callbacks": ["GridscanISPyBCallback"],
55
- "subplan_name": CONST.PLAN.GRID_DETECT_AND_DO_GRIDSCAN,
56
- "hyperion_parameters": parameters.model_dump_json(),
57
- },
56
+ return bpp.set_run_key_wrapper(
57
+ bpp.run_wrapper(
58
+ plan_generator,
59
+ md={
60
+ "activate_callbacks": ["GridscanISPyBCallback"],
61
+ "subplan_name": CONST.PLAN.GRID_DETECT_AND_DO_GRIDSCAN,
62
+ "hyperion_parameters": parameters.model_dump_json(),
63
+ },
64
+ ),
65
+ CONST.PLAN.ISPYB_ACTIVATION,
58
66
  )
59
67
 
60
68
 
@@ -85,7 +93,7 @@ class GridscanISPyBCallback(BaseISPyBCallback):
85
93
  self._processing_start_time: float | None = None
86
94
 
87
95
  def activity_gated_start(self, doc: RunStart):
88
- if doc.get("subplan_name") == CONST.PLAN.DO_FGS:
96
+ if doc.get("subplan_name") == PlanNameConstants.DO_FGS:
89
97
  self._start_of_fgs_uid = doc.get("uid")
90
98
  if doc.get("subplan_name") == CONST.PLAN.GRID_DETECT_AND_DO_GRIDSCAN:
91
99
  self.uid_to_finalize_on = doc.get("uid")
@@ -93,7 +101,9 @@ class GridscanISPyBCallback(BaseISPyBCallback):
93
101
  "ISPyB callback received start document with experiment parameters and "
94
102
  f"uid: {self.uid_to_finalize_on}"
95
103
  )
96
- self.params = GridCommon.from_json(doc.get("hyperion_parameters"))
104
+ hyperion_params = doc.get("hyperion_parameters")
105
+ assert isinstance(hyperion_params, str)
106
+ self.params = GridCommon.model_validate_json(hyperion_params)
97
107
  self.ispyb = StoreInIspyb(self.ispyb_config)
98
108
  data_collection_group_info = populate_data_collection_group(self.params)
99
109
 
@@ -147,7 +157,8 @@ class GridscanISPyBCallback(BaseISPyBCallback):
147
157
  ISPYB_LOGGER.info(
148
158
  f"Amending comment based on Zocalo reading doc: {format_doc_for_log(doc)}"
149
159
  )
150
- raw_results = doc["data"]["zocalo-results"]
160
+
161
+ raw_results = get_processing_results_from_event("zocalo", doc)
151
162
  if len(raw_results) > 0:
152
163
  for n, res in enumerate(raw_results):
153
164
  bb = res["bounding_box"]
@@ -178,25 +189,25 @@ class GridscanISPyBCallback(BaseISPyBCallback):
178
189
  data = doc["data"]
179
190
  data_collection_id = None
180
191
  data_collection_info = DataCollectionInfo(
181
- xtal_snapshot1=data.get("oav_grid_snapshot_last_path_full_overlay"),
182
- xtal_snapshot2=data.get("oav_grid_snapshot_last_path_outer"),
183
- xtal_snapshot3=data.get("oav_grid_snapshot_last_saved_path"),
192
+ xtal_snapshot1=data.get("oav-grid_snapshot-last_path_full_overlay"),
193
+ xtal_snapshot2=data.get("oav-grid_snapshot-last_path_outer"),
194
+ xtal_snapshot3=data.get("oav-grid_snapshot-last_saved_path"),
184
195
  n_images=(
185
- data["oav_grid_snapshot_num_boxes_x"]
186
- * data["oav_grid_snapshot_num_boxes_y"]
196
+ data["oav-grid_snapshot-num_boxes_x"]
197
+ * data["oav-grid_snapshot-num_boxes_y"]
187
198
  ),
188
199
  )
189
- microns_per_pixel_x = data["oav_grid_snapshot_microns_per_pixel_x"]
190
- microns_per_pixel_y = data["oav_grid_snapshot_microns_per_pixel_y"]
200
+ microns_per_pixel_x = data["oav-microns_per_pixel_x"]
201
+ microns_per_pixel_y = data["oav-microns_per_pixel_y"]
191
202
  data_collection_grid_info = DataCollectionGridInfo(
192
- dx_in_mm=data["oav_grid_snapshot_box_width"] * microns_per_pixel_x / 1000,
193
- dy_in_mm=data["oav_grid_snapshot_box_width"] * microns_per_pixel_y / 1000,
194
- steps_x=data["oav_grid_snapshot_num_boxes_x"],
195
- steps_y=data["oav_grid_snapshot_num_boxes_y"],
203
+ dx_in_mm=data["oav-grid_snapshot-box_width"] * microns_per_pixel_x / 1000,
204
+ dy_in_mm=data["oav-grid_snapshot-box_width"] * microns_per_pixel_y / 1000,
205
+ steps_x=data["oav-grid_snapshot-num_boxes_x"],
206
+ steps_y=data["oav-grid_snapshot-num_boxes_y"],
196
207
  microns_per_pixel_x=microns_per_pixel_x,
197
208
  microns_per_pixel_y=microns_per_pixel_y,
198
- snapshot_offset_x_pixel=int(data["oav_grid_snapshot_top_left_x"]),
199
- snapshot_offset_y_pixel=int(data["oav_grid_snapshot_top_left_y"]),
209
+ snapshot_offset_x_pixel=int(data["oav-grid_snapshot-top_left_x"]),
210
+ snapshot_offset_y_pixel=int(data["oav-grid_snapshot-top_left_y"]),
200
211
  orientation=Orientation.HORIZONTAL,
201
212
  snaked=True,
202
213
  )
@@ -12,7 +12,7 @@ from mx_bluesky.hyperion.external_interaction.nexus.nexus_utils import (
12
12
  from mx_bluesky.hyperion.external_interaction.nexus.write_nexus import NexusWriter
13
13
  from mx_bluesky.hyperion.log import NEXUS_LOGGER
14
14
  from mx_bluesky.hyperion.parameters.constants import CONST
15
- from mx_bluesky.hyperion.parameters.gridscan import ThreeDGridScan
15
+ from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from event_model.documents import Event, EventDescriptor, RunStart
@@ -45,11 +45,12 @@ class GridscanNexusFileCallback(PlanReactiveCallback):
45
45
 
46
46
  def activity_gated_start(self, doc: RunStart):
47
47
  if doc.get("subplan_name") == CONST.PLAN.GRIDSCAN_OUTER:
48
- json_params = doc.get("hyperion_parameters")
48
+ hyperion_params = doc.get("hyperion_parameters")
49
+ assert isinstance(hyperion_params, str)
49
50
  NEXUS_LOGGER.info(
50
- f"Nexus writer received start document with experiment parameters {json_params}"
51
+ f"Nexus writer received start document with experiment parameters {hyperion_params}"
51
52
  )
52
- parameters = ThreeDGridScan.from_json(json_params)
53
+ parameters = HyperionThreeDGridScan.model_validate_json(hyperion_params)
53
54
  d_size = parameters.detector_params.detector_size_constants.det_size_pixels
54
55
  grid_n_img_1 = parameters.scan_indices[1]
55
56
  grid_n_img_2 = parameters.num_images - grid_n_img_1
@@ -1,35 +1,18 @@
1
+ from functools import cache
2
+
1
3
  from daq_config_server.client import ConfigServer
2
- from pydantic import BaseModel
3
4
 
5
+ from mx_bluesky.common.external_interaction.config_server import FeatureFlags
4
6
  from mx_bluesky.hyperion.log import LOGGER
5
7
  from mx_bluesky.hyperion.parameters.constants import CONST
6
8
 
7
- _CONFIG_SERVER: ConfigServer | None = None
8
-
9
-
10
- def config_server() -> ConfigServer:
11
- global _CONFIG_SERVER
12
- if _CONFIG_SERVER is None:
13
- _CONFIG_SERVER = ConfigServer(CONST.CONFIG_SERVER_URL, LOGGER)
14
- return _CONFIG_SERVER
15
-
16
-
17
- class FeatureFlags(BaseModel):
18
- # The default value will be used as the fallback when doing a best-effort fetch
19
- # from the service
20
- use_panda_for_gridscan: bool = False
21
- use_gpu_for_gridscan: bool = False
22
- set_stub_offsets: bool = False
23
-
24
- @classmethod
25
- def _get_flags(cls):
26
- flags = config_server().best_effort_get_all_feature_flags()
27
- return {f: flags[f] for f in flags if f in cls.__fields__.keys()}
28
9
 
29
- @classmethod
30
- def best_effort(cls):
31
- return cls(**cls._get_flags())
10
+ class HyperionFeatureFlags(FeatureFlags):
11
+ @staticmethod
12
+ @cache
13
+ def get_config_server() -> ConfigServer:
14
+ return ConfigServer(CONST.CONFIG_SERVER_URL, LOGGER)
32
15
 
33
- def update_self_from_server(self):
34
- for flag, value in self._get_flags().items():
35
- setattr(self, flag, value)
16
+ use_panda_for_gridscan: bool = CONST.I03.USE_PANDA_FOR_GRIDSCAN
17
+ compare_cpu_and_gpu_zocalo: bool = CONST.I03.COMPARE_CPU_AND_GPU_ZOCALO
18
+ set_stub_offsets: bool = CONST.I03.SET_STUB_OFFSETS
@@ -1,13 +1,4 @@
1
- from mx_bluesky.hyperion.exceptions import WarningException
2
-
3
-
4
1
  class ISPyBDepositionNotMade(Exception):
5
2
  """Raised when the ISPyB or Zocalo callbacks can't access ISPyB deposition numbers."""
6
3
 
7
4
  pass
8
-
9
-
10
- class NoCentreFoundException(WarningException):
11
- """Error for if zocalo is unable to find the centre during a gridscan."""
12
-
13
- pass