mx-bluesky 1.4.5__py3-none-any.whl → 1.4.7__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 (90) hide show
  1. mx_bluesky/_version.py +9 -4
  2. mx_bluesky/beamlines/aithre_lasershaping/__init__.py +13 -0
  3. mx_bluesky/beamlines/aithre_lasershaping/check_goniometer_performance.py +29 -0
  4. mx_bluesky/beamlines/aithre_lasershaping/goniometer_controls.py +18 -0
  5. mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +45 -28
  6. mx_bluesky/beamlines/i04/thawing_plan.py +19 -14
  7. mx_bluesky/beamlines/i24/serial/__init__.py +14 -0
  8. mx_bluesky/beamlines/i24/serial/dcid.py +3 -1
  9. mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +12 -12
  10. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +31 -30
  11. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +16 -14
  12. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +19 -21
  13. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +11 -4
  14. mx_bluesky/beamlines/i24/serial/parameters/constants.py +1 -1
  15. mx_bluesky/beamlines/i24/serial/set_visit_directory.sh +1 -1
  16. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +16 -16
  17. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +48 -49
  18. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +2 -2
  19. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +11 -9
  20. mx_bluesky/beamlines/i24/serial/web_gui_plans/general_plans.py +109 -0
  21. mx_bluesky/beamlines/i24/serial/write_nexus.py +5 -4
  22. mx_bluesky/common/device_setup_plans/xbpm_feedback.py +45 -0
  23. mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +2 -4
  24. mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +1 -1
  25. mx_bluesky/common/external_interaction/callbacks/common/plan_reactive_callback.py +2 -2
  26. mx_bluesky/common/external_interaction/callbacks/common/zocalo_callback.py +18 -15
  27. mx_bluesky/common/external_interaction/callbacks/sample_handling/__init__.py +0 -0
  28. mx_bluesky/{hyperion → common}/external_interaction/callbacks/sample_handling/sample_handling_callback.py +29 -12
  29. mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py +43 -7
  30. mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_mapping.py +1 -1
  31. mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py +2 -1
  32. mx_bluesky/common/external_interaction/ispyb/data_model.py +1 -0
  33. mx_bluesky/common/external_interaction/ispyb/exp_eye_store.py +6 -2
  34. mx_bluesky/common/external_interaction/ispyb/ispyb_store.py +21 -1
  35. mx_bluesky/common/external_interaction/nexus/nexus_utils.py +1 -1
  36. mx_bluesky/common/parameters/constants.py +3 -1
  37. mx_bluesky/common/parameters/gridscan.py +36 -1
  38. mx_bluesky/common/plans/do_fgs.py +4 -6
  39. mx_bluesky/common/plans/read_hardware.py +78 -0
  40. mx_bluesky/common/plans/write_sample_status.py +46 -0
  41. mx_bluesky/common/preprocessors/__init__.py +0 -0
  42. mx_bluesky/common/preprocessors/preprocessors.py +105 -0
  43. mx_bluesky/common/protocols/__init__.py +0 -0
  44. mx_bluesky/common/protocols/protocols.py +10 -0
  45. mx_bluesky/common/utils/context.py +68 -0
  46. mx_bluesky/{hyperion/experiment_plans/common → common}/xrc_result.py +16 -0
  47. mx_bluesky/hyperion/__main__.py +7 -9
  48. mx_bluesky/hyperion/baton_handler.py +84 -0
  49. mx_bluesky/hyperion/device_setup_plans/setup_oav.py +5 -5
  50. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +5 -1
  51. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +2 -2
  52. mx_bluesky/hyperion/device_setup_plans/smargon.py +6 -6
  53. mx_bluesky/hyperion/device_setup_plans/utils.py +2 -2
  54. mx_bluesky/hyperion/experiment_plans/__init__.py +0 -4
  55. mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py +12 -31
  56. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +0 -7
  57. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +44 -97
  58. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +6 -6
  59. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +8 -6
  60. mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +11 -11
  61. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +5 -5
  62. mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +1 -1
  63. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +2 -4
  64. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +15 -13
  65. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +10 -10
  66. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +1 -29
  67. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +30 -27
  68. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +25 -6
  69. mx_bluesky/hyperion/external_interaction/agamemnon.py +242 -0
  70. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +12 -6
  71. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +1 -1
  72. mx_bluesky/hyperion/external_interaction/callbacks/snapshot_callback.py +107 -0
  73. mx_bluesky/hyperion/external_interaction/config_server.py +6 -6
  74. mx_bluesky/hyperion/parameters/device_composites.py +49 -0
  75. mx_bluesky/hyperion/parameters/gridscan.py +3 -3
  76. mx_bluesky/hyperion/parameters/rotation.py +1 -1
  77. mx_bluesky/hyperion/utils/__init__.py +1 -0
  78. mx_bluesky/hyperion/utils/context.py +0 -65
  79. mx_bluesky/hyperion/utils/validation.py +3 -3
  80. {mx_bluesky-1.4.5.dist-info → mx_bluesky-1.4.7.dist-info}/METADATA +6 -5
  81. {mx_bluesky-1.4.5.dist-info → mx_bluesky-1.4.7.dist-info}/RECORD +86 -72
  82. {mx_bluesky-1.4.5.dist-info → mx_bluesky-1.4.7.dist-info}/WHEEL +1 -1
  83. {mx_bluesky-1.4.5.dist-info → mx_bluesky-1.4.7.dist-info}/entry_points.txt +1 -0
  84. mx_bluesky/common/device_setup_plans/read_hardware_for_setup.py +0 -14
  85. mx_bluesky/common/external_interaction/callbacks/common/aperture_change_callback.py +0 -22
  86. mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py +0 -54
  87. mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +0 -103
  88. /mx_bluesky/{hyperion/external_interaction/callbacks/sample_handling → beamlines/i24/serial/web_gui_plans}/__init__.py +0 -0
  89. {mx_bluesky-1.4.5.dist-info → mx_bluesky-1.4.7.dist-info/licenses}/LICENSE +0 -0
  90. {mx_bluesky-1.4.5.dist-info → mx_bluesky-1.4.7.dist-info}/top_level.txt +0 -0
@@ -77,7 +77,7 @@ class StoreInIspyb:
77
77
  scan_data_infos,
78
78
  ) -> IspybIds:
79
79
  with ispyb.open(self.ISPYB_CONFIG_PATH) as conn:
80
- assert conn is not None, "Failed to connect to ISPyB"
80
+ assert conn, "Failed to connect to ISPyB"
81
81
  if data_collection_group_info:
82
82
  ispyb_ids.data_collection_group_id = (
83
83
  self._store_data_collection_group_table(
@@ -152,6 +152,19 @@ class StoreInIspyb:
152
152
  data_collection_id, comment, delimiter
153
153
  )
154
154
 
155
+ def update_data_collection_group_table(
156
+ self,
157
+ dcg_info: DataCollectionGroupInfo,
158
+ data_collection_group_id: int | None = None,
159
+ ) -> None:
160
+ with ispyb.open(self.ISPYB_CONFIG_PATH) as conn:
161
+ assert conn is not None, "Failed to connect to ISPyB!"
162
+ self._store_data_collection_group_table(
163
+ conn,
164
+ dcg_info,
165
+ data_collection_group_id,
166
+ )
167
+
155
168
  def _update_scan_with_end_time_and_status(
156
169
  self,
157
170
  end_time: str,
@@ -206,9 +219,16 @@ class StoreInIspyb:
206
219
  def _store_data_collection_table(
207
220
  self, conn, data_collection_id, data_collection_info
208
221
  ):
222
+ if data_collection_id and data_collection_info.comments:
223
+ self.append_to_comment(
224
+ data_collection_id, data_collection_info.comments, " "
225
+ )
226
+ data_collection_info.comments = None
227
+
209
228
  params = self._fill_common_data_collection_params(
210
229
  conn, data_collection_id, data_collection_info
211
230
  )
231
+
212
232
  return self._upsert_data_collection(conn, params)
213
233
 
214
234
  def _store_single_scan_data(
@@ -144,7 +144,7 @@ def create_detector_parameters(detector_params: DetectorParams) -> Detector:
144
144
  list(
145
145
  detector_params.get_beam_position_pixels(detector_params.detector_distance)
146
146
  ),
147
- detector_params.exposure_time,
147
+ detector_params.exposure_time_s,
148
148
  [(-1.0, 0.0, 0.0), (0.0, -1.0, 0.0)],
149
149
  )
150
150
 
@@ -58,6 +58,8 @@ class PlanNameConstants:
58
58
  ROTATION_OUTER = "rotation_scan_with_cleanup"
59
59
  ROTATION_MAIN = "rotation_scan_main"
60
60
  FLYSCAN_RESULTS = "xray_centre_results"
61
+ SET_ENERGY = "set_energy"
62
+ UNNAMED_RUN = "unnamed_run"
61
63
 
62
64
 
63
65
  @dataclass(frozen=True)
@@ -130,7 +132,7 @@ class PlanGroupCheckpointConstants:
130
132
  class DeviceSettingsConstants:
131
133
  PANDA_FLYSCAN_SETTINGS_FILENAME = "panda-gridscan"
132
134
  PANDA_FLYSCAN_SETTINGS_DIR = os.path.abspath(
133
- f"{ROOT_DIR}/hyperion/resources/panda/{PANDA_FLYSCAN_SETTINGS_FILENAME}"
135
+ f"{ROOT_DIR}/hyperion/resources/panda/"
134
136
  )
135
137
 
136
138
 
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dodal.devices.aperturescatterguard import ApertureValue
4
+ from dodal.devices.detector.det_dim_constants import EIGER2_X_9M_SIZE, EIGER2_X_16M_SIZE
5
+ from dodal.devices.detector.detector import DetectorParams
4
6
  from dodal.devices.fast_grid_scan import (
5
7
  ZebraGridScanParams,
6
8
  )
9
+ from dodal.utils import get_beamline_name
7
10
  from pydantic import Field, PrivateAttr
8
11
  from scanspec.core import Path as ScanPath
9
12
  from scanspec.specs import Line, Static
@@ -18,11 +21,12 @@ from mx_bluesky.common.parameters.components import (
18
21
  XyzStarts,
19
22
  )
20
23
  from mx_bluesky.common.parameters.constants import (
24
+ DetectorParamConstants,
21
25
  GridscanParamConstants,
22
26
  HardwareConstants,
23
27
  )
24
28
 
25
- """Parameter models in this file are abstract. They should be inherited by a top-level model"""
29
+ DETECTOR_SIZE_PER_BEAMLINE = {"i02-1": EIGER2_X_9M_SIZE, "dev": EIGER2_X_16M_SIZE}
26
30
 
27
31
 
28
32
  class GridCommon(
@@ -146,3 +150,34 @@ class SpecifiedThreeDGridScan(
146
150
  @property
147
151
  def num_images(self) -> int:
148
152
  return len(self.scan_points["sam_x"])
153
+
154
+ @property
155
+ def detector_params(self):
156
+ self.det_dist_to_beam_converter_path = (
157
+ self.det_dist_to_beam_converter_path
158
+ or DetectorParamConstants.BEAM_XY_LUT_PATH
159
+ )
160
+ optional_args = {}
161
+ if self.run_number:
162
+ optional_args["run_number"] = self.run_number
163
+ assert self.detector_distance_mm is not None, (
164
+ "Detector distance must be filled before generating DetectorParams"
165
+ )
166
+ return DetectorParams(
167
+ detector_size_constants=DETECTOR_SIZE_PER_BEAMLINE[
168
+ get_beamline_name("dev")
169
+ ],
170
+ expected_energy_ev=self.demand_energy_ev,
171
+ exposure_time_s=self.exposure_time_s,
172
+ directory=self.storage_directory,
173
+ prefix=self.file_name,
174
+ detector_distance=self.detector_distance_mm,
175
+ omega_start=self.omega_start_deg or 0,
176
+ omega_increment=0,
177
+ num_images_per_trigger=1,
178
+ num_triggers=self.num_images,
179
+ use_roi_mode=self.use_roi_mode,
180
+ det_dist_to_beam_converter_path=self.det_dist_to_beam_converter_path,
181
+ trigger_mode=self.trigger_mode,
182
+ **optional_args,
183
+ )
@@ -14,12 +14,10 @@ from dodal.log import LOGGER
14
14
  from dodal.plan_stubs.check_topup import check_topup_and_wait_if_necessary
15
15
  from scanspec.core import AxesPoints, Axis
16
16
 
17
- from mx_bluesky.common.device_setup_plans.read_hardware_for_setup import (
18
- read_hardware_for_zocalo,
19
- )
20
17
  from mx_bluesky.common.parameters.constants import (
21
18
  PlanNameConstants,
22
19
  )
20
+ from mx_bluesky.common.plans.read_hardware import read_hardware_for_zocalo
23
21
  from mx_bluesky.common.utils.tracing import TRACER
24
22
 
25
23
 
@@ -30,7 +28,7 @@ def _wait_for_zocalo_to_stage_then_do_fgs(
30
28
  during_collection_plan: Callable[[], MsgGenerator] | None = None,
31
29
  ):
32
30
  expected_images = yield from bps.rd(grid_scan_device.expected_images)
33
- exposure_sec_per_image = yield from bps.rd(detector.cam.acquire_time) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
31
+ exposure_sec_per_image = yield from bps.rd(detector.cam.acquire_time) # type: ignore # Fix types in ophyd-async (https://github.com/DiamondLightSource/mx-bluesky/issues/855)
34
32
  LOGGER.info("waiting for topup if necessary...")
35
33
  yield from check_topup_and_wait_if_necessary(
36
34
  synchrotron,
@@ -101,8 +99,8 @@ def kickoff_and_complete_gridscan(
101
99
  }
102
100
  )
103
101
  @bpp.contingency_decorator(
104
- except_plan=lambda e: (yield from bps.stop(detector)), # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
105
- else_plan=lambda: (yield from bps.unstage(detector)), # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
102
+ except_plan=lambda e: (yield from bps.stop(detector)), # type: ignore # Fix types in ophyd-async (https://github.com/DiamondLightSource/mx-bluesky/issues/855)
103
+ else_plan=lambda: (yield from bps.unstage(detector)),
106
104
  )
107
105
  def _decorated_do_fgs():
108
106
  yield from _wait_for_zocalo_to_stage_then_do_fgs(
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ import bluesky.plan_stubs as bps
4
+ from bluesky.protocols import Readable
5
+ from dodal.devices.aperturescatterguard import ApertureScatterguard
6
+ from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
7
+ from dodal.devices.dcm import DCM
8
+ from dodal.devices.eiger import EigerDetector
9
+ from dodal.devices.flux import Flux
10
+ from dodal.devices.s4_slit_gaps import S4SlitGaps
11
+ from dodal.devices.smargon import Smargon
12
+ from dodal.devices.synchrotron import Synchrotron
13
+ from dodal.devices.undulator import Undulator
14
+
15
+ from mx_bluesky.common.parameters.constants import (
16
+ DocDescriptorNames,
17
+ )
18
+ from mx_bluesky.common.utils.log import LOGGER
19
+
20
+
21
+ def read_hardware_plan(
22
+ signals: list[Readable],
23
+ event_name: str,
24
+ ):
25
+ LOGGER.info(f"Reading status of beamline for event, {event_name}")
26
+ yield from bps.create(name=event_name)
27
+ for signal in signals:
28
+ yield from bps.read(signal)
29
+ yield from bps.save()
30
+
31
+
32
+ def read_hardware_for_zocalo(detector: EigerDetector):
33
+ """ "
34
+ If the RunEngine is subscribed to the ZocaloCallback, this plan will also trigger zocalo.
35
+ """
36
+ yield from read_hardware_plan(
37
+ [detector.odin.file_writer.id], # type: ignore
38
+ DocDescriptorNames.ZOCALO_HW_READ,
39
+ )
40
+
41
+
42
+ def standard_read_hardware_pre_collection(
43
+ undulator: Undulator,
44
+ synchrotron: Synchrotron,
45
+ s4_slit_gaps: S4SlitGaps,
46
+ dcm: DCM,
47
+ smargon: Smargon,
48
+ ):
49
+ LOGGER.info("Reading status of beamline for callbacks, pre collection.")
50
+ signals_to_read_pre_flyscan = [
51
+ undulator.current_gap,
52
+ synchrotron.synchrotron_mode,
53
+ s4_slit_gaps,
54
+ smargon,
55
+ dcm.energy_in_kev,
56
+ ]
57
+ yield from read_hardware_plan(
58
+ signals_to_read_pre_flyscan, DocDescriptorNames.HARDWARE_READ_PRE
59
+ )
60
+
61
+
62
+ def standard_read_hardware_during_collection(
63
+ aperture_scatterguard: ApertureScatterguard,
64
+ attenuator: BinaryFilterAttenuator,
65
+ flux: Flux,
66
+ dcm: DCM,
67
+ detector: EigerDetector,
68
+ ):
69
+ signals_to_read_during_collection = [
70
+ aperture_scatterguard,
71
+ attenuator.actual_transmission,
72
+ flux.flux_reading,
73
+ dcm.energy_in_kev,
74
+ detector.bit_depth,
75
+ ]
76
+ yield from read_hardware_plan(
77
+ signals_to_read_during_collection, DocDescriptorNames.HARDWARE_READ_DURING
78
+ )
@@ -0,0 +1,46 @@
1
+ from enum import StrEnum
2
+
3
+ import bluesky.plan_stubs as bps
4
+ import bluesky.preprocessors as bpp
5
+
6
+ from mx_bluesky.common.external_interaction.callbacks.sample_handling.sample_handling_callback import (
7
+ SampleHandlingCallback,
8
+ )
9
+ from mx_bluesky.common.utils.exceptions import SampleException
10
+
11
+
12
+ class SampleStatusExceptionType(StrEnum):
13
+ BEAMLINE = "Beamline"
14
+ SAMPLE = "Sample"
15
+
16
+
17
+ @bpp.subs_decorator(SampleHandlingCallback())
18
+ def deposit_sample_error(exception_type: SampleStatusExceptionType, sample_id: int):
19
+ @bpp.run_decorator(
20
+ md={
21
+ "metadata": {"sample_id": sample_id},
22
+ "activate_callbacks": ["SampleHandlingCallback"],
23
+ }
24
+ )
25
+ def _inner():
26
+ yield from bps.null()
27
+ if exception_type == SampleStatusExceptionType.BEAMLINE:
28
+ raise AssertionError()
29
+ elif exception_type == SampleStatusExceptionType.SAMPLE:
30
+ raise SampleException
31
+
32
+ yield from _inner()
33
+
34
+
35
+ @bpp.subs_decorator(SampleHandlingCallback(record_loaded_on_success=True))
36
+ def deposit_loaded_sample(sample_id: int):
37
+ @bpp.run_decorator(
38
+ md={
39
+ "metadata": {"sample_id": sample_id},
40
+ "activate_callbacks": ["SampleHandlingCallback"],
41
+ }
42
+ )
43
+ def _inner():
44
+ yield from bps.null()
45
+
46
+ yield from _inner()
File without changes
@@ -0,0 +1,105 @@
1
+ from bluesky import preprocessors as bpp
2
+ from bluesky.preprocessors import plan_mutator
3
+ from bluesky.utils import Msg, MsgGenerator, make_decorator
4
+
5
+ from mx_bluesky.common.device_setup_plans.xbpm_feedback import (
6
+ check_and_pause_feedback,
7
+ unpause_xbpm_feedback_and_set_transmission_to_1,
8
+ )
9
+ from mx_bluesky.common.parameters.constants import PlanNameConstants
10
+ from mx_bluesky.common.protocols.protocols import (
11
+ XBPMPauseDevices,
12
+ )
13
+
14
+
15
+ def transmission_and_xbpm_feedback_for_collection_wrapper(
16
+ plan: MsgGenerator,
17
+ devices: XBPMPauseDevices,
18
+ desired_transmission_fraction: float,
19
+ run_key_to_wrap: PlanNameConstants | None = None,
20
+ ):
21
+ """
22
+ Sets the transmission for the data collection, ensuring the xbpm feedback is valid, then resets it immediately after
23
+ the run has finished.
24
+
25
+ This wrapper should be attached to the entry point of any beamline-specific plan that may disrupt the XBPM feedback,
26
+ such as a data collection or an x-ray center grid scan.
27
+ This wrapper will do nothing if no runs are seen.
28
+
29
+ XBPM feedback isn't reliable during collections due to:
30
+ * Objects (e.g. attenuator) crossing the beam can cause large (incorrect) feedback movements
31
+ * Lower transmissions/higher energies are less reliable for the xbpm
32
+
33
+ So we need to keep the transmission at 100% and the feedback on when not collecting
34
+ and then turn it off and set the correct transmission for collection. The feedback
35
+ mostly accounts for slow thermal drift so it is safe to assume that the beam is
36
+ stable during a collection.
37
+
38
+ Args:
39
+ plan: The plan performing the data collection.
40
+ devices (XBPMPauseDevices): Composite device including The XBPM device that is responsible for keeping
41
+ the beam in position, and attenuator
42
+ desired_transmission_fraction (float): The desired transmission for the collection
43
+ run_key_to_wrap: (str | None): Pausing XBPM and setting transmission is inserted after the 'open_run' message is seen with
44
+ the matching run key, and unpausing and resetting transmission is inserted after the corresponding 'close_run' message is
45
+ seen. If not specified, instead wrap the first run encountered.
46
+ """
47
+
48
+ _wrapped_run_name: None | str = None
49
+
50
+ def head(msg: Msg):
51
+ yield from check_and_pause_feedback(
52
+ devices.xbpm_feedback,
53
+ devices.attenuator,
54
+ desired_transmission_fraction,
55
+ )
56
+
57
+ # Allow 'open_run' message to pass through
58
+ yield msg
59
+
60
+ def tail():
61
+ yield from unpause_xbpm_feedback_and_set_transmission_to_1(
62
+ devices.xbpm_feedback, devices.attenuator
63
+ )
64
+
65
+ def insert_plans(msg: Msg):
66
+ # Wrap the specified run, or, if none specified, wrap the first run encountered
67
+ nonlocal _wrapped_run_name
68
+
69
+ match msg.command:
70
+ case "open_run":
71
+ # If we specified a run key, did we encounter it
72
+ # If we didn't specify, then insert the plans and track the name of the run
73
+ if (
74
+ not (run_key_to_wrap or _wrapped_run_name)
75
+ or run_key_to_wrap is msg.run
76
+ ):
77
+ _wrapped_run_name = msg.run if msg.run else "unnamed_run"
78
+ return head(msg), None
79
+ case "close_run":
80
+ # Check if the run tracked from above was closed
81
+ # An exception is raised in the RunEngine if two unnamed runs are opened
82
+ # at the same time, so we are safe from unpausing on the wrong run
83
+ if (_wrapped_run_name == "unnamed_run" and not msg.run) or (
84
+ msg.run and _wrapped_run_name and _wrapped_run_name is msg.run
85
+ ):
86
+ return None, tail()
87
+
88
+ return None, None
89
+
90
+ # Contingency wrapper can cause unpausing to occur on exception and again on close_run.
91
+ # Not needed after https://github.com/bluesky/bluesky/issues/1891
92
+ return (
93
+ yield from bpp.contingency_wrapper(
94
+ plan_mutator(plan, insert_plans),
95
+ except_plan=lambda _: unpause_xbpm_feedback_and_set_transmission_to_1(
96
+ devices.xbpm_feedback,
97
+ devices.attenuator,
98
+ ),
99
+ )
100
+ )
101
+
102
+
103
+ transmission_and_xbpm_feedback_for_collection_decorator = make_decorator(
104
+ transmission_and_xbpm_feedback_for_collection_wrapper
105
+ )
File without changes
@@ -0,0 +1,10 @@
1
+ from typing import Protocol, runtime_checkable
2
+
3
+ from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
4
+ from dodal.devices.xbpm_feedback import XBPMFeedback
5
+
6
+
7
+ @runtime_checkable
8
+ class XBPMPauseDevices(Protocol):
9
+ xbpm_feedback: XBPMFeedback
10
+ attenuator: BinaryFilterAttenuator
@@ -0,0 +1,68 @@
1
+ import dataclasses
2
+ from typing import Any, ClassVar, Protocol, TypeVar, get_type_hints
3
+
4
+ from blueapi.core import BlueskyContext
5
+ from blueapi.core.bluesky_types import Device
6
+
7
+ from mx_bluesky.common.utils.log import LOGGER
8
+
9
+ T = TypeVar("T", bound=Device)
10
+
11
+
12
+ class _IsDataclass(Protocol):
13
+ """Protocol followed by any dataclass"""
14
+
15
+ __dataclass_fields__: ClassVar[dict]
16
+
17
+
18
+ DT = TypeVar("DT", bound=_IsDataclass)
19
+
20
+
21
+ def find_device_in_context(
22
+ context: BlueskyContext,
23
+ name: str,
24
+ # Typing in here is wrong (see https://github.com/microsoft/pyright/issues/7228#issuecomment-1934500232)
25
+ # but this whole thing will go away when we do https://github.com/DiamondLightSource/hyperion/issues/868
26
+ expected_type: type[T] = Device, # type: ignore
27
+ ) -> T:
28
+ LOGGER.debug(f"Looking for device {name} of type {expected_type} in context")
29
+
30
+ device = context.find_device(name)
31
+ if device is None:
32
+ raise ValueError(
33
+ f"Cannot find device named '{name}' in bluesky context {context.devices}."
34
+ )
35
+
36
+ if not isinstance(device, expected_type):
37
+ raise ValueError(
38
+ f"Found device named '{name}' and expected it to be a '{expected_type}' but it was a '{device.__class__.__name__}'"
39
+ )
40
+
41
+ LOGGER.debug(f"Found matching device {device}")
42
+ return device
43
+
44
+
45
+ def device_composite_from_context(context: BlueskyContext, dc: type[DT]) -> DT:
46
+ """
47
+ Initializes all of the devices referenced in a given dataclass from a provided
48
+ context, checking that the types of devices returned by the context are compatible
49
+ with the type annotations of the dataclass.
50
+
51
+ Note that if the context was not created with `wait_for_connection=True` devices may
52
+ still be unconnected.
53
+ """
54
+ LOGGER.debug(
55
+ f"Attempting to initialize devices referenced in dataclass {dc} from blueapi context"
56
+ )
57
+
58
+ devices: dict[str, Any] = {}
59
+ dc_type_hints: dict[str, Any] = get_type_hints(dc)
60
+
61
+ for field in dataclasses.fields(dc):
62
+ device = find_device_in_context(
63
+ context, field.name, expected_type=dc_type_hints.get(field.name, Device)
64
+ )
65
+
66
+ devices[field.name] = device
67
+
68
+ return dc(**devices)
@@ -5,6 +5,8 @@ from collections.abc import Callable, Sequence
5
5
  from functools import partial
6
6
 
7
7
  import numpy as np
8
+ from bluesky.callbacks import CallbackBase
9
+ from event_model import RunStart
8
10
 
9
11
  from mx_bluesky.common.parameters.components import (
10
12
  MultiXtalSelection,
@@ -12,6 +14,20 @@ from mx_bluesky.common.parameters.components import (
12
14
  )
13
15
 
14
16
 
17
+ class XRayCentreEventHandler(CallbackBase):
18
+ def __init__(self):
19
+ super().__init__()
20
+ self.xray_centre_results: Sequence[XRayCentreResult] | None = None
21
+
22
+ def start(self, doc: RunStart) -> RunStart | None:
23
+ if "xray_centre_results" in doc:
24
+ self.xray_centre_results = [
25
+ XRayCentreResult(**result_dict)
26
+ for result_dict in doc["xray_centre_results"] # type: ignore
27
+ ]
28
+ return doc
29
+
30
+
15
31
  @dataclasses.dataclass
16
32
  class XRayCentreResult:
17
33
  """
@@ -16,9 +16,6 @@ from flask import Flask, request
16
16
  from flask_restful import Api, Resource
17
17
  from pydantic.dataclasses import dataclass
18
18
 
19
- from mx_bluesky.common.external_interaction.callbacks.common.aperture_change_callback import (
20
- ApertureChangeCallback,
21
- )
22
19
  from mx_bluesky.common.external_interaction.callbacks.common.log_uid_tag_callback import (
23
20
  LogUidTaggingCallback,
24
21
  )
@@ -38,6 +35,10 @@ from mx_bluesky.hyperion.experiment_plans.experiment_registry import (
38
35
  PLAN_REGISTRY,
39
36
  PlanNotFound,
40
37
  )
38
+ from mx_bluesky.hyperion.external_interaction.agamemnon import (
39
+ compare_params,
40
+ update_params_from_agamemnon,
41
+ )
41
42
  from mx_bluesky.hyperion.parameters.cli import parse_cli_args
42
43
  from mx_bluesky.hyperion.parameters.constants import CONST
43
44
  from mx_bluesky.hyperion.utils.context import setup_context
@@ -86,13 +87,11 @@ class BlueskyRunner:
86
87
  self.command_queue: Queue[Command] = Queue()
87
88
  self.current_status: StatusAndMessage = StatusAndMessage(Status.IDLE)
88
89
  self.last_run_aborted: bool = False
89
- self.aperture_change_callback = ApertureChangeCallback()
90
90
  self.logging_uid_tag_callback = LogUidTaggingCallback()
91
91
  self.context: BlueskyContext
92
92
 
93
93
  self.RE = RE
94
94
  self.context = context
95
- RE.subscribe(self.aperture_change_callback)
96
95
  RE.subscribe(self.logging_uid_tag_callback)
97
96
 
98
97
  LOGGER.info("Connecting to external callback ZMQ proxy...")
@@ -172,10 +171,7 @@ class BlueskyRunner:
172
171
  with TRACER.start_span("do_run"):
173
172
  self.RE(command.experiment(command.devices, command.parameters))
174
173
 
175
- self.current_status = StatusAndMessage(
176
- Status.IDLE,
177
- self.aperture_change_callback.last_selected_aperture,
178
- )
174
+ self.current_status = StatusAndMessage(Status.IDLE)
179
175
 
180
176
  self.last_run_aborted = False
181
177
  except WarningException as exception:
@@ -208,6 +204,8 @@ def compose_start_args(context: BlueskyContext, plan_name: str, action: Actions)
208
204
  )
209
205
  try:
210
206
  parameters = experiment_internal_param_type(**json.loads(request.data))
207
+ parameters = update_params_from_agamemnon(parameters)
208
+ compare_params(parameters)
211
209
  if parameters.model_extra:
212
210
  raise ValueError(f"Extra fields not allowed {parameters.model_extra}")
213
211
  except Exception as e:
@@ -0,0 +1,84 @@
1
+ from bluesky import plan_stubs as bps
2
+ from bluesky import preprocessors as bpp
3
+ from dodal.devices.baton import Baton
4
+
5
+ from mx_bluesky.common.utils.exceptions import WarningException
6
+ from mx_bluesky.hyperion.experiment_plans.load_centre_collect_full_plan import (
7
+ LoadCentreCollectComposite,
8
+ load_centre_collect_full,
9
+ )
10
+ from mx_bluesky.hyperion.external_interaction.agamemnon import (
11
+ create_parameters_from_agamemnon,
12
+ )
13
+ from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
14
+
15
+ HYPERION_USER = "Hyperion"
16
+ NO_USER = "None"
17
+
18
+
19
+ def wait_for_hyperion_requested(baton: Baton):
20
+ SLEEP_PER_CHECK = 0.1
21
+ while True:
22
+ requested_user = yield from bps.rd(baton.requested_user)
23
+ if requested_user == HYPERION_USER:
24
+ break
25
+ yield from bps.sleep(SLEEP_PER_CHECK)
26
+
27
+
28
+ def ignore_sample_errors(exception: Exception):
29
+ yield from bps.null()
30
+ # For sample errors we want to continue the loop
31
+ if not isinstance(exception, WarningException):
32
+ raise exception
33
+
34
+
35
+ def main_hyperion_loop(baton: Baton, composite: LoadCentreCollectComposite):
36
+ requested_user = yield from bps.rd(baton.requested_user)
37
+ while requested_user == HYPERION_USER:
38
+
39
+ def inner_loop():
40
+ parameters: LoadCentreCollect | None = create_parameters_from_agamemnon() # type: ignore # not complete until https://github.com/DiamondLightSource/mx-bluesky/issues/773
41
+ if parameters:
42
+ yield from load_centre_collect_full(composite, parameters)
43
+ else:
44
+ yield from bps.mv(baton.requested_user, NO_USER)
45
+
46
+ yield from bpp.contingency_wrapper(
47
+ inner_loop(), except_plan=ignore_sample_errors, auto_raise=False
48
+ )
49
+ requested_user = yield from bps.rd(baton.requested_user)
50
+
51
+
52
+ def move_to_default_state():
53
+ # To be filled in in https://github.com/DiamondLightSource/mx-bluesky/issues/396
54
+ yield from bps.null()
55
+
56
+
57
+ def run_udc_when_requested(baton: Baton, composite: LoadCentreCollectComposite):
58
+ """This will wait for the baton to be handed to hyperion and then run through the
59
+ UDC queue from agamemnon until:
60
+ 1. There are no more instructions from agamemnon
61
+ 2. There is an error on the beamline
62
+ 3. The baton is requested by another party
63
+
64
+ In the case of 1. or 2. hyperion will immediately release the baton. In the case of
65
+ 3. the baton will be released after the next collection has finished."""
66
+
67
+ yield from wait_for_hyperion_requested(baton)
68
+ yield from bps.abs_set(baton.current_user, HYPERION_USER)
69
+
70
+ def default_state_then_collect():
71
+ yield from move_to_default_state()
72
+ yield from main_hyperion_loop(baton, composite)
73
+
74
+ def release_baton():
75
+ # If hyperion has given up the baton itself we need to also release requested
76
+ # user so that hyperion doesn't think we're requested again
77
+ requested_user = yield from bps.rd(baton.requested_user)
78
+ if requested_user == HYPERION_USER:
79
+ yield from bps.abs_set(baton.requested_user, NO_USER)
80
+ yield from bps.abs_set(baton.current_user, NO_USER)
81
+
82
+ yield from bpp.contingency_wrapper(
83
+ default_state_then_collect(), final_plan=release_baton
84
+ )
@@ -55,14 +55,14 @@ def setup_pin_tip_detection_params(
55
55
 
56
56
 
57
57
  def setup_general_oav_params(oav: OAV, parameters: OAVParameters):
58
- yield from set_using_group(oav.cam.color_mode, ColorMode.RGB1) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
59
- yield from set_using_group(oav.cam.acquire_period, parameters.acquire_period) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
60
- yield from set_using_group(oav.cam.acquire_time, parameters.exposure) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
61
- yield from set_using_group(oav.cam.gain, parameters.gain) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
58
+ yield from set_using_group(oav.cam.color_mode, ColorMode.RGB1)
59
+ yield from set_using_group(oav.cam.acquire_period, parameters.acquire_period)
60
+ yield from set_using_group(oav.cam.acquire_time, parameters.exposure)
61
+ yield from set_using_group(oav.cam.gain, parameters.gain)
62
62
 
63
63
  zoom_level_str = f"{float(parameters.zoom)}x"
64
64
  yield from bps.abs_set(
65
- oav.zoom_controller, # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
65
+ oav.zoom_controller,
66
66
  zoom_level_str,
67
67
  wait=True,
68
68
  )