mx-bluesky 0.0.2__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 (150) hide show
  1. mx_bluesky/__main__.py +1 -2
  2. mx_bluesky/_version.py +14 -2
  3. mx_bluesky/beamlines/i04/__init__.py +3 -0
  4. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +45 -0
  5. mx_bluesky/beamlines/i04/thawing_plan.py +85 -0
  6. mx_bluesky/beamlines/i24/serial/__init__.py +49 -0
  7. mx_bluesky/beamlines/i24/serial/blueapi_config.yaml +12 -0
  8. mx_bluesky/{I24 → beamlines/i24}/serial/dcid.py +53 -41
  9. mx_bluesky/{I24 → beamlines/i24}/serial/extruder/EX-gui-edm/DetStage.edl +3 -4
  10. mx_bluesky/{I24 → beamlines/i24}/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +28 -32
  11. mx_bluesky/{I24 → beamlines/i24}/serial/extruder/EX-gui-edm/microdrop_alignment.edl +0 -1
  12. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +516 -0
  13. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/CustomChip_py3v1.edl +3 -4
  14. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/DetStage.edl +3 -4
  15. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +273 -223
  16. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/ME14E-GeneralPurpose.edl +0 -1
  17. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/MappingLite-oxford_py3v1.edl +12 -13
  18. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/PMAC_Command.edl +0 -1
  19. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/Shutter_Control.edl +0 -1
  20. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/microdrop_alignment.edl +0 -1
  21. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/nudgechip.edl +0 -1
  22. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/FT-gui-edm/pumpprobe-py3v1.edl +273 -143
  23. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/short1-laser.png +0 -0
  24. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/short2-laser.png +0 -0
  25. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/ft_utils.py +24 -1
  26. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +808 -0
  27. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +377 -416
  28. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/i24ssx_Chip_Mapping_py3v1.py +34 -40
  29. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +328 -0
  30. mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/i24ssx_moveonclick.py +66 -48
  31. mx_bluesky/{I24 → beamlines/i24}/serial/log.py +66 -19
  32. mx_bluesky/beamlines/i24/serial/parameters/__init__.py +15 -0
  33. mx_bluesky/beamlines/i24/serial/parameters/constants.py +47 -0
  34. mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py +103 -0
  35. mx_bluesky/beamlines/i24/serial/parameters/fixed_target/cs/cs_maker.json +9 -0
  36. mx_bluesky/{I24 → beamlines/i24}/serial/parameters/fixed_target/cs/motor_direction.txt +1 -1
  37. mx_bluesky/{I24 → beamlines/i24}/serial/parameters/fixed_target/pvar_files/minichip-oxford.pvar +1 -1
  38. mx_bluesky/beamlines/i24/serial/parameters/utils.py +42 -0
  39. mx_bluesky/beamlines/i24/serial/run_extruder.sh +19 -0
  40. mx_bluesky/beamlines/i24/serial/run_fixed_target.sh +22 -0
  41. mx_bluesky/beamlines/i24/serial/run_serial.py +36 -0
  42. mx_bluesky/{I24 → beamlines/i24}/serial/set_visit_directory.sh +6 -1
  43. mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/pv.py +1 -62
  44. mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/pv_abstract.py +6 -7
  45. mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/setup_beamline.py +90 -269
  46. mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/setup_detector.py +47 -40
  47. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +459 -0
  48. mx_bluesky/beamlines/i24/serial/start_blueapi.sh +28 -0
  49. mx_bluesky/beamlines/i24/serial/write_nexus.py +105 -0
  50. mx_bluesky/example.py +4 -4
  51. mx_bluesky/hyperion/__init__.py +1 -0
  52. mx_bluesky/hyperion/__main__.py +374 -0
  53. mx_bluesky/hyperion/device_setup_plans/__init__.py +0 -0
  54. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +134 -0
  55. mx_bluesky/hyperion/device_setup_plans/manipulate_sample.py +110 -0
  56. mx_bluesky/hyperion/device_setup_plans/position_detector.py +16 -0
  57. mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py +60 -0
  58. mx_bluesky/hyperion/device_setup_plans/setup_oav.py +87 -0
  59. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +210 -0
  60. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +214 -0
  61. mx_bluesky/hyperion/device_setup_plans/smargon.py +25 -0
  62. mx_bluesky/hyperion/device_setup_plans/utils.py +44 -0
  63. mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +93 -0
  64. mx_bluesky/hyperion/exceptions.py +47 -0
  65. mx_bluesky/hyperion/experiment_plans/__init__.py +30 -0
  66. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +84 -0
  67. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +528 -0
  68. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +209 -0
  69. mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +173 -0
  70. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +81 -0
  71. mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +463 -0
  72. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +119 -0
  73. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +164 -0
  74. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +322 -0
  75. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +436 -0
  76. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +68 -0
  77. mx_bluesky/hyperion/external_interaction/__init__.py +9 -0
  78. mx_bluesky/hyperion/external_interaction/callbacks/__init__.py +10 -0
  79. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +148 -0
  80. mx_bluesky/hyperion/external_interaction/callbacks/aperture_change_callback.py +22 -0
  81. mx_bluesky/hyperion/external_interaction/callbacks/common/__init__.py +0 -0
  82. mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py +46 -0
  83. mx_bluesky/hyperion/external_interaction/callbacks/common/ispyb_mapping.py +70 -0
  84. mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py +88 -0
  85. mx_bluesky/hyperion/external_interaction/callbacks/ispyb_callback_base.py +203 -0
  86. mx_bluesky/hyperion/external_interaction/callbacks/log_uid_tag_callback.py +20 -0
  87. mx_bluesky/hyperion/external_interaction/callbacks/logging_callback.py +29 -0
  88. mx_bluesky/hyperion/external_interaction/callbacks/plan_reactive_callback.py +101 -0
  89. mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +88 -0
  90. mx_bluesky/hyperion/external_interaction/callbacks/rotation/__init__.py +0 -0
  91. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +174 -0
  92. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +17 -0
  93. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +102 -0
  94. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/__init__.py +0 -0
  95. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +269 -0
  96. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_mapping.py +53 -0
  97. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py +95 -0
  98. mx_bluesky/hyperion/external_interaction/callbacks/zocalo_callback.py +92 -0
  99. mx_bluesky/hyperion/external_interaction/config_server.py +35 -0
  100. mx_bluesky/hyperion/external_interaction/exceptions.py +13 -0
  101. mx_bluesky/hyperion/external_interaction/ispyb/__init__.py +0 -0
  102. mx_bluesky/hyperion/external_interaction/ispyb/data_model.py +95 -0
  103. mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +125 -0
  104. mx_bluesky/hyperion/external_interaction/ispyb/ispyb_store.py +276 -0
  105. mx_bluesky/hyperion/external_interaction/ispyb/ispyb_utils.py +29 -0
  106. mx_bluesky/hyperion/external_interaction/nexus/__init__.py +0 -0
  107. mx_bluesky/hyperion/external_interaction/nexus/nexus_utils.py +148 -0
  108. mx_bluesky/hyperion/external_interaction/nexus/write_nexus.py +114 -0
  109. mx_bluesky/hyperion/log.py +99 -0
  110. mx_bluesky/hyperion/parameters/__init__.py +2 -0
  111. mx_bluesky/hyperion/parameters/cli.py +68 -0
  112. mx_bluesky/hyperion/parameters/components.py +253 -0
  113. mx_bluesky/hyperion/parameters/constants.py +158 -0
  114. mx_bluesky/hyperion/parameters/gridscan.py +216 -0
  115. mx_bluesky/hyperion/parameters/rotation.py +160 -0
  116. mx_bluesky/hyperion/resources/panda/panda-gridscan.yaml +964 -0
  117. mx_bluesky/hyperion/tracing.py +28 -0
  118. mx_bluesky/hyperion/utils/context.py +84 -0
  119. mx_bluesky/hyperion/utils/utils.py +25 -0
  120. mx_bluesky/hyperion/utils/validation.py +196 -0
  121. mx_bluesky/jupyter_example.ipynb +3 -2
  122. {mx_bluesky-0.0.2.dist-info → mx_bluesky-1.1.0.dist-info}/METADATA +53 -32
  123. mx_bluesky-1.1.0.dist-info/RECORD +136 -0
  124. {mx_bluesky-0.0.2.dist-info → mx_bluesky-1.1.0.dist-info}/WHEEL +1 -1
  125. mx_bluesky-1.1.0.dist-info/entry_points.txt +8 -0
  126. mx_bluesky/I24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +0 -476
  127. mx_bluesky/I24/serial/fixed_target/FT-gui-edm/ME14E-motors.edl +0 -1874
  128. mx_bluesky/I24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +0 -706
  129. mx_bluesky/I24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +0 -463
  130. mx_bluesky/I24/serial/parameters/__init__.py +0 -5
  131. mx_bluesky/I24/serial/parameters/constants.py +0 -39
  132. mx_bluesky/I24/serial/parameters/fixed_target/cs/cs_maker.json +0 -9
  133. mx_bluesky/I24/serial/parameters/fixed_target/cs/fiducial_1.txt +0 -4
  134. mx_bluesky/I24/serial/parameters/fixed_target/cs/fiducial_2.txt +0 -4
  135. mx_bluesky/I24/serial/parameters/fixed_target/litemaps/currentchip.map +0 -81
  136. mx_bluesky/I24/serial/parameters/fixed_target/parameters.txt +0 -13
  137. mx_bluesky/I24/serial/run_serial.py +0 -52
  138. mx_bluesky/I24/serial/write_nexus.py +0 -113
  139. mx_bluesky-0.0.2.dist-info/RECORD +0 -58
  140. mx_bluesky-0.0.2.dist-info/entry_points.txt +0 -4
  141. /mx_bluesky/{I24 → beamlines}/__init__.py +0 -0
  142. /mx_bluesky/{I24/serial → beamlines/i24}/__init__.py +0 -0
  143. /mx_bluesky/{I24 → beamlines/i24}/serial/extruder/__init__.py +0 -0
  144. /mx_bluesky/{I24 → beamlines/i24}/serial/fixed_target/__init__.py +0 -0
  145. /mx_bluesky/{I24 → beamlines/i24}/serial/parameters/fixed_target/pvar_files/oxford.pvar +0 -0
  146. /mx_bluesky/{I24 → beamlines/i24}/serial/run_ssx.sh +0 -0
  147. /mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/__init__.py +0 -0
  148. /mx_bluesky/{I24 → beamlines/i24}/serial/setup_beamline/ca.py +0 -0
  149. {mx_bluesky-0.0.2.dist-info → mx_bluesky-1.1.0.dist-info}/LICENSE +0 -0
  150. {mx_bluesky-0.0.2.dist-info → mx_bluesky-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,276 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from dataclasses import asdict
5
+ from typing import TYPE_CHECKING
6
+
7
+ import ispyb
8
+ import ispyb.sqlalchemy
9
+ from ispyb.connector.mysqlsp.main import ISPyBMySQLSPConnector as Connector
10
+ from ispyb.sp.mxacquisition import MXAcquisition
11
+ from ispyb.strictordereddict import StrictOrderedDict
12
+ from pydantic import BaseModel
13
+
14
+ from mx_bluesky.hyperion.external_interaction.ispyb.data_model import (
15
+ DataCollectionGridInfo,
16
+ DataCollectionGroupInfo,
17
+ DataCollectionInfo,
18
+ ScanDataInfo,
19
+ )
20
+ from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_utils import (
21
+ get_current_time_string,
22
+ get_session_id_from_visit,
23
+ )
24
+ from mx_bluesky.hyperion.log import ISPYB_LOGGER
25
+ from mx_bluesky.hyperion.tracing import TRACER
26
+
27
+ if TYPE_CHECKING:
28
+ pass
29
+
30
+ I03_EIGER_DETECTOR = 78
31
+ EIGER_FILE_SUFFIX = "h5"
32
+
33
+
34
+ class IspybIds(BaseModel):
35
+ data_collection_ids: tuple[int, ...] = ()
36
+ data_collection_group_id: int | None = None
37
+ grid_ids: tuple[int, ...] = ()
38
+
39
+
40
+ class StoreInIspyb:
41
+ def __init__(self, ispyb_config: str) -> None:
42
+ self.ISPYB_CONFIG_PATH: str = ispyb_config
43
+ self._data_collection_group_id: int | None
44
+
45
+ def begin_deposition(
46
+ self,
47
+ data_collection_group_info: DataCollectionGroupInfo,
48
+ scan_data_infos: Sequence[ScanDataInfo],
49
+ ) -> IspybIds:
50
+ ispyb_ids = IspybIds()
51
+ if scan_data_infos[0].data_collection_info:
52
+ ispyb_ids.data_collection_group_id = scan_data_infos[
53
+ 0
54
+ ].data_collection_info.parent_id
55
+
56
+ return self._begin_or_update_deposition(
57
+ ispyb_ids, data_collection_group_info, scan_data_infos
58
+ )
59
+
60
+ def update_deposition(
61
+ self,
62
+ ispyb_ids,
63
+ scan_data_infos: Sequence[ScanDataInfo],
64
+ ) -> IspybIds:
65
+ assert (
66
+ ispyb_ids.data_collection_group_id
67
+ ), "Attempted to store scan data without a collection group"
68
+ assert (
69
+ ispyb_ids.data_collection_ids
70
+ ), "Attempted to store scan data without a collection"
71
+ return self._begin_or_update_deposition(ispyb_ids, None, scan_data_infos)
72
+
73
+ def _begin_or_update_deposition(
74
+ self,
75
+ ispyb_ids,
76
+ data_collection_group_info: DataCollectionGroupInfo | None,
77
+ scan_data_infos,
78
+ ) -> IspybIds:
79
+ with ispyb.open(self.ISPYB_CONFIG_PATH) as conn:
80
+ assert conn is not None, "Failed to connect to ISPyB"
81
+ if data_collection_group_info:
82
+ ispyb_ids.data_collection_group_id = (
83
+ self._store_data_collection_group_table(
84
+ conn,
85
+ data_collection_group_info,
86
+ ispyb_ids.data_collection_group_id,
87
+ )
88
+ )
89
+ else:
90
+ assert ispyb_ids.data_collection_group_id, "Attempt to update data collection without a data collection group ID"
91
+
92
+ grid_ids = list(ispyb_ids.grid_ids)
93
+ data_collection_ids_out = list(ispyb_ids.data_collection_ids)
94
+ for scan_data_info in scan_data_infos:
95
+ data_collection_id = scan_data_info.data_collection_id
96
+ if (
97
+ scan_data_info.data_collection_info
98
+ and not scan_data_info.data_collection_info.parent_id
99
+ ):
100
+ scan_data_info.data_collection_info.parent_id = (
101
+ ispyb_ids.data_collection_group_id
102
+ )
103
+
104
+ new_data_collection_id, grid_id = self._store_single_scan_data(
105
+ conn, scan_data_info, data_collection_id
106
+ )
107
+ if not data_collection_id:
108
+ data_collection_ids_out.append(new_data_collection_id)
109
+ if grid_id:
110
+ grid_ids.append(grid_id)
111
+ ispyb_ids = IspybIds(
112
+ data_collection_ids=tuple(data_collection_ids_out),
113
+ grid_ids=tuple(grid_ids),
114
+ data_collection_group_id=ispyb_ids.data_collection_group_id,
115
+ )
116
+ return ispyb_ids
117
+
118
+ def end_deposition(self, ispyb_ids: IspybIds, success: str, reason: str):
119
+ assert (
120
+ ispyb_ids.data_collection_ids
121
+ ), "Can't end ISPyB deposition, data_collection IDs are missing"
122
+ assert (
123
+ ispyb_ids.data_collection_group_id is not None
124
+ ), "Cannot end ISPyB deposition without data collection group ID"
125
+
126
+ for id_ in ispyb_ids.data_collection_ids:
127
+ ISPYB_LOGGER.info(
128
+ f"End ispyb deposition with status '{success}' and reason '{reason}'."
129
+ )
130
+ if success == "fail" or success == "abort":
131
+ run_status = "DataCollection Unsuccessful"
132
+ else:
133
+ run_status = "DataCollection Successful"
134
+ current_time = get_current_time_string()
135
+ self._update_scan_with_end_time_and_status(
136
+ current_time,
137
+ run_status,
138
+ reason,
139
+ id_,
140
+ ispyb_ids.data_collection_group_id,
141
+ )
142
+
143
+ def append_to_comment(
144
+ self, data_collection_id: int, comment: str, delimiter: str = " "
145
+ ) -> None:
146
+ with ispyb.open(self.ISPYB_CONFIG_PATH) as conn:
147
+ assert conn is not None, "Failed to connect to ISPyB!"
148
+ mx_acquisition: MXAcquisition = conn.mx_acquisition
149
+ mx_acquisition.update_data_collection_append_comments(
150
+ data_collection_id, comment, delimiter
151
+ )
152
+
153
+ def _update_scan_with_end_time_and_status(
154
+ self,
155
+ end_time: str,
156
+ run_status: str,
157
+ reason: str,
158
+ data_collection_id: int,
159
+ data_collection_group_id: int,
160
+ ) -> None:
161
+ if reason is not None and reason != "":
162
+ self.append_to_comment(data_collection_id, f"{run_status} reason: {reason}")
163
+
164
+ with ispyb.open(self.ISPYB_CONFIG_PATH) as conn:
165
+ assert conn is not None, "Failed to connect to ISPyB!"
166
+
167
+ mx_acquisition: MXAcquisition = conn.mx_acquisition
168
+
169
+ params = mx_acquisition.get_data_collection_params()
170
+ params["id"] = data_collection_id
171
+ params["parentid"] = data_collection_group_id
172
+ params["endtime"] = end_time
173
+ params["run_status"] = run_status
174
+
175
+ mx_acquisition.upsert_data_collection(list(params.values()))
176
+
177
+ def _store_position_table(
178
+ self, conn: Connector, dc_pos_info, data_collection_id
179
+ ) -> int:
180
+ mx_acquisition: MXAcquisition = conn.mx_acquisition
181
+
182
+ params = mx_acquisition.get_dc_position_params()
183
+ params["id"] = data_collection_id
184
+ params |= asdict(dc_pos_info)
185
+
186
+ return mx_acquisition.update_dc_position(list(params.values()))
187
+
188
+ def _store_data_collection_group_table(
189
+ self,
190
+ conn: Connector,
191
+ dcg_info: DataCollectionGroupInfo,
192
+ data_collection_group_id: int | None = None,
193
+ ) -> int:
194
+ mx_acquisition: MXAcquisition = conn.mx_acquisition
195
+
196
+ params = mx_acquisition.get_data_collection_group_params()
197
+ if data_collection_group_id:
198
+ params["id"] = data_collection_group_id
199
+ params["parent_id"] = get_session_id_from_visit(conn, dcg_info.visit_string)
200
+ params |= {k: v for k, v in asdict(dcg_info).items() if k != "visit_string"}
201
+
202
+ return self._upsert_data_collection_group(conn, params)
203
+
204
+ def _store_data_collection_table(
205
+ self, conn, data_collection_id, data_collection_info
206
+ ):
207
+ params = self._fill_common_data_collection_params(
208
+ conn, data_collection_id, data_collection_info
209
+ )
210
+ return self._upsert_data_collection(conn, params)
211
+
212
+ def _store_single_scan_data(
213
+ self, conn, scan_data_info, data_collection_id=None
214
+ ) -> tuple[int, int | None]:
215
+ data_collection_id = self._store_data_collection_table(
216
+ conn, data_collection_id, scan_data_info.data_collection_info
217
+ )
218
+
219
+ if scan_data_info.data_collection_position_info:
220
+ self._store_position_table(
221
+ conn,
222
+ scan_data_info.data_collection_position_info,
223
+ data_collection_id,
224
+ )
225
+
226
+ grid_id = None
227
+ if scan_data_info.data_collection_grid_info:
228
+ grid_id = self._store_grid_info_table(
229
+ conn,
230
+ data_collection_id,
231
+ scan_data_info.data_collection_grid_info,
232
+ )
233
+ return data_collection_id, grid_id
234
+
235
+ def _store_grid_info_table(
236
+ self,
237
+ conn: Connector,
238
+ ispyb_data_collection_id: int,
239
+ dc_grid_info: DataCollectionGridInfo,
240
+ ) -> int:
241
+ mx_acquisition: MXAcquisition = conn.mx_acquisition
242
+ params = mx_acquisition.get_dc_grid_params()
243
+ params |= dc_grid_info.as_dict()
244
+ params["parentid"] = ispyb_data_collection_id
245
+ return mx_acquisition.upsert_dc_grid(list(params.values()))
246
+
247
+ def _fill_common_data_collection_params(
248
+ self, conn, data_collection_id, data_collection_info: DataCollectionInfo
249
+ ) -> StrictOrderedDict:
250
+ mx_acquisition: MXAcquisition = conn.mx_acquisition
251
+ params = mx_acquisition.get_data_collection_params()
252
+
253
+ if data_collection_id:
254
+ params["id"] = data_collection_id
255
+ if data_collection_info.visit_string:
256
+ # This is only needed for populating the DataCollectionGroup
257
+ params["visit_id"] = get_session_id_from_visit(
258
+ conn, data_collection_info.visit_string
259
+ )
260
+ params |= {
261
+ k: v for k, v in asdict(data_collection_info).items() if k != "visit_string"
262
+ }
263
+
264
+ return params
265
+
266
+ @staticmethod
267
+ @TRACER.start_as_current_span("_upsert_data_collection_group")
268
+ def _upsert_data_collection_group(
269
+ conn: Connector, params: StrictOrderedDict
270
+ ) -> int:
271
+ return conn.mx_acquisition.upsert_data_collection_group(list(params.values()))
272
+
273
+ @staticmethod
274
+ @TRACER.start_as_current_span("_upsert_data_collection")
275
+ def _upsert_data_collection(conn: Connector, params: StrictOrderedDict) -> int:
276
+ return conn.mx_acquisition.upsert_data_collection(list(params.values()))
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import os
5
+
6
+ from ispyb import NoResult
7
+ from ispyb.connector.mysqlsp.main import ISPyBMySQLSPConnector as Connector
8
+ from ispyb.sp.core import Core
9
+
10
+ from mx_bluesky.hyperion.parameters.constants import CONST
11
+
12
+ VISIT_PATH_REGEX = r".+/([a-zA-Z]{2}\d{4,5}-\d{1,3})(/?$)"
13
+
14
+
15
+ def get_ispyb_config():
16
+ return os.environ.get("ISPYB_CONFIG_PATH", CONST.SIM.ISPYB_CONFIG)
17
+
18
+
19
+ def get_session_id_from_visit(conn: Connector, visit: str):
20
+ try:
21
+ core: Core = conn.core
22
+ return core.retrieve_visit_id(visit)
23
+ except NoResult as e:
24
+ raise NoResult(f"No session ID found in ispyb for visit {visit}") from e
25
+
26
+
27
+ def get_current_time_string():
28
+ now = datetime.datetime.now()
29
+ return now.strftime("%Y-%m-%d %H:%M:%S")
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from datetime import datetime, timedelta
5
+
6
+ import numpy as np
7
+ from dodal.devices.detector import DetectorParams
8
+ from dodal.devices.zebra import RotationDirection
9
+ from nexgen.nxs_utils import Attenuator, Axis, Beam, Detector, EigerDetector, Goniometer
10
+ from nexgen.nxs_utils.axes import TransformationType
11
+ from numpy.typing import DTypeLike
12
+
13
+ from mx_bluesky.hyperion.log import NEXUS_LOGGER
14
+ from mx_bluesky.hyperion.utils.utils import convert_eV_to_angstrom
15
+
16
+
17
+ def vds_type_based_on_bit_depth(detector_bit_depth: int) -> DTypeLike:
18
+ """Works out the datatype for the VDS, based on the bit depth from the detector."""
19
+ if detector_bit_depth == 8:
20
+ return np.uint8
21
+ elif detector_bit_depth == 16:
22
+ return np.uint16
23
+ elif detector_bit_depth == 32:
24
+ return np.uint32
25
+ else:
26
+ NEXUS_LOGGER.error(
27
+ f"Unknown detector bit depth {detector_bit_depth}, assuming 16-bit"
28
+ )
29
+ return np.uint16
30
+
31
+
32
+ def create_goniometer_axes(
33
+ omega_start: float,
34
+ scan_points: dict | None,
35
+ x_y_z_increments: tuple[float, float, float] = (0.0, 0.0, 0.0),
36
+ chi: float = 0.0,
37
+ phi: float = 0.0,
38
+ rotation_direction: RotationDirection = RotationDirection.NEGATIVE,
39
+ ):
40
+ """Returns a Nexgen 'Goniometer' object with the dependency chain of I03's Smargon
41
+ goniometer. If scan points is provided these values will be used in preference to
42
+ those from the params object.
43
+
44
+ Args:
45
+ omega_start (float): the starting position of omega, the only extra value that
46
+ needs to be specified except for the scan points.
47
+ scan_points (dict): a dictionary of points in the scan for each axis. Obtained
48
+ by calculating the scan path with scanspec and calling
49
+ consume() on it.
50
+ x_y_z_increments: optionally, specify the increments between each image for
51
+ the x, y, and z axes. Will be ignored if scan_points
52
+ is provided.
53
+ """
54
+ gonio_axes = [
55
+ Axis(
56
+ "omega",
57
+ ".",
58
+ TransformationType.ROTATION,
59
+ (1.0 * rotation_direction.multiplier, 0.0, 0.0),
60
+ omega_start,
61
+ ),
62
+ Axis(
63
+ name="sam_z",
64
+ depends="omega",
65
+ transformation_type=TransformationType.TRANSLATION,
66
+ vector=(0.0, 0.0, 1.0),
67
+ start_pos=0.0,
68
+ increment=x_y_z_increments[2],
69
+ ),
70
+ Axis(
71
+ name="sam_y",
72
+ depends="sam_z",
73
+ transformation_type=TransformationType.TRANSLATION,
74
+ vector=(0.0, 1.0, 0.0),
75
+ start_pos=0.0,
76
+ increment=x_y_z_increments[1],
77
+ ),
78
+ Axis(
79
+ name="sam_x",
80
+ depends="sam_y",
81
+ transformation_type=TransformationType.TRANSLATION,
82
+ vector=(1.0, 0.0, 0.0),
83
+ start_pos=0.0,
84
+ increment=x_y_z_increments[0],
85
+ ),
86
+ Axis(
87
+ "chi", "sam_x", TransformationType.ROTATION, (0.006, -0.0264, 0.9996), chi
88
+ ),
89
+ Axis("phi", "chi", TransformationType.ROTATION, (-1, -0.0025, -0.0056), phi),
90
+ ]
91
+ return Goniometer(gonio_axes, scan_points)
92
+
93
+
94
+ def get_start_and_predicted_end_time(time_expected: float) -> tuple[str, str]:
95
+ time_format = r"%Y-%m-%dT%H:%M:%SZ"
96
+ start = datetime.utcfromtimestamp(time.time())
97
+ end_est = start + timedelta(seconds=time_expected)
98
+ return start.strftime(time_format), end_est.strftime(time_format)
99
+
100
+
101
+ def create_detector_parameters(detector_params: DetectorParams) -> Detector:
102
+ """Returns the detector information in a format that nexgen wants.
103
+
104
+ Args:
105
+ detector_params (DetectorParams): The detector params as Hyperion stores them.
106
+
107
+ Returns:
108
+ Detector: Detector description for nexgen.
109
+ """
110
+ detector_pixels = detector_params.get_detector_size_pizels()
111
+
112
+ eiger_params = EigerDetector(
113
+ "Eiger 16M", (detector_pixels.height, detector_pixels.width), "Si", 46051, 0
114
+ )
115
+
116
+ detector_axes = [
117
+ Axis(
118
+ "det_z",
119
+ ".",
120
+ TransformationType.TRANSLATION,
121
+ (0.0, 0.0, 1.0),
122
+ detector_params.detector_distance,
123
+ )
124
+ ]
125
+ # Eiger parameters, axes, beam_center, exp_time, [fast, slow]
126
+ return Detector(
127
+ eiger_params,
128
+ detector_axes,
129
+ list(
130
+ detector_params.get_beam_position_pixels(detector_params.detector_distance)
131
+ ),
132
+ detector_params.exposure_time,
133
+ [(-1.0, 0.0, 0.0), (0.0, -1.0, 0.0)],
134
+ )
135
+
136
+
137
+ def create_beam_and_attenuator_parameters(
138
+ energy_kev: float, flux: float, transmission_fraction: float
139
+ ) -> tuple[Beam, Attenuator]:
140
+ """Create beam and attenuator objects that nexgen can understands
141
+
142
+ Returns:
143
+ tuple[Beam, Attenuator]: Descriptions of the beam and attenuator for nexgen.
144
+ """
145
+ return (
146
+ Beam(convert_eV_to_angstrom(energy_kev * 1000), flux), # pyright: ignore
147
+ Attenuator(transmission_fraction), # pyright: ignore
148
+ )
@@ -0,0 +1,114 @@
1
+ """
2
+ Define beamline parameters for I03, Eiger detector and give an example of writing a
3
+ gridscan.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ from pathlib import Path
10
+
11
+ from dodal.devices.zebra import RotationDirection
12
+ from dodal.utils import get_beamline_name
13
+ from nexgen.nxs_utils import Attenuator, Beam, Detector, Goniometer, Source
14
+ from nexgen.nxs_write.nxmx_writer import NXmxFileWriter
15
+ from numpy.typing import DTypeLike
16
+ from scanspec.core import AxesPoints
17
+
18
+ from mx_bluesky.hyperion.external_interaction.nexus.nexus_utils import (
19
+ create_detector_parameters,
20
+ create_goniometer_axes,
21
+ get_start_and_predicted_end_time,
22
+ )
23
+ from mx_bluesky.hyperion.parameters.components import DiffractionExperimentWithSample
24
+
25
+
26
+ class NexusWriter:
27
+ def __init__(
28
+ self,
29
+ parameters: DiffractionExperimentWithSample,
30
+ data_shape: tuple[int, int, int],
31
+ scan_points: AxesPoints,
32
+ *,
33
+ run_number: int | None = None,
34
+ omega_start_deg: float = 0,
35
+ chi_start_deg: float = 0,
36
+ phi_start_deg: float = 0,
37
+ vds_start_index: int = 0,
38
+ # override default values when there is more than one collection per
39
+ # detector arming event:
40
+ full_num_of_images: int | None = None,
41
+ meta_data_run_number: int | None = None,
42
+ rotation_direction: RotationDirection = RotationDirection.NEGATIVE,
43
+ ) -> None:
44
+ self.beam: Beam | None = None
45
+ self.attenuator: Attenuator | None = None
46
+ self.scan_points: dict = scan_points
47
+ self.data_shape: tuple[int, int, int] = data_shape
48
+ self.run_number: int = (
49
+ run_number if run_number else parameters.detector_params.run_number
50
+ )
51
+ self.detector: Detector = create_detector_parameters(parameters.detector_params)
52
+ self.source: Source = Source(get_beamline_name("S03"))
53
+ self.directory: Path = Path(parameters.storage_directory)
54
+ self.start_index: int = vds_start_index
55
+ self.full_num_of_images: int = full_num_of_images or parameters.num_images
56
+ self.data_filename: str = (
57
+ f"{parameters.file_name}_{meta_data_run_number}"
58
+ if meta_data_run_number
59
+ else parameters.detector_params.full_filename
60
+ )
61
+ self.nexus_file: Path = (
62
+ self.directory / f"{parameters.file_name}_{self.run_number}.nxs"
63
+ )
64
+ self.master_file: Path = (
65
+ self.directory / f"{parameters.file_name}_{self.run_number}_master.h5"
66
+ )
67
+ self.goniometer: Goniometer = create_goniometer_axes(
68
+ omega_start_deg,
69
+ self.scan_points,
70
+ chi=chi_start_deg,
71
+ phi=phi_start_deg,
72
+ rotation_direction=rotation_direction,
73
+ )
74
+
75
+ def create_nexus_file(self, bit_depth: DTypeLike):
76
+ """
77
+ Creates a nexus file based on the parameters supplied when this object was
78
+ initialised.
79
+ """
80
+ start_time, est_end_time = get_start_and_predicted_end_time(
81
+ self.detector.exp_time * self.full_num_of_images
82
+ )
83
+
84
+ assert self.beam is not None
85
+ assert self.attenuator is not None
86
+
87
+ vds_shape = self.data_shape
88
+
89
+ for filename in [self.nexus_file, self.master_file]:
90
+ NXmx_Writer = NXmxFileWriter(
91
+ filename,
92
+ self.goniometer,
93
+ self.detector,
94
+ self.source,
95
+ self.beam,
96
+ self.attenuator,
97
+ self.full_num_of_images,
98
+ )
99
+ NXmx_Writer.write(
100
+ image_filename=f"{self.data_filename}",
101
+ start_time=start_time,
102
+ est_end_time=est_end_time,
103
+ )
104
+ NXmx_Writer.write_vds(
105
+ vds_offset=self.start_index, vds_shape=vds_shape, vds_dtype=bit_depth
106
+ )
107
+
108
+ def get_image_datafiles(self, max_images_per_file=1000):
109
+ return [
110
+ self.directory / f"{self.data_filename}_{h5_num + 1:06}.h5"
111
+ for h5_num in range(
112
+ math.ceil(self.full_num_of_images / max_images_per_file)
113
+ )
114
+ ]
@@ -0,0 +1,99 @@
1
+ import logging
2
+ from logging.handlers import TimedRotatingFileHandler
3
+ from os import environ
4
+ from pathlib import Path
5
+
6
+ from dodal.log import (
7
+ ERROR_LOG_BUFFER_LINES,
8
+ CircularMemoryHandler,
9
+ DodalLogHandlers,
10
+ integrate_bluesky_and_ophyd_logging,
11
+ set_up_all_logging_handlers,
12
+ )
13
+ from dodal.log import LOGGER as dodal_logger
14
+
15
+ from mx_bluesky.hyperion.parameters.constants import CONST
16
+
17
+ LOGGER = logging.getLogger("Hyperion")
18
+ LOGGER.setLevel("DEBUG")
19
+ LOGGER.parent = dodal_logger
20
+ __logger_handlers: DodalLogHandlers | None = None
21
+
22
+ ISPYB_LOGGER = logging.getLogger("Hyperion ISPyB and Zocalo callbacks")
23
+ ISPYB_LOGGER.setLevel(logging.DEBUG)
24
+
25
+ NEXUS_LOGGER = logging.getLogger("Hyperion NeXus callbacks")
26
+ NEXUS_LOGGER.setLevel(logging.DEBUG)
27
+
28
+ ALL_LOGGERS = [LOGGER, ISPYB_LOGGER, NEXUS_LOGGER]
29
+
30
+
31
+ class ExperimentMetadataTagFilter(logging.Filter):
32
+ dc_group_id: str | None = None
33
+ run_uid: str | None = None
34
+
35
+ def filter(self, record):
36
+ if self.dc_group_id:
37
+ record.dc_group_id = self.dc_group_id
38
+ if self.run_uid:
39
+ record.run_uid = self.run_uid
40
+ return True
41
+
42
+
43
+ tag_filter = ExperimentMetadataTagFilter()
44
+
45
+
46
+ def set_dcgid_tag(dcgid):
47
+ """Set the datacollection group id as a tag on all subsequent log messages.
48
+ Setting to None will remove the tag."""
49
+ tag_filter.dc_group_id = dcgid
50
+
51
+
52
+ def set_uid_tag(uid):
53
+ tag_filter.run_uid = uid
54
+
55
+
56
+ def do_default_logging_setup(dev_mode=False):
57
+ handlers = set_up_all_logging_handlers(
58
+ dodal_logger,
59
+ _get_logging_dir(),
60
+ "hyperion.log",
61
+ dev_mode,
62
+ ERROR_LOG_BUFFER_LINES,
63
+ CONST.GRAYLOG_PORT,
64
+ )
65
+ integrate_bluesky_and_ophyd_logging(dodal_logger)
66
+ handlers["graylog_handler"].addFilter(tag_filter)
67
+
68
+ global __logger_handlers
69
+ __logger_handlers = handlers
70
+
71
+
72
+ def _get_debug_handler() -> CircularMemoryHandler:
73
+ assert (
74
+ __logger_handlers is not None
75
+ ), "You can only use this after running the default logging setup"
76
+ return __logger_handlers["debug_memory_handler"]
77
+
78
+
79
+ def flush_debug_handler() -> str:
80
+ """Writes the contents of the circular debug log buffer to disk and returns the written filename"""
81
+ handler = _get_debug_handler()
82
+ assert isinstance(
83
+ handler.target, TimedRotatingFileHandler
84
+ ), "Circular memory handler doesn't have an appropriate fileHandler target"
85
+ handler.flush()
86
+ return handler.target.baseFilename
87
+
88
+
89
+ def _get_logging_dir() -> Path:
90
+ """Get the path to write the hyperion log files to.
91
+
92
+ If the HYPERION_LOG_DIR environment variable exists then logs will be put in here.
93
+ If no environment variable is found it will default it to the ./tmp/dev directory.
94
+
95
+ Returns:
96
+ logging_path (Path): Path to the log file for the file handler to write to.
97
+ """
98
+ logging_path = Path(environ.get("HYPERION_LOG_DIR") or "./tmp/dev/")
99
+ return logging_path
@@ -0,0 +1,2 @@
1
+ """This module handles the translation between externally supplied parameters and the
2
+ internal parameter model."""