mx-bluesky 0.3.1__py3-none-any.whl → 1.1.0__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 (138) hide show
  1. mx_bluesky/_version.py +2 -2
  2. mx_bluesky/beamlines/i04/__init__.py +3 -0
  3. mx_bluesky/{i04 → beamlines/i04}/thawing_plan.py +5 -4
  4. mx_bluesky/{i24 → beamlines/i24}/serial/blueapi_config.yaml +1 -1
  5. mx_bluesky/{i24 → beamlines/i24}/serial/dcid.py +2 -2
  6. mx_bluesky/{i24 → beamlines/i24}/serial/extruder/EX-gui-edm/DetStage.edl +3 -3
  7. mx_bluesky/{i24 → beamlines/i24}/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +7 -7
  8. mx_bluesky/{i24 → beamlines/i24}/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +12 -9
  9. mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/CustomChip_py3v1.edl +3 -3
  10. mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/DetStage.edl +3 -3
  11. mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +245 -200
  12. mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/MappingLite-oxford_py3v1.edl +4 -4
  13. mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/pumpprobe-py3v1.edl +8 -8
  14. mx_bluesky/beamlines/i24/serial/fixed_target/__init__.py +0 -0
  15. mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +80 -70
  16. mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +20 -21
  17. mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/i24ssx_Chip_Mapping_py3v1.py +5 -5
  18. mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +7 -4
  19. mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/i24ssx_moveonclick.py +59 -39
  20. mx_bluesky/{i24 → beamlines/i24}/serial/log.py +1 -9
  21. mx_bluesky/beamlines/i24/serial/parameters/__init__.py +15 -0
  22. mx_bluesky/{i24 → beamlines/i24}/serial/parameters/constants.py +1 -1
  23. mx_bluesky/{i24 → beamlines/i24}/serial/parameters/experiment_parameters.py +4 -25
  24. mx_bluesky/{i24 → beamlines/i24}/serial/parameters/utils.py +5 -3
  25. mx_bluesky/{i24 → beamlines/i24}/serial/run_serial.py +1 -1
  26. mx_bluesky/{i24 → beamlines/i24}/serial/setup_beamline/pv_abstract.py +1 -1
  27. mx_bluesky/{i24 → beamlines/i24}/serial/setup_beamline/setup_beamline.py +2 -2
  28. mx_bluesky/{i24 → beamlines/i24}/serial/setup_beamline/setup_detector.py +5 -5
  29. mx_bluesky/{i24 → beamlines/i24}/serial/write_nexus.py +6 -3
  30. mx_bluesky/hyperion/__init__.py +1 -0
  31. mx_bluesky/hyperion/__main__.py +374 -0
  32. mx_bluesky/hyperion/device_setup_plans/__init__.py +0 -0
  33. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +134 -0
  34. mx_bluesky/hyperion/device_setup_plans/manipulate_sample.py +110 -0
  35. mx_bluesky/hyperion/device_setup_plans/position_detector.py +16 -0
  36. mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py +60 -0
  37. mx_bluesky/hyperion/device_setup_plans/setup_oav.py +87 -0
  38. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +210 -0
  39. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +214 -0
  40. mx_bluesky/hyperion/device_setup_plans/smargon.py +25 -0
  41. mx_bluesky/hyperion/device_setup_plans/utils.py +44 -0
  42. mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +93 -0
  43. mx_bluesky/hyperion/exceptions.py +47 -0
  44. mx_bluesky/hyperion/experiment_plans/__init__.py +30 -0
  45. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +84 -0
  46. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +528 -0
  47. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +209 -0
  48. mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +173 -0
  49. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +81 -0
  50. mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +463 -0
  51. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +119 -0
  52. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +164 -0
  53. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +322 -0
  54. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +436 -0
  55. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +68 -0
  56. mx_bluesky/hyperion/external_interaction/__init__.py +9 -0
  57. mx_bluesky/hyperion/external_interaction/callbacks/__init__.py +10 -0
  58. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +148 -0
  59. mx_bluesky/hyperion/external_interaction/callbacks/aperture_change_callback.py +22 -0
  60. mx_bluesky/hyperion/external_interaction/callbacks/common/__init__.py +0 -0
  61. mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py +46 -0
  62. mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py +70 -0
  63. mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py +88 -0
  64. mx_bluesky/hyperion/external_interaction/callbacks/ispyb_callback_base.py +203 -0
  65. mx_bluesky/hyperion/external_interaction/callbacks/log_uid_tag_callback.py +20 -0
  66. mx_bluesky/hyperion/external_interaction/callbacks/logging_callback.py +29 -0
  67. mx_bluesky/hyperion/external_interaction/callbacks/plan_reactive_callback.py +101 -0
  68. mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +88 -0
  69. mx_bluesky/hyperion/external_interaction/callbacks/rotation/__init__.py +0 -0
  70. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +174 -0
  71. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +17 -0
  72. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +102 -0
  73. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/__init__.py +0 -0
  74. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +269 -0
  75. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_mapping.py +53 -0
  76. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py +95 -0
  77. mx_bluesky/hyperion/external_interaction/callbacks/zocalo_callback.py +92 -0
  78. mx_bluesky/hyperion/external_interaction/config_server.py +35 -0
  79. mx_bluesky/hyperion/external_interaction/exceptions.py +13 -0
  80. mx_bluesky/hyperion/external_interaction/ispyb/__init__.py +0 -0
  81. mx_bluesky/hyperion/external_interaction/ispyb/data_model.py +95 -0
  82. mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +125 -0
  83. mx_bluesky/hyperion/external_interaction/ispyb/ispyb_store.py +276 -0
  84. mx_bluesky/hyperion/external_interaction/ispyb/ispyb_utils.py +29 -0
  85. mx_bluesky/hyperion/external_interaction/nexus/__init__.py +0 -0
  86. mx_bluesky/hyperion/external_interaction/nexus/nexus_utils.py +148 -0
  87. mx_bluesky/hyperion/external_interaction/nexus/write_nexus.py +114 -0
  88. mx_bluesky/hyperion/log.py +99 -0
  89. mx_bluesky/hyperion/parameters/__init__.py +2 -0
  90. mx_bluesky/hyperion/parameters/cli.py +68 -0
  91. mx_bluesky/{parameters → hyperion/parameters}/components.py +77 -24
  92. mx_bluesky/hyperion/parameters/constants.py +158 -0
  93. mx_bluesky/hyperion/parameters/gridscan.py +216 -0
  94. mx_bluesky/hyperion/parameters/rotation.py +160 -0
  95. mx_bluesky/hyperion/resources/panda/panda-gridscan.yaml +964 -0
  96. mx_bluesky/hyperion/tracing.py +28 -0
  97. mx_bluesky/hyperion/utils/context.py +84 -0
  98. mx_bluesky/hyperion/utils/utils.py +25 -0
  99. mx_bluesky/hyperion/utils/validation.py +196 -0
  100. mx_bluesky/jupyter_example.ipynb +3 -2
  101. {mx_bluesky-0.3.1.dist-info → mx_bluesky-1.1.0.dist-info}/METADATA +26 -11
  102. mx_bluesky-1.1.0.dist-info/RECORD +136 -0
  103. {mx_bluesky-0.3.1.dist-info → mx_bluesky-1.1.0.dist-info}/WHEEL +1 -1
  104. mx_bluesky-1.1.0.dist-info/entry_points.txt +8 -0
  105. mx_bluesky/i04/__init__.py +0 -3
  106. mx_bluesky/i24/serial/parameters/__init__.py +0 -15
  107. mx_bluesky/parameters/__init__.py +0 -31
  108. mx_bluesky-0.3.1.dist-info/RECORD +0 -67
  109. mx_bluesky-0.3.1.dist-info/entry_points.txt +0 -4
  110. /mx_bluesky/{i24 → beamlines}/__init__.py +0 -0
  111. /mx_bluesky/{i04 → beamlines/i04}/callbacks/murko_callback.py +0 -0
  112. /mx_bluesky/{i24/serial/extruder → beamlines/i24}/__init__.py +0 -0
  113. /mx_bluesky/{i24 → beamlines/i24}/serial/__init__.py +0 -0
  114. /mx_bluesky/{i24 → beamlines/i24}/serial/extruder/EX-gui-edm/microdrop_alignment.edl +0 -0
  115. /mx_bluesky/{i24/serial/fixed_target → beamlines/i24/serial/extruder}/__init__.py +0 -0
  116. /mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/ME14E-GeneralPurpose.edl +0 -0
  117. /mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/PMAC_Command.edl +0 -0
  118. /mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/Shutter_Control.edl +0 -0
  119. /mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/microdrop_alignment.edl +0 -0
  120. /mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/nudgechip.edl +0 -0
  121. /mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/short1-laser.png +0 -0
  122. /mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/short2-laser.png +0 -0
  123. /mx_bluesky/{i24 → beamlines/i24}/serial/fixed_target/ft_utils.py +0 -0
  124. /mx_bluesky/{i24 → beamlines/i24}/serial/parameters/fixed_target/cs/cs_maker.json +0 -0
  125. /mx_bluesky/{i24 → beamlines/i24}/serial/parameters/fixed_target/cs/motor_direction.txt +0 -0
  126. /mx_bluesky/{i24 → beamlines/i24}/serial/parameters/fixed_target/pvar_files/minichip-oxford.pvar +0 -0
  127. /mx_bluesky/{i24 → beamlines/i24}/serial/parameters/fixed_target/pvar_files/oxford.pvar +0 -0
  128. /mx_bluesky/{i24 → beamlines/i24}/serial/run_extruder.sh +0 -0
  129. /mx_bluesky/{i24 → beamlines/i24}/serial/run_fixed_target.sh +0 -0
  130. /mx_bluesky/{i24 → beamlines/i24}/serial/run_ssx.sh +0 -0
  131. /mx_bluesky/{i24 → beamlines/i24}/serial/set_visit_directory.sh +0 -0
  132. /mx_bluesky/{i24 → beamlines/i24}/serial/setup_beamline/__init__.py +0 -0
  133. /mx_bluesky/{i24 → beamlines/i24}/serial/setup_beamline/ca.py +0 -0
  134. /mx_bluesky/{i24 → beamlines/i24}/serial/setup_beamline/pv.py +0 -0
  135. /mx_bluesky/{i24 → beamlines/i24}/serial/setup_beamline/setup_zebra_plans.py +0 -0
  136. /mx_bluesky/{i24 → beamlines/i24}/serial/start_blueapi.sh +0 -0
  137. {mx_bluesky-0.3.1.dist-info → mx_bluesky-1.1.0.dist-info}/LICENSE +0 -0
  138. {mx_bluesky-0.3.1.dist-info → mx_bluesky-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,46 @@
1
+ from collections.abc import Callable
2
+
3
+ from bluesky.callbacks import CallbackBase
4
+
5
+ from mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback import (
6
+ RobotLoadISPyBCallback,
7
+ )
8
+ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_callback import (
9
+ RotationISPyBCallback,
10
+ )
11
+ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.nexus_callback import (
12
+ RotationNexusFileCallback,
13
+ )
14
+ from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import (
15
+ GridscanISPyBCallback,
16
+ )
17
+ from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.nexus_callback import (
18
+ GridscanNexusFileCallback,
19
+ )
20
+ from mx_bluesky.hyperion.external_interaction.callbacks.zocalo_callback import (
21
+ ZocaloCallback,
22
+ )
23
+
24
+ CallbacksFactory = Callable[[], tuple[CallbackBase, ...]]
25
+
26
+
27
+ def create_robot_load_and_centre_callbacks() -> (
28
+ tuple[GridscanNexusFileCallback, GridscanISPyBCallback, RobotLoadISPyBCallback]
29
+ ):
30
+ return (
31
+ GridscanNexusFileCallback(),
32
+ GridscanISPyBCallback(emit=ZocaloCallback()),
33
+ RobotLoadISPyBCallback(),
34
+ )
35
+
36
+
37
+ def create_gridscan_callbacks() -> (
38
+ tuple[GridscanNexusFileCallback, GridscanISPyBCallback]
39
+ ):
40
+ return (GridscanNexusFileCallback(), GridscanISPyBCallback(emit=ZocaloCallback()))
41
+
42
+
43
+ def create_rotation_callbacks() -> (
44
+ tuple[RotationNexusFileCallback, RotationISPyBCallback]
45
+ ):
46
+ return (RotationNexusFileCallback(), RotationISPyBCallback(emit=ZocaloCallback()))
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from mx_bluesky.hyperion.external_interaction.ispyb.data_model import (
6
+ DataCollectionGroupInfo,
7
+ DataCollectionInfo,
8
+ )
9
+ from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import (
10
+ EIGER_FILE_SUFFIX,
11
+ I03_EIGER_DETECTOR,
12
+ )
13
+ from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_utils import (
14
+ VISIT_PATH_REGEX,
15
+ get_current_time_string,
16
+ )
17
+ from mx_bluesky.hyperion.parameters.components import DiffractionExperimentWithSample
18
+
19
+
20
+ def populate_data_collection_group(params: DiffractionExperimentWithSample):
21
+ dcg_info = DataCollectionGroupInfo(
22
+ visit_string=params.visit,
23
+ experiment_type=params.ispyb_experiment_type.value,
24
+ sample_id=params.sample_id,
25
+ )
26
+ return dcg_info
27
+
28
+
29
+ def populate_remaining_data_collection_info(
30
+ comment,
31
+ data_collection_group_id,
32
+ data_collection_info: DataCollectionInfo,
33
+ params: DiffractionExperimentWithSample,
34
+ ):
35
+ data_collection_info.visit_string = params.visit
36
+ data_collection_info.parent_id = data_collection_group_id
37
+ data_collection_info.sample_id = params.sample_id
38
+ data_collection_info.detector_id = I03_EIGER_DETECTOR
39
+ data_collection_info.comments = comment
40
+ data_collection_info.detector_distance = params.detector_params.detector_distance
41
+ data_collection_info.exp_time = params.detector_params.exposure_time
42
+ data_collection_info.imgdir = params.detector_params.directory
43
+ data_collection_info.imgprefix = params.detector_params.prefix
44
+ data_collection_info.imgsuffix = EIGER_FILE_SUFFIX
45
+ # Both overlap and n_passes included for backwards compatibility,
46
+ # planned to be removed later
47
+ data_collection_info.n_passes = 1
48
+ data_collection_info.overlap = 0
49
+ data_collection_info.start_image_number = 1
50
+ beam_position = params.detector_params.get_beam_position_mm(
51
+ params.detector_params.detector_distance
52
+ )
53
+ data_collection_info.xbeam = beam_position[0]
54
+ data_collection_info.ybeam = beam_position[1]
55
+ data_collection_info.start_time = get_current_time_string()
56
+ # temporary file template until nxs filewriting is integrated and we can use
57
+ # that file name
58
+ data_collection_info.file_template = f"{params.detector_params.prefix}_{data_collection_info.data_collection_number}_master.h5"
59
+ return data_collection_info
60
+
61
+
62
+ def get_proposal_and_session_from_visit_string(visit_string: str) -> tuple[str, int]:
63
+ visit_parts = visit_string.split("-")
64
+ assert len(visit_parts) == 2, f"Unexpected visit string {visit_string}"
65
+ return visit_parts[0], int(visit_parts[1])
66
+
67
+
68
+ def get_visit_string_from_path(path: str | None) -> str | None:
69
+ match = re.search(VISIT_PATH_REGEX, path) if path else None
70
+ return str(match.group(1)) if match else None
@@ -0,0 +1,88 @@
1
+ from typing import TypedDict
2
+
3
+ import numpy as np
4
+ from bluesky.callbacks import CallbackBase
5
+ from dodal.devices.oav.oav_detector import OAVConfigParams
6
+ from dodal.devices.oav.utils import calculate_x_y_z_of_pixel
7
+ from event_model.documents import Event
8
+
9
+ from mx_bluesky.hyperion.log import LOGGER
10
+
11
+
12
+ class GridParamUpdate(TypedDict):
13
+ x_start_um: float
14
+ y_start_um: float
15
+ y2_start_um: float
16
+ z_start_um: float
17
+ z2_start_um: float
18
+ x_steps: int
19
+ y_steps: int
20
+ z_steps: int
21
+ x_step_size_um: float
22
+ y_step_size_um: float
23
+ z_step_size_um: float
24
+
25
+
26
+ class GridDetectionCallback(CallbackBase):
27
+ def __init__(
28
+ self,
29
+ oav_params: OAVConfigParams,
30
+ *args,
31
+ ) -> None:
32
+ super().__init__(*args)
33
+ self.oav_params = oav_params
34
+ self.start_positions: list = []
35
+ self.box_numbers: list = []
36
+
37
+ def event(self, doc: Event):
38
+ 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"]
41
+ x_of_centre_of_first_box_px = top_left_x_px + box_width_px / 2
42
+
43
+ top_left_y_px = data["oav_grid_snapshot_top_left_y"]
44
+ y_of_centre_of_first_box_px = top_left_y_px + box_width_px / 2
45
+
46
+ smargon_omega = data["smargon-omega"]
47
+ current_xyz = np.array(
48
+ [data["smargon-x"], data["smargon-y"], data["smargon-z"]]
49
+ )
50
+
51
+ centre_of_first_box = (
52
+ x_of_centre_of_first_box_px,
53
+ y_of_centre_of_first_box_px,
54
+ )
55
+
56
+ position_grid_start = calculate_x_y_z_of_pixel(
57
+ current_xyz, smargon_omega, centre_of_first_box, self.oav_params
58
+ )
59
+
60
+ LOGGER.info(f"Calculated start position {position_grid_start}")
61
+
62
+ self.start_positions.append(position_grid_start)
63
+ self.box_numbers.append(
64
+ (
65
+ data["oav_grid_snapshot_num_boxes_x"],
66
+ data["oav_grid_snapshot_num_boxes_y"],
67
+ )
68
+ )
69
+
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
73
+ return doc
74
+
75
+ def get_grid_parameters(self) -> GridParamUpdate:
76
+ 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_steps": self.box_numbers[0][0],
83
+ "y_steps": self.box_numbers[0][1],
84
+ "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,
88
+ }
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import abstractmethod
4
+ from collections.abc import Callable, Sequence
5
+ from typing import TYPE_CHECKING, Any, TypeVar, cast
6
+
7
+ from dodal.beamline_specific_utils.i03 import beam_size_from_aperture
8
+ from dodal.devices.detector.det_resolution import resolution
9
+ from dodal.devices.synchrotron import SynchrotronMode
10
+
11
+ from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import (
12
+ PlanReactiveCallback,
13
+ )
14
+ from mx_bluesky.hyperion.external_interaction.ispyb.data_model import (
15
+ DataCollectionInfo,
16
+ DataCollectionPositionInfo,
17
+ ScanDataInfo,
18
+ )
19
+ from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_store import (
20
+ IspybIds,
21
+ StoreInIspyb,
22
+ )
23
+ 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
26
+ from mx_bluesky.hyperion.parameters.constants import CONST
27
+ from mx_bluesky.hyperion.utils.utils import convert_eV_to_angstrom
28
+
29
+ from .logging_callback import format_doc_for_log
30
+
31
+ D = TypeVar("D")
32
+ if TYPE_CHECKING:
33
+ from event_model.documents import Event, EventDescriptor, RunStart, RunStop
34
+
35
+
36
+ class BaseISPyBCallback(PlanReactiveCallback):
37
+ def __init__(
38
+ self,
39
+ *,
40
+ emit: Callable[..., Any] | None = None,
41
+ ) -> None:
42
+ """Subclasses should run super().__init__() with parameters, then set
43
+ self.ispyb to the type of ispyb relevant to the experiment and define the type
44
+ for self.ispyb_ids."""
45
+ ISPYB_LOGGER.debug("Initialising ISPyB callback")
46
+ super().__init__(log=ISPYB_LOGGER, emit=emit)
47
+ self._oav_snapshot_event_idx: int = 0
48
+ self.params: DiffractionExperimentWithSample | None = None
49
+ self.ispyb: StoreInIspyb
50
+ self.descriptors: dict[str, EventDescriptor] = {}
51
+ self.ispyb_config = get_ispyb_config()
52
+ if (
53
+ self.ispyb_config == CONST.SIM.ISPYB_CONFIG
54
+ or self.ispyb_config == CONST.SIM.DEV_ISPYB_DATABASE_CFG
55
+ ):
56
+ ISPYB_LOGGER.warning(
57
+ f"{self.__class__} using dev ISPyB config: {self.ispyb_config}. If you"
58
+ "want to use the real database, please set the ISPYB_CONFIG_PATH "
59
+ "environment variable."
60
+ )
61
+ self.uid_to_finalize_on: str | None = None
62
+ self.ispyb_ids: IspybIds = IspybIds()
63
+ self.log = ISPYB_LOGGER
64
+
65
+ def activity_gated_start(self, doc: RunStart):
66
+ self._oav_snapshot_event_idx = 0
67
+ return self._tag_doc(doc)
68
+
69
+ def activity_gated_descriptor(self, doc: EventDescriptor):
70
+ self.descriptors[doc["uid"]] = doc
71
+ return self._tag_doc(doc)
72
+
73
+ def activity_gated_event(self, doc: Event) -> Event:
74
+ """Subclasses should extend this to add a call to set_dcig_tag from
75
+ hyperion.log"""
76
+ ISPYB_LOGGER.debug("ISPyB handler received event document.")
77
+ assert self.ispyb is not None, "ISPyB deposition wasn't initialised!"
78
+ assert self.params is not None, "ISPyB handler didn't receive parameters!"
79
+
80
+ event_descriptor = self.descriptors.get(doc["descriptor"])
81
+ if event_descriptor is None:
82
+ ISPYB_LOGGER.warning(
83
+ f"Ispyb handler {self} received event doc {format_doc_for_log(doc)} and "
84
+ "has no corresponding descriptor record"
85
+ )
86
+ return doc
87
+ match event_descriptor.get("name"):
88
+ case CONST.DESCRIPTORS.HARDWARE_READ_PRE:
89
+ scan_data_infos = self._handle_ispyb_hardware_read(doc)
90
+ case CONST.DESCRIPTORS.HARDWARE_READ_DURING:
91
+ scan_data_infos = self._handle_ispyb_transmission_flux_read(doc)
92
+ case _:
93
+ return self._tag_doc(doc)
94
+ self.ispyb_ids = self.ispyb.update_deposition(self.ispyb_ids, scan_data_infos)
95
+ ISPYB_LOGGER.info(f"Received ISPYB IDs: {self.ispyb_ids}")
96
+ return self._tag_doc(doc)
97
+
98
+ def _handle_ispyb_hardware_read(self, doc) -> Sequence[ScanDataInfo]:
99
+ assert self.params, "Event handled before activity_gated_start received params"
100
+ ISPYB_LOGGER.info("ISPyB handler received event from read hardware")
101
+ assert isinstance(
102
+ synchrotron_mode := doc["data"]["synchrotron-synchrotron_mode"],
103
+ SynchrotronMode,
104
+ )
105
+
106
+ hwscan_data_collection_info = DataCollectionInfo(
107
+ undulator_gap1=doc["data"]["undulator-current_gap"],
108
+ synchrotron_mode=synchrotron_mode.value,
109
+ slitgap_horizontal=doc["data"]["s4_slit_gaps_xgap"],
110
+ slitgap_vertical=doc["data"]["s4_slit_gaps_ygap"],
111
+ )
112
+ hwscan_position_info = DataCollectionPositionInfo(
113
+ pos_x=doc["data"]["smargon-x"],
114
+ pos_y=doc["data"]["smargon-y"],
115
+ pos_z=doc["data"]["smargon-z"],
116
+ )
117
+ scan_data_infos = self.populate_info_for_update(
118
+ hwscan_data_collection_info, hwscan_position_info, self.params
119
+ )
120
+ ISPYB_LOGGER.info("Updating ispyb data collection after hardware read.")
121
+ return scan_data_infos
122
+
123
+ def _handle_ispyb_transmission_flux_read(self, doc) -> Sequence[ScanDataInfo]:
124
+ assert self.params
125
+ aperture = doc["data"]["aperture_scatterguard-selected_aperture"]
126
+ aperture_radius = doc["data"]["aperture_scatterguard-radius"]
127
+ beamsize = beam_size_from_aperture(aperture_radius)
128
+ beamsize_x_mm = beamsize.x_um / 1000 if beamsize.x_um else None
129
+ beamsize_y_mm = beamsize.y_um / 1000 if beamsize.y_um else None
130
+ hwscan_data_collection_info = DataCollectionInfo(
131
+ beamsize_at_samplex=beamsize_x_mm,
132
+ beamsize_at_sampley=beamsize_y_mm,
133
+ focal_spot_size_at_samplex=beamsize_x_mm,
134
+ focal_spot_size_at_sampley=beamsize_y_mm,
135
+ flux=doc["data"]["flux_flux_reading"],
136
+ )
137
+ if transmission := doc["data"]["attenuator-actual_transmission"]:
138
+ # Ispyb wants the transmission in a percentage, we use fractions
139
+ 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
+ )
150
+ scan_data_infos = self.populate_info_for_update(
151
+ hwscan_data_collection_info, None, self.params
152
+ )
153
+ ISPYB_LOGGER.info("Updating ispyb data collection after flux read.")
154
+ self.append_to_comment(f"Aperture: {aperture}. ")
155
+ return scan_data_infos
156
+
157
+ @abstractmethod
158
+ def populate_info_for_update(
159
+ self,
160
+ event_sourced_data_collection_info: DataCollectionInfo,
161
+ event_sourced_position_info: DataCollectionPositionInfo | None,
162
+ params: DiffractionExperimentWithSample,
163
+ ) -> Sequence[ScanDataInfo]:
164
+ pass
165
+
166
+ def activity_gated_stop(self, doc: RunStop) -> RunStop:
167
+ """Subclasses must check that they are recieving a stop document for the correct
168
+ uid to use this method!"""
169
+ assert (
170
+ self.ispyb is not None
171
+ ), "ISPyB handler received stop document, but deposition object doesn't exist!"
172
+ ISPYB_LOGGER.debug("ISPyB handler received stop document.")
173
+ exit_status = (
174
+ doc.get("exit_status") or "Exit status not available in stop document!"
175
+ )
176
+ reason = doc.get("reason") or ""
177
+ set_dcgid_tag(None)
178
+ try:
179
+ self.ispyb.end_deposition(self.ispyb_ids, exit_status, reason)
180
+ except Exception as e:
181
+ ISPYB_LOGGER.warning(
182
+ f"Failed to finalise ISPyB deposition on stop document: {format_doc_for_log(doc)} with exception: {e}"
183
+ )
184
+ return self._tag_doc(doc)
185
+
186
+ def _append_to_comment(self, id: int, comment: str) -> None:
187
+ assert self.ispyb is not None
188
+ try:
189
+ self.ispyb.append_to_comment(id, comment)
190
+ except TypeError:
191
+ ISPYB_LOGGER.warning(
192
+ "ISPyB deposition not initialised, can't update comment."
193
+ )
194
+
195
+ def append_to_comment(self, comment: str):
196
+ for id in self.ispyb_ids.data_collection_ids:
197
+ self._append_to_comment(id, comment)
198
+
199
+ def _tag_doc(self, doc: D) -> D:
200
+ assert isinstance(doc, dict)
201
+ if self.ispyb_ids:
202
+ doc["ispyb_dcids"] = self.ispyb_ids.data_collection_ids
203
+ return cast(D, doc)
@@ -0,0 +1,20 @@
1
+ from bluesky.callbacks import CallbackBase
2
+ from event_model import RunStart, RunStop
3
+
4
+ from mx_bluesky.hyperion.log import set_uid_tag
5
+
6
+
7
+ class LogUidTaggingCallback(CallbackBase):
8
+ def __init__(self) -> None:
9
+ """Sets the logging filter to add the outermost run uid to graylog messages"""
10
+ self.run_uid = None
11
+
12
+ def start(self, doc: RunStart):
13
+ if self.run_uid is None:
14
+ self.run_uid = doc.get("uid")
15
+ set_uid_tag(self.run_uid)
16
+
17
+ def stop(self, doc: RunStop):
18
+ if doc.get("run_start") == self.run_uid:
19
+ self.run_uid = None
20
+ set_uid_tag(None)
@@ -0,0 +1,29 @@
1
+ import json
2
+
3
+ from bluesky.callbacks import CallbackBase
4
+
5
+ from mx_bluesky.hyperion.log import LOGGER
6
+
7
+
8
+ class _BestEffortEncoder(json.JSONEncoder):
9
+ def default(self, o):
10
+ return repr(o)
11
+
12
+
13
+ def format_doc_for_log(doc):
14
+ return json.dumps(doc, indent=2, cls=_BestEffortEncoder)
15
+
16
+
17
+ class VerbosePlanExecutionLoggingCallback(CallbackBase):
18
+ def start(self, doc):
19
+ LOGGER.info(f"START: {format_doc_for_log(doc)}")
20
+
21
+ def descriptor(self, doc):
22
+ LOGGER.info(f"DESCRIPTOR: {format_doc_for_log(doc)}")
23
+
24
+ def event(self, doc):
25
+ LOGGER.info(f"EVENT: {format_doc_for_log(doc)}")
26
+ return doc
27
+
28
+ def stop(self, doc):
29
+ LOGGER.info(f"STOP: {format_doc_for_log(doc)}")
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from logging import Logger
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from bluesky.callbacks import CallbackBase
8
+
9
+ if TYPE_CHECKING:
10
+ from event_model.documents import Event, EventDescriptor, RunStart, RunStop
11
+
12
+
13
+ class PlanReactiveCallback(CallbackBase):
14
+ log: Logger # type: ignore # this is initialised to None and not annotated in the superclass
15
+
16
+ def __init__(
17
+ self,
18
+ log: Logger,
19
+ *,
20
+ emit: Callable[..., Any] | None = None,
21
+ ) -> None:
22
+ """A callback base class which can be left permanently subscribed to a plan, and
23
+ will 'activate' and 'deactivate' at the start and end of a plan which provides
24
+ metadata to trigger this.
25
+ The run_decorator of the plan should include in its metadata dictionary the key
26
+ 'activate callbacks', with a list of strings of the callback class(es) to
27
+ activate or deactivate. On a recieving a start doc which specifies this, this
28
+ class will be activated, and on recieving the stop document for the
29
+ corresponding uid it will deactivate. The ordinary 'start', 'descriptor',
30
+ 'event' and 'stop' methods will be triggered as normal, and will in turn trigger
31
+ 'activity_gated_' methods - to preserve this functionality, subclasses which
32
+ override 'start' etc. should include a call to super().start(...) etc.
33
+ The logic of how activation is triggered will change to a more readable, version
34
+ in the future (https://github.com/DiamondLightSource/hyperion/issues/964)."""
35
+
36
+ super().__init__(emit=emit)
37
+ self.emit_cb = emit # to avoid GC; base class only holds a WeakRef
38
+ self.active = False
39
+ self.activity_uid = 0
40
+ self.log = log
41
+
42
+ def _run_activity_gated(self, name: str, func, doc, override=False):
43
+ # Runs `func` if self.active is True or overide is true. Override can be used
44
+ # to run the function even after setting self.active to False, i.e. in the last
45
+ # handler of a run.
46
+
47
+ running_gated_function = override or self.active
48
+ if not running_gated_function:
49
+ return doc
50
+ try:
51
+ return self.emit(name, func(doc))
52
+ except Exception as e:
53
+ self.log.exception(e)
54
+ raise
55
+
56
+ def start(self, doc: RunStart) -> RunStart | None:
57
+ callbacks_to_activate = doc.get("activate_callbacks")
58
+ if callbacks_to_activate and not self.active:
59
+ activate = type(self).__name__ in callbacks_to_activate
60
+ self.active = activate
61
+ self.log.info(
62
+ f"{'' if activate else 'not'} activating {type(self).__name__}"
63
+ )
64
+ self.activity_uid = doc.get("uid")
65
+ return self._run_activity_gated("start", self.activity_gated_start, doc)
66
+
67
+ def descriptor(self, doc: EventDescriptor) -> EventDescriptor | None:
68
+ return self._run_activity_gated(
69
+ "descriptor", self.activity_gated_descriptor, doc
70
+ )
71
+
72
+ def event(self, doc: Event) -> Event | None: # type: ignore
73
+ return self._run_activity_gated("event", self.activity_gated_event, doc)
74
+
75
+ def stop(self, doc: RunStop) -> RunStop | None:
76
+ do_stop = self.active
77
+ if doc.get("run_start") == self.activity_uid:
78
+ self.active = False
79
+ self.activity_uid = 0
80
+ return (
81
+ self._run_activity_gated(
82
+ "stop", self.activity_gated_stop, doc, override=True
83
+ )
84
+ if do_stop
85
+ else doc
86
+ )
87
+
88
+ def activity_gated_start(self, doc: RunStart) -> RunStart | None:
89
+ return doc
90
+
91
+ def activity_gated_descriptor(self, doc: EventDescriptor) -> EventDescriptor | None:
92
+ return doc
93
+
94
+ def activity_gated_event(self, doc: Event) -> Event | None:
95
+ return doc
96
+
97
+ def activity_gated_stop(self, doc: RunStop) -> RunStop | None:
98
+ return doc
99
+
100
+ def __repr__(self) -> str:
101
+ return f"<{self.__class__.__name__} with id: {hex(id(self))} - active: {self.active}>"
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from event_model.documents import EventDescriptor
6
+
7
+ from mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping import (
8
+ get_proposal_and_session_from_visit_string,
9
+ get_visit_string_from_path,
10
+ )
11
+ from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import (
12
+ PlanReactiveCallback,
13
+ )
14
+ from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import (
15
+ ExpeyeInteraction,
16
+ RobotActionID,
17
+ )
18
+ from mx_bluesky.hyperion.log import ISPYB_LOGGER
19
+ from mx_bluesky.hyperion.parameters.constants import CONST
20
+
21
+ if TYPE_CHECKING:
22
+ from event_model.documents import Event, EventDescriptor, RunStart, RunStop
23
+
24
+
25
+ class RobotLoadISPyBCallback(PlanReactiveCallback):
26
+ def __init__(self) -> None:
27
+ ISPYB_LOGGER.debug("Initialising ISPyB Robot Load Callback")
28
+ super().__init__(log=ISPYB_LOGGER)
29
+ self.run_uid: str | None = None
30
+ self.descriptors: dict[str, EventDescriptor] = {}
31
+ self.action_id: RobotActionID | None = None
32
+ self.expeye = ExpeyeInteraction()
33
+
34
+ def activity_gated_start(self, doc: RunStart):
35
+ ISPYB_LOGGER.debug("ISPyB robot load callback received start document.")
36
+ if doc.get("subplan_name") == CONST.PLAN.ROBOT_LOAD:
37
+ ISPYB_LOGGER.debug(f"ISPyB robot load callback received: {doc}")
38
+ self.run_uid = doc.get("uid")
39
+ assert isinstance(metadata := doc.get("metadata"), dict)
40
+ assert isinstance(
41
+ visit := get_visit_string_from_path(metadata["visit_path"]), str
42
+ )
43
+ proposal, session = get_proposal_and_session_from_visit_string(visit)
44
+ self.action_id = self.expeye.start_load(
45
+ proposal,
46
+ session,
47
+ metadata["sample_id"],
48
+ metadata["sample_puck"],
49
+ metadata["sample_pin"],
50
+ )
51
+ return super().activity_gated_start(doc)
52
+
53
+ def activity_gated_descriptor(self, doc: EventDescriptor) -> EventDescriptor | None:
54
+ self.descriptors[doc["uid"]] = doc
55
+ return super().activity_gated_descriptor(doc)
56
+
57
+ def activity_gated_event(self, doc: Event) -> Event | None:
58
+ event_descriptor = self.descriptors.get(doc["descriptor"])
59
+ if (
60
+ event_descriptor
61
+ and event_descriptor.get("name") == CONST.DESCRIPTORS.ROBOT_LOAD
62
+ ):
63
+ assert (
64
+ self.action_id is not None
65
+ ), "ISPyB Robot load callback event called unexpectedly"
66
+ barcode = doc["data"]["robot-barcode"]
67
+ oav_snapshot = doc["data"]["oav_snapshot_last_saved_path"]
68
+ webcam_snapshot = doc["data"]["webcam-last_saved_path"]
69
+ # I03 uses webcam/oav snapshots in place of before/after snapshots
70
+ self.expeye.update_barcode_and_snapshots(
71
+ self.action_id, barcode, webcam_snapshot, oav_snapshot
72
+ )
73
+
74
+ return super().activity_gated_event(doc)
75
+
76
+ def activity_gated_stop(self, doc: RunStop) -> RunStop | None:
77
+ ISPYB_LOGGER.debug("ISPyB robot load callback received stop document.")
78
+ if doc.get("run_start") == self.run_uid:
79
+ assert (
80
+ self.action_id is not None
81
+ ), "ISPyB Robot load callback stop called unexpectedly"
82
+ exit_status = (
83
+ doc.get("exit_status") or "Exit status not available in stop document!"
84
+ )
85
+ reason = doc.get("reason") or "OK"
86
+ self.expeye.end_load(self.action_id, exit_status, reason)
87
+ self.action_id = None
88
+ return super().activity_gated_stop(doc)