mx-bluesky 1.5.14__py3-none-any.whl → 1.5.16__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 (74) hide show
  1. mx_bluesky/Getting started.ipynb +1 -0
  2. mx_bluesky/_version.py +2 -2
  3. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +18 -0
  4. mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +6 -7
  5. mx_bluesky/beamlines/i04/thawing_plan.py +1 -0
  6. mx_bluesky/beamlines/i24/jungfrau_commissioning/__init__.py +13 -0
  7. mx_bluesky/beamlines/i24/jungfrau_commissioning/callbacks/__init__.py +0 -0
  8. mx_bluesky/beamlines/i24/jungfrau_commissioning/callbacks/metadata_writer.py +86 -0
  9. mx_bluesky/beamlines/i24/jungfrau_commissioning/composites.py +35 -0
  10. mx_bluesky/beamlines/i24/jungfrau_commissioning/experiment_plans/do_darks.py +18 -19
  11. mx_bluesky/beamlines/i24/jungfrau_commissioning/experiment_plans/rotation_scan_plan.py +292 -0
  12. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/do_external_acquisition.py +3 -8
  13. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/do_internal_acquisition.py +4 -5
  14. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/plan_utils.py +14 -18
  15. mx_bluesky/beamlines/i24/parameters/__init__.py +0 -0
  16. mx_bluesky/beamlines/i24/parameters/constants.py +9 -0
  17. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_extruder_collect_py3v2.py +1 -1
  18. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +2 -2
  19. mx_bluesky/beamlines/i24/serial/web_gui_plans/oav_plans.py +21 -0
  20. mx_bluesky/common/device_setup_plans/robot_load_unload.py +2 -24
  21. mx_bluesky/common/device_setup_plans/setup_zebra_and_shutter.py +5 -2
  22. mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +1 -1
  23. mx_bluesky/common/experiment_plans/inner_plans/read_hardware.py +2 -0
  24. mx_bluesky/common/experiment_plans/rotation/__init__.py +0 -0
  25. mx_bluesky/common/experiment_plans/rotation/rotation_utils.py +127 -0
  26. mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +14 -2
  27. mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +1 -2
  28. mx_bluesky/common/external_interaction/ispyb/data_model.py +4 -1
  29. mx_bluesky/common/external_interaction/ispyb/exp_eye_store.py +3 -1
  30. mx_bluesky/common/parameters/components.py +17 -7
  31. mx_bluesky/common/parameters/constants.py +6 -0
  32. mx_bluesky/{hyperion → common}/parameters/rotation.py +10 -8
  33. mx_bluesky/common/preprocessors/preprocessors.py +98 -36
  34. mx_bluesky/hyperion/__main__.py +55 -22
  35. mx_bluesky/hyperion/baton_handler.py +24 -64
  36. mx_bluesky/hyperion/blueapi_config.yaml +17 -0
  37. mx_bluesky/hyperion/blueapi_dev_config.yaml +16 -0
  38. mx_bluesky/hyperion/blueapi_plans/__init__.py +96 -0
  39. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +8 -6
  40. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +1 -1
  41. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +3 -1
  42. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +4 -5
  43. mx_bluesky/hyperion/experiment_plans/hyperion_grid_detect_then_xray_centre_plan.py +2 -2
  44. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +3 -1
  45. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +17 -6
  46. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +0 -3
  47. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +12 -126
  48. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +2 -2
  49. mx_bluesky/hyperion/experiment_plans/udc_default_state.py +8 -2
  50. mx_bluesky/hyperion/external_interaction/agamemnon.py +3 -8
  51. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +121 -47
  52. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +3 -1
  53. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +3 -1
  54. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +6 -3
  55. mx_bluesky/hyperion/external_interaction/callbacks/stomp/__init__.py +0 -0
  56. mx_bluesky/hyperion/external_interaction/callbacks/stomp/dispatcher.py +33 -0
  57. mx_bluesky/hyperion/in_process_runner.py +132 -0
  58. mx_bluesky/hyperion/parameters/cli.py +43 -4
  59. mx_bluesky/hyperion/parameters/components.py +13 -0
  60. mx_bluesky/hyperion/parameters/constants.py +2 -9
  61. mx_bluesky/hyperion/parameters/load_centre_collect.py +3 -1
  62. mx_bluesky/hyperion/plan_runner.py +45 -66
  63. mx_bluesky/hyperion/plan_runner_api.py +3 -4
  64. mx_bluesky/hyperion/supervisor/__init__.py +3 -0
  65. mx_bluesky/hyperion/supervisor/_supervisor.py +116 -0
  66. mx_bluesky/hyperion/supervisor/client_config.yaml +6 -0
  67. mx_bluesky/hyperion/supervisor/supervisor_config.yaml +10 -0
  68. mx_bluesky/hyperion/supervisor/supervisor_dev_config.yaml +9 -0
  69. {mx_bluesky-1.5.14.dist-info → mx_bluesky-1.5.16.dist-info}/METADATA +3 -31
  70. {mx_bluesky-1.5.14.dist-info → mx_bluesky-1.5.16.dist-info}/RECORD +74 -54
  71. {mx_bluesky-1.5.14.dist-info → mx_bluesky-1.5.16.dist-info}/WHEEL +0 -0
  72. {mx_bluesky-1.5.14.dist-info → mx_bluesky-1.5.16.dist-info}/entry_points.txt +0 -0
  73. {mx_bluesky-1.5.14.dist-info → mx_bluesky-1.5.16.dist-info}/licenses/LICENSE +0 -0
  74. {mx_bluesky-1.5.14.dist-info → mx_bluesky-1.5.16.dist-info}/top_level.txt +0 -0
@@ -52,6 +52,7 @@
52
52
  "import importlib\n",
53
53
  "\n",
54
54
  "from dodal.utils import collect_factories\n",
55
+ "\n",
55
56
  "beamline = \"i02_2\"\n",
56
57
  "module_name = f\"dodal.beamlines.{beamline}\"\n",
57
58
  "beamline_module = importlib.import_module(module_name)\n",
mx_bluesky/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.5.14'
32
- __version_tuple__ = version_tuple = (1, 5, 14)
31
+ __version__ = version = '1.5.16'
32
+ __version_tuple__ = version_tuple = (1, 5, 16)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -7,6 +7,7 @@ from bluesky.callbacks import CallbackBase
7
7
  from dodal.log import LOGGER
8
8
  from event_model.documents import Event, RunStart, RunStop
9
9
  from redis import StrictRedis
10
+ from redis.exceptions import ConnectionError
10
11
 
11
12
  FORWARDING_COMPLETE_MESSAGE = "image_forwarding_complete"
12
13
 
@@ -57,7 +58,20 @@ class MurkoCallback(CallbackBase):
57
58
  self.last_uuid = None
58
59
  self.previous_omegas: list[OmegaReading] = []
59
60
 
61
+ def _check_redis_connection(self):
62
+ try:
63
+ self.redis_client.ping()
64
+ return True
65
+ except ConnectionError:
66
+ LOGGER.warning(
67
+ f"Failed to connect to redis: {self.redis_client}. Murko callback will not run"
68
+ )
69
+ return False
70
+
60
71
  def start(self, doc: RunStart) -> RunStart | None:
72
+ self.redis_connected = self._check_redis_connection()
73
+ if not self.redis_connected:
74
+ return doc
61
75
  self.murko_metadata: dict = {"sample_id": doc.get("sample_id")}
62
76
  self.last_uuid = None
63
77
  self.previous_omegas = []
@@ -67,6 +81,8 @@ class MurkoCallback(CallbackBase):
67
81
  return doc
68
82
 
69
83
  def event(self, doc: Event) -> Event:
84
+ if not self.redis_connected:
85
+ return doc
70
86
  data = doc["data"]
71
87
  for prefix in ("oav", "oav_full_screen"):
72
88
  if f"{prefix}-beam_centre_j" in data:
@@ -114,6 +130,8 @@ class MurkoCallback(CallbackBase):
114
130
  self.redis_client.publish("murko", json.dumps(metadata))
115
131
 
116
132
  def stop(self, doc: RunStop) -> RunStop | None:
133
+ if not self.redis_connected:
134
+ return doc
117
135
  LOGGER.info(f"Finished streaming {self.murko_metadata['sample_id']} to murko")
118
136
  LOGGER.info(
119
137
  f"Publishing forwarding complete message: {FORWARDING_COMPLETE_MESSAGE}"
@@ -78,7 +78,7 @@ from mx_bluesky.common.parameters.gridscan import (
78
78
  SpecifiedThreeDGridScan,
79
79
  )
80
80
  from mx_bluesky.common.preprocessors.preprocessors import (
81
- transmission_and_xbpm_feedback_for_collection_decorator,
81
+ set_transmission_and_trigger_xbpm_feedback_before_collection_decorator,
82
82
  )
83
83
  from mx_bluesky.common.utils.exceptions import CrystalNotFoundError
84
84
  from mx_bluesky.common.utils.log import LOGGER
@@ -203,7 +203,7 @@ def i04_default_grid_detect_and_xray_centre(
203
203
 
204
204
  @bpp.subs_decorator(callbacks)
205
205
  @verify_undulator_gap_before_run_decorator(composite)
206
- @transmission_and_xbpm_feedback_for_collection_decorator(
206
+ @set_transmission_and_trigger_xbpm_feedback_before_collection_decorator(
207
207
  composite,
208
208
  grid_common_params.transmission_frac,
209
209
  PlanNameConstants.GRIDSCAN_OUTER,
@@ -278,11 +278,8 @@ def construct_i04_specific_features(
278
278
  signals_to_read_pre_flyscan = [
279
279
  xrc_composite.undulator.current_gap,
280
280
  xrc_composite.synchrotron.synchrotron_mode,
281
- xrc_composite.s4_slit_gaps.xgap,
282
- xrc_composite.s4_slit_gaps.ygap,
283
- xrc_composite.smargon.x,
284
- xrc_composite.smargon.y,
285
- xrc_composite.smargon.z,
281
+ xrc_composite.s4_slit_gaps,
282
+ xrc_composite.smargon,
286
283
  xrc_composite.dcm.energy_in_keV,
287
284
  ]
288
285
 
@@ -293,6 +290,8 @@ def construct_i04_specific_features(
293
290
  xrc_composite.dcm.energy_in_keV,
294
291
  xrc_composite.eiger.bit_depth,
295
292
  xrc_composite.beamsize,
293
+ xrc_composite.eiger.cam.roi_mode,
294
+ xrc_composite.eiger.ispyb_detector_id,
296
295
  ]
297
296
 
298
297
  tidy_plan = partial(
@@ -88,6 +88,7 @@ def thaw_and_murko_centre(
88
88
  initial_zoom_level = yield from bps.rd(oav_fs.zoom_controller.level)
89
89
  initial_velocity = yield from bps.rd(smargon.omega.velocity)
90
90
  new_velocity = abs(rotation / time_to_thaw) * 2.0
91
+
91
92
  murko_callback = MurkoCallback(
92
93
  RedisConstants.REDIS_HOST,
93
94
  RedisConstants.REDIS_PASSWORD,
@@ -0,0 +1,13 @@
1
+ from mx_bluesky.beamlines.i24.jungfrau_commissioning.experiment_plans.do_darks import (
2
+ do_non_pedestal_darks,
3
+ do_pedestal_darks,
4
+ )
5
+ from mx_bluesky.beamlines.i24.jungfrau_commissioning.experiment_plans.rotation_scan_plan import (
6
+ single_rotation_plan,
7
+ )
8
+
9
+ __all__ = [
10
+ "do_pedestal_darks",
11
+ "do_non_pedestal_darks",
12
+ "single_rotation_plan",
13
+ ]
@@ -0,0 +1,86 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from bluesky.callbacks import CallbackBase
5
+
6
+ from mx_bluesky.beamlines.i24.parameters.constants import PlanNameConstants
7
+ from mx_bluesky.common.parameters.rotation import SingleRotationScan
8
+ from mx_bluesky.common.utils.log import LOGGER
9
+
10
+ READING_DUMP_FILENAME = "collection_info.json"
11
+
12
+
13
+ class JsonMetadataWriter(CallbackBase):
14
+ """Callback class to handle the creation of metadata json files for commissioning.
15
+
16
+ To use, subscribe the Bluesky RunEngine to an instance of this class.
17
+ E.g.:
18
+ metadata_writer_callback = JsonMetadataWriter(parameters)
19
+ RE.subscribe(metadata_writer_callback)
20
+ Or decorate a plan using bluesky.preprocessors.subs_decorator.
21
+
22
+ See: https://blueskyproject.io/bluesky/callbacks.html#ways-to-invoke-callbacks
23
+
24
+ """
25
+
26
+ def __init__(self):
27
+ self.wavelength_in_a = None
28
+ self.energy_in_kev = None
29
+ self.detector_distance_mm = None
30
+ self.final_path: Path | None = None
31
+ self.descriptors: dict[str, dict] = {}
32
+ self.transmission: float | None = None
33
+ self.parameters: SingleRotationScan | None = None
34
+
35
+ super().__init__()
36
+
37
+ def start(self, doc: dict): # type: ignore
38
+ if doc.get("subplan_name") == PlanNameConstants.ROTATION_MAIN:
39
+ json_params = doc.get("rotation_scan_params")
40
+ assert json_params is not None
41
+ LOGGER.info(
42
+ f"Metadata writer recieved start document with experiment parameters {json_params}"
43
+ )
44
+ self.parameters = SingleRotationScan(**json.loads(json_params))
45
+ self.run_start_uid = doc.get("uid")
46
+
47
+ def descriptor(self, doc: dict): # type: ignore
48
+ self.descriptors[doc["uid"]] = doc
49
+
50
+ def event(self, doc: dict): # type: ignore
51
+ event_descriptor = self.descriptors[doc["descriptor"]]
52
+
53
+ if event_descriptor.get("name") == PlanNameConstants.ROTATION_DEVICE_READ:
54
+ assert self.parameters is not None
55
+ data = doc.get("data")
56
+ assert data is not None
57
+ self.wavelength_in_a = data.get("dcm-wavelength_in_a")
58
+ self.energy_in_kev = data.get("dcm-energy_in_keV")
59
+ self.detector_distance_mm = data.get("detector_motion-z")
60
+ assert data.get("detector-_writer-file_path"), (
61
+ "No detector writer path was found"
62
+ )
63
+ self.final_path = Path(data.get("detector-_writer-file_path"))
64
+
65
+ LOGGER.info(
66
+ f"Metadata writer received parameters, energy_in_kev: {self.energy_in_kev}, wavelength: {self.wavelength_in_a}, det_distance_mm: {self.detector_distance_mm}, file path: {self.final_path}"
67
+ )
68
+
69
+ def stop(self, doc: dict): # type: ignore
70
+ assert self.parameters is not None
71
+ if (
72
+ self.run_start_uid is not None
73
+ and doc.get("run_start") == self.run_start_uid
74
+ and self.final_path
75
+ ):
76
+ with open(self.final_path / READING_DUMP_FILENAME, "w") as f:
77
+ f.write(
78
+ json.dumps(
79
+ {
80
+ "wavelength_in_a": self.wavelength_in_a,
81
+ "energy_kev": self.energy_in_kev,
82
+ "angular_increment_deg": self.parameters.rotation_increment_deg,
83
+ "detector_distance_mm": self.detector_distance_mm,
84
+ }
85
+ )
86
+ )
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import pydantic
4
+ from dodal.devices.attenuator.attenuator import EnumFilterAttenuator
5
+ from dodal.devices.hutch_shutter import HutchShutter
6
+ from dodal.devices.i24.aperture import Aperture
7
+ from dodal.devices.i24.beamstop import Beamstop
8
+ from dodal.devices.i24.commissioning_jungfrau import CommissioningJungfrau
9
+ from dodal.devices.i24.dcm import DCM
10
+ from dodal.devices.i24.dual_backlight import DualBacklight
11
+ from dodal.devices.i24.vgonio import VerticalGoniometer
12
+ from dodal.devices.motors import YZStage
13
+ from dodal.devices.synchrotron import Synchrotron
14
+ from dodal.devices.xbpm_feedback import XBPMFeedback
15
+ from dodal.devices.zebra.zebra import Zebra
16
+ from dodal.devices.zebra.zebra_controlled_shutter import ZebraShutter
17
+
18
+
19
+ @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
20
+ class RotationScanComposite:
21
+ """All devices which are directly or indirectly required by this plan"""
22
+
23
+ aperture: Aperture
24
+ attenuator: EnumFilterAttenuator
25
+ jungfrau: CommissioningJungfrau
26
+ gonio: VerticalGoniometer
27
+ synchrotron: Synchrotron
28
+ sample_shutter: ZebraShutter
29
+ zebra: Zebra
30
+ xbpm_feedback: XBPMFeedback
31
+ hutch_shutter: HutchShutter
32
+ beamstop: Beamstop
33
+ det_stage: YZStage
34
+ backlight: DualBacklight
35
+ dcm: DCM
@@ -13,7 +13,6 @@ from pydantic import PositiveInt
13
13
 
14
14
  from mx_bluesky.beamlines.i24.jungfrau_commissioning.plan_stubs.plan_utils import (
15
15
  fly_jungfrau,
16
- override_file_path,
17
16
  )
18
17
  from mx_bluesky.common.utils.log import LOGGER
19
18
 
@@ -25,8 +24,8 @@ def do_pedestal_darks(
25
24
  exp_time_s: float = 0.001,
26
25
  pedestal_frames: PositiveInt = 20,
27
26
  pedestal_loops: PositiveInt = 200,
27
+ filename: str = "pedestal_darks",
28
28
  jungfrau: CommissioningJungfrau = inject("jungfrau"),
29
- path_of_output_file: str | None = None,
30
29
  ) -> MsgGenerator:
31
30
  """Acquire darks in pedestal mode, using dynamic gain mode. This calibrates the offsets
32
31
  for the jungfrau, and must be performed before acquiring real data in dynamic gain mode.
@@ -46,18 +45,19 @@ def do_pedestal_darks(
46
45
  exp_time_s: Length of detector exposure for each frame.
47
46
  pedestal_frames: Number of frames acquired per pedestal loop.
48
47
  pedestal_loops: Number of times to acquire a set of pedestal_frames
48
+ filename: Name of output file
49
49
  jungfrau: Jungfrau device
50
- path_of_output_file: Absolute path of the detector file output, including file name. If None, then use the PathProvider
51
- set during Jungfrau device instantiation
52
50
  """
53
51
 
54
52
  @bpp.set_run_key_decorator(PEDESTAL_DARKS_RUN)
55
- @bpp.run_decorator(md={"subplan_name": PEDESTAL_DARKS_RUN})
53
+ @bpp.run_decorator(
54
+ md={
55
+ "subplan_name": PEDESTAL_DARKS_RUN,
56
+ "detector_file_template": filename,
57
+ }
58
+ )
56
59
  @bpp.stage_decorator([jungfrau])
57
60
  def _do_decorated_plan():
58
- if path_of_output_file:
59
- override_file_path(jungfrau, path_of_output_file)
60
-
61
61
  trigger_info = create_jungfrau_pedestal_triggering_info(
62
62
  exp_time_s, pedestal_frames, pedestal_loops
63
63
  )
@@ -67,12 +67,11 @@ def do_pedestal_darks(
67
67
  yield from bps.mv(
68
68
  jungfrau.drv.acquisition_type,
69
69
  AcquisitionType.PEDESTAL,
70
- jungfrau.drv.gain_mode,
71
- GainMode.DYNAMIC,
72
70
  )
73
71
  yield from fly_jungfrau(
74
72
  jungfrau,
75
73
  trigger_info,
74
+ GainMode.DYNAMIC,
76
75
  wait=True,
77
76
  log_on_percentage_prefix="Jungfrau pedestal dynamic gain mode darks triggers received",
78
77
  )
@@ -84,8 +83,8 @@ def do_non_pedestal_darks(
84
83
  gain_mode: GainMode,
85
84
  exp_time_s: float = 0.001,
86
85
  total_triggers: PositiveInt = 1000,
86
+ filename: str = "darks",
87
87
  jungfrau: CommissioningJungfrau = inject("jungfrau"),
88
- path_of_output_file: str | None = None,
89
88
  ) -> MsgGenerator:
90
89
  """Internally take a set of images at a given gain mode.
91
90
 
@@ -96,26 +95,26 @@ def do_non_pedestal_darks(
96
95
  exp_time_s: Length of detector exposure for each trigger.
97
96
  total_triggers: Total triggers for the dark scan.
98
97
  jungfrau: Jungfrau device
99
- path_of_output_file: Absolute path of the detector file output, including file name. If None, then use the PathProvider
100
- set during Jungfrau device instantiation
98
+ filename: Name of output file
101
99
  """
102
100
 
103
101
  @bpp.set_run_key_decorator(STANDARD_DARKS_RUN)
104
- @bpp.run_decorator(md={"subplan_name": STANDARD_DARKS_RUN})
102
+ @bpp.run_decorator(
103
+ md={
104
+ "subplan_name": STANDARD_DARKS_RUN,
105
+ "detector_file_template": filename,
106
+ }
107
+ )
105
108
  @bpp.stage_decorator([jungfrau])
106
109
  def _do_decorated_plan():
107
- if path_of_output_file:
108
- override_file_path(jungfrau, path_of_output_file)
109
-
110
110
  trigger_info = create_jungfrau_internal_triggering_info(
111
111
  total_triggers, exp_time_s
112
112
  )
113
113
 
114
- yield from bps.mv(jungfrau.drv.gain_mode, gain_mode)
115
-
116
114
  yield from fly_jungfrau(
117
115
  jungfrau,
118
116
  trigger_info,
117
+ gain_mode,
119
118
  wait=True,
120
119
  log_on_percentage_prefix=f"Jungfrau {gain_mode} gain mode darks triggers received",
121
120
  )
@@ -0,0 +1,292 @@
1
+ from functools import partial
2
+
3
+ import bluesky.plan_stubs as bps
4
+ import bluesky.preprocessors as bpp
5
+ from bluesky.preprocessors import run_decorator
6
+ from bluesky.utils import MsgGenerator
7
+ from dodal.devices.hutch_shutter import ShutterState
8
+ from dodal.devices.i24.aperture import AperturePositions
9
+ from dodal.devices.i24.beamstop import BeamstopPositions
10
+ from dodal.devices.i24.commissioning_jungfrau import CommissioningJungfrau
11
+ from dodal.devices.i24.dual_backlight import BacklightPositions
12
+ from dodal.devices.zebra.zebra import ArmDemand, I24Axes, Zebra
13
+ from dodal.devices.zebra.zebra_controlled_shutter import ZebraShutter
14
+ from ophyd_async.fastcs.jungfrau import (
15
+ GainMode,
16
+ create_jungfrau_external_triggering_info,
17
+ )
18
+ from pydantic import BaseModel, field_validator
19
+
20
+ from mx_bluesky.beamlines.i24.jungfrau_commissioning.callbacks.metadata_writer import (
21
+ JsonMetadataWriter,
22
+ )
23
+ from mx_bluesky.beamlines.i24.jungfrau_commissioning.composites import (
24
+ RotationScanComposite,
25
+ )
26
+ from mx_bluesky.beamlines.i24.jungfrau_commissioning.plan_stubs.plan_utils import (
27
+ JF_COMPLETE_GROUP,
28
+ fly_jungfrau,
29
+ )
30
+ from mx_bluesky.beamlines.i24.parameters.constants import (
31
+ PlanNameConstants,
32
+ )
33
+ from mx_bluesky.common.device_setup_plans.setup_zebra_and_shutter import (
34
+ setup_zebra_for_rotation,
35
+ tidy_up_zebra_after_rotation_scan,
36
+ )
37
+ from mx_bluesky.common.experiment_plans.inner_plans.read_hardware import (
38
+ read_hardware_plan,
39
+ )
40
+ from mx_bluesky.common.experiment_plans.rotation.rotation_utils import (
41
+ RotationMotionProfile,
42
+ calculate_motion_profile,
43
+ )
44
+ from mx_bluesky.common.parameters.components import PARAMETER_VERSION
45
+ from mx_bluesky.common.parameters.constants import (
46
+ USE_NUMTRACKER,
47
+ PlanGroupCheckpointConstants,
48
+ )
49
+ from mx_bluesky.common.parameters.rotation import (
50
+ SingleRotationScan,
51
+ )
52
+ from mx_bluesky.common.utils.log import LOGGER
53
+
54
+ READING_DUMP_FILENAME = "collection_info.json"
55
+
56
+ # Should be read from config file, see
57
+ # https://github.com/DiamondLightSource/mx-bluesky/issues/1502
58
+ JF_DET_STAGE_Y_POSITION_MM = 730
59
+ DEFAULT_DETECTOR_DISTANCE_MM = 200
60
+
61
+
62
+ class ExternalRotationScanParams(BaseModel):
63
+ transmission_fractions: list[float]
64
+ exposure_time_s: float
65
+ omega_start_deg: float = 0
66
+ rotation_increment_per_image_deg: float = 0.1
67
+ filename: str = "rotations"
68
+ detector_distance_mm: float = DEFAULT_DETECTOR_DISTANCE_MM
69
+ sample_id: int
70
+
71
+ @field_validator("transmission_fractions")
72
+ @classmethod
73
+ def validate_transmission_fractions(cls, values):
74
+ for v in values:
75
+ if not 0 <= v <= 1:
76
+ raise ValueError(
77
+ f"All transmission fractions must be between 0 and 1; got {v}"
78
+ )
79
+ return values
80
+
81
+
82
+ def _get_internal_rotation_params(
83
+ entry_params: ExternalRotationScanParams, transmission: float
84
+ ) -> SingleRotationScan:
85
+ return SingleRotationScan(
86
+ sample_id=entry_params.sample_id,
87
+ visit=USE_NUMTRACKER, # See https://github.com/DiamondLightSource/mx-bluesky/issues/1527
88
+ parameter_model_version=PARAMETER_VERSION,
89
+ file_name=entry_params.filename,
90
+ transmission_frac=transmission,
91
+ exposure_time_s=entry_params.exposure_time_s,
92
+ storage_directory=USE_NUMTRACKER,
93
+ )
94
+
95
+
96
+ class HutchClosedError(Exception): ...
97
+
98
+
99
+ def rotation_scan_plan(
100
+ composite: RotationScanComposite, params: ExternalRotationScanParams
101
+ ) -> MsgGenerator:
102
+ """BlueAPI entry point for i24 JF rotation scans"""
103
+
104
+ for transmission in params.transmission_fractions:
105
+ rotation_params = _get_internal_rotation_params(params, transmission)
106
+ yield from single_rotation_plan(composite, rotation_params)
107
+
108
+
109
+ def set_up_beamline_for_rotation(
110
+ composite: RotationScanComposite,
111
+ det_z_mm: float,
112
+ transmission_frac: float,
113
+ ):
114
+ """Check hutch is open, then, in parallel, move backlight in,
115
+ move aperture in, move beamstop out and move det stages in. Wait for this parallel
116
+ move to finish."""
117
+
118
+ hutch_shutter_state: ShutterState = yield from bps.rd(
119
+ composite.hutch_shutter.status
120
+ )
121
+ LOGGER.info(f"Hutch shutter: {hutch_shutter_state}")
122
+ if hutch_shutter_state != ShutterState.OPEN:
123
+ LOGGER.error(f"Hutch shutter is not open! State is {hutch_shutter_state}")
124
+ raise HutchClosedError(
125
+ f"Hutch shutter is not open! State is {hutch_shutter_state}"
126
+ )
127
+
128
+ LOGGER.info(
129
+ "Making sure aperture and beamstop are in, detector stages are in position, backlight is out, and transmission is set..."
130
+ )
131
+ yield from bps.mv(
132
+ composite.aperture.position,
133
+ AperturePositions.IN,
134
+ composite.beamstop.pos_select,
135
+ BeamstopPositions.DATA_COLLECTION,
136
+ composite.det_stage.y,
137
+ JF_DET_STAGE_Y_POSITION_MM,
138
+ composite.backlight.backlight_position,
139
+ BacklightPositions.OUT,
140
+ composite.det_stage.z,
141
+ det_z_mm,
142
+ composite.attenuator,
143
+ transmission_frac,
144
+ )
145
+
146
+
147
+ def single_rotation_plan(
148
+ composite: RotationScanComposite,
149
+ params: SingleRotationScan,
150
+ ):
151
+ """A stub plan to collect diffraction images from a sample continuously rotating
152
+ about a fixed axis - for now this axis is limited to omega.
153
+ Needs additional setup of the sample environment and a wrapper to clean up."""
154
+
155
+ @bpp.set_run_key_decorator(PlanNameConstants.SINGLE_ROTATION_SCAN)
156
+ @run_decorator()
157
+ def _plan_in_run_decorator():
158
+ if not params.detector_distance_mm:
159
+ LOGGER.info(
160
+ f"Using default detector distance of {DEFAULT_DETECTOR_DISTANCE_MM} mm"
161
+ )
162
+ params.detector_distance_mm = DEFAULT_DETECTOR_DISTANCE_MM
163
+
164
+ yield from set_up_beamline_for_rotation(
165
+ composite, params.detector_distance_mm, params.transmission_frac
166
+ )
167
+
168
+ # This value isn't actually used, see https://github.com/DiamondLightSource/mx-bluesky/issues/1224
169
+ _motor_time_to_speed = 1
170
+ _max_velocity_deg_s = yield from bps.rd(composite.gonio.omega.max_velocity)
171
+
172
+ motion_values = calculate_motion_profile(
173
+ params, _motor_time_to_speed, _max_velocity_deg_s
174
+ )
175
+
176
+ # Callback which intercepts read documents and writes to json file,
177
+ # used for saving device metadata
178
+ metadata_writer = JsonMetadataWriter()
179
+
180
+ @bpp.subs_decorator([metadata_writer])
181
+ @bpp.set_run_key_decorator(PlanNameConstants.ROTATION_MAIN)
182
+ @bpp.run_decorator(
183
+ md={
184
+ "subplan_name": PlanNameConstants.ROTATION_MAIN,
185
+ "scan_points": [params.scan_points],
186
+ "rotation_scan_params": params.model_dump_json(),
187
+ "detector_file_template": params.file_name,
188
+ }
189
+ )
190
+ def _rotation_scan_plan(
191
+ motion_values: RotationMotionProfile,
192
+ composite: RotationScanComposite,
193
+ ):
194
+ _jf_trigger_info = create_jungfrau_external_triggering_info(
195
+ params.num_images, params.detector_params.exposure_time_s
196
+ )
197
+
198
+ axis = composite.gonio.omega
199
+
200
+ # can move to start as fast as possible
201
+ yield from bps.abs_set(
202
+ axis.velocity, motion_values.max_velocity_deg_s, wait=True
203
+ )
204
+ LOGGER.info(f"Moving omega to start value, {motion_values.start_scan_deg=}")
205
+ yield from bps.abs_set(
206
+ axis,
207
+ motion_values.start_motion_deg,
208
+ group=PlanGroupCheckpointConstants.ROTATION_READY_FOR_DC,
209
+ )
210
+
211
+ yield from setup_zebra_for_rotation(
212
+ composite.zebra,
213
+ composite.sample_shutter,
214
+ axis=I24Axes.OMEGA,
215
+ start_angle=motion_values.start_scan_deg,
216
+ scan_width=motion_values.scan_width_deg,
217
+ direction=motion_values.direction,
218
+ shutter_opening_deg=motion_values.shutter_opening_deg,
219
+ shutter_opening_s=motion_values.shutter_time_s,
220
+ )
221
+
222
+ yield from bps.wait(PlanGroupCheckpointConstants.ROTATION_READY_FOR_DC)
223
+
224
+ # Get ready for the actual scan
225
+ yield from bps.abs_set(
226
+ axis.velocity, motion_values.speed_for_rotation_deg_s, wait=True
227
+ )
228
+ yield from bps.abs_set(composite.zebra.pc.arm, ArmDemand.ARM, wait=True)
229
+
230
+ # Should check topup gate here, but not yet implemented,
231
+ # see https://github.com/DiamondLightSource/mx-bluesky/issues/1501
232
+
233
+ # Read hardware after preparing jungfrau so that device metadata output from callback is correct
234
+ # Whilst metadata is being written in bluesky we need to access the private writer here
235
+ read_hardware_partial = partial(
236
+ read_hardware_plan,
237
+ [
238
+ composite.dcm.energy_in_keV,
239
+ composite.dcm.wavelength_in_a,
240
+ composite.det_stage.z,
241
+ composite.jungfrau._writer.file_path, # noqa: SLF001 N
242
+ ],
243
+ PlanNameConstants.ROTATION_DEVICE_READ,
244
+ )
245
+
246
+ yield from fly_jungfrau(
247
+ composite.jungfrau,
248
+ _jf_trigger_info,
249
+ GainMode.DYNAMIC,
250
+ wait=False,
251
+ log_on_percentage_prefix="Jungfrau rotation scan triggers received",
252
+ read_hardware_after_prepare_plan=read_hardware_partial,
253
+ )
254
+
255
+ LOGGER.info("Executing rotation scan")
256
+ yield from bps.rel_set(
257
+ axis,
258
+ motion_values.distance_to_move_deg,
259
+ wait=False,
260
+ group=JF_COMPLETE_GROUP,
261
+ )
262
+
263
+ LOGGER.info(
264
+ "Waiting for omega to finish moving and for Jungfrau to receive correct number of triggers"
265
+ )
266
+ yield from bps.wait(group=JF_COMPLETE_GROUP)
267
+
268
+ yield from bpp.finalize_wrapper(
269
+ _rotation_scan_plan(motion_values, composite),
270
+ final_plan=partial(
271
+ _cleanup_plan,
272
+ composite.zebra,
273
+ composite.jungfrau,
274
+ composite.sample_shutter,
275
+ ),
276
+ )
277
+
278
+ yield from _plan_in_run_decorator()
279
+
280
+
281
+ def _cleanup_plan(
282
+ zebra: Zebra,
283
+ jf: CommissioningJungfrau,
284
+ zebra_shutter: ZebraShutter,
285
+ group="rotation cleanup",
286
+ ):
287
+ LOGGER.info("Tidying up Zebra and Jungfrau...")
288
+ yield from bps.unstage(jf, group=group)
289
+ yield from tidy_up_zebra_after_rotation_scan(
290
+ zebra, zebra_shutter, group=group, wait=False
291
+ )
292
+ yield from bps.wait(group=group)
@@ -5,20 +5,20 @@ from ophyd_async.core import (
5
5
  WatchableAsyncStatus,
6
6
  )
7
7
  from ophyd_async.fastcs.jungfrau import (
8
+ GainMode,
8
9
  create_jungfrau_external_triggering_info,
9
10
  )
10
11
  from pydantic import PositiveInt
11
12
 
12
13
  from mx_bluesky.beamlines.i24.jungfrau_commissioning.plan_stubs.plan_utils import (
13
14
  fly_jungfrau,
14
- override_file_path,
15
15
  )
16
16
 
17
17
 
18
18
  def do_external_acquisition(
19
19
  exp_time_s: float,
20
+ gain_mode: GainMode,
20
21
  total_triggers: PositiveInt = 1,
21
- output_file_path: str | None = None,
22
22
  wait: bool = False,
23
23
  jungfrau: CommissioningJungfrau = inject("commissioning_jungfrau"),
24
24
  ) -> MsgGenerator[WatchableAsyncStatus]:
@@ -32,14 +32,9 @@ def do_external_acquisition(
32
32
  exp_time_s: Length of detector exposure for each frame.
33
33
  total_triggers: Number of external triggers received before acquisition is marked as complete.
34
34
  jungfrau: Jungfrau device
35
- output_file_name: Absolute path of the detector file output, including file name. If None, then use the PathProvider
36
- set during jungfrau device instantiation
37
35
  wait: Optionally block until data collection is complete.
38
36
  """
39
37
 
40
- if output_file_path:
41
- override_file_path(jungfrau, output_file_path)
42
-
43
38
  trigger_info = create_jungfrau_external_triggering_info(total_triggers, exp_time_s)
44
- status = yield from fly_jungfrau(jungfrau, trigger_info, wait=wait)
39
+ status = yield from fly_jungfrau(jungfrau, trigger_info, gain_mode, wait=wait)
45
40
  return status