mx-bluesky 1.5.15__py3-none-any.whl → 1.6.3__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 (88) hide show
  1. mx_bluesky/Getting started.ipynb +1 -0
  2. mx_bluesky/_version.py +2 -2
  3. mx_bluesky/beamlines/i02_1/parameters/gridscan.py +1 -1
  4. mx_bluesky/beamlines/i04/__init__.py +4 -0
  5. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +18 -0
  6. mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +5 -4
  7. mx_bluesky/beamlines/i04/oav_centering_plans/oav_imaging.py +224 -10
  8. mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +5 -2
  9. mx_bluesky/beamlines/i04/thawing_plan.py +3 -2
  10. mx_bluesky/beamlines/i24/jungfrau_commissioning/__init__.py +13 -0
  11. mx_bluesky/beamlines/i24/jungfrau_commissioning/callbacks/__init__.py +0 -0
  12. mx_bluesky/beamlines/i24/jungfrau_commissioning/callbacks/metadata_writer.py +86 -0
  13. mx_bluesky/beamlines/i24/jungfrau_commissioning/composites.py +35 -0
  14. mx_bluesky/beamlines/i24/jungfrau_commissioning/experiment_plans/do_darks.py +19 -20
  15. mx_bluesky/beamlines/i24/jungfrau_commissioning/experiment_plans/rotation_scan_plan.py +292 -0
  16. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/do_external_acquisition.py +4 -9
  17. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/do_internal_acquisition.py +4 -5
  18. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/plan_utils.py +15 -19
  19. mx_bluesky/beamlines/i24/parameters/__init__.py +0 -0
  20. mx_bluesky/beamlines/i24/parameters/constants.py +9 -0
  21. mx_bluesky/beamlines/i24/serial/dcid.py +3 -3
  22. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_extruder_collect_py3v2.py +7 -7
  23. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_chip_collect_py3v1.py +7 -7
  24. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_chip_manager_py3v1.py +3 -3
  25. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +3 -3
  26. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +5 -5
  27. mx_bluesky/beamlines/i24/serial/web_gui_plans/general_plans.py +7 -7
  28. mx_bluesky/beamlines/i24/serial/web_gui_plans/oav_plans.py +1 -1
  29. mx_bluesky/common/device_setup_plans/robot_load_unload.py +2 -24
  30. mx_bluesky/common/device_setup_plans/setup_zebra_and_shutter.py +5 -2
  31. mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +2 -2
  32. mx_bluesky/common/experiment_plans/inner_plans/read_hardware.py +1 -0
  33. mx_bluesky/common/experiment_plans/oav_grid_detection_plan.py +1 -1
  34. mx_bluesky/common/experiment_plans/pin_tip_centring_plan.py +2 -2
  35. mx_bluesky/common/experiment_plans/rotation/__init__.py +0 -0
  36. mx_bluesky/common/experiment_plans/rotation/rotation_utils.py +127 -0
  37. mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +13 -2
  38. mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +0 -2
  39. mx_bluesky/common/external_interaction/ispyb/data_model.py +1 -1
  40. mx_bluesky/common/external_interaction/ispyb/exp_eye_store.py +1 -1
  41. mx_bluesky/common/parameters/components.py +17 -7
  42. mx_bluesky/common/parameters/constants.py +6 -0
  43. mx_bluesky/{hyperion → common}/parameters/rotation.py +10 -8
  44. mx_bluesky/common/preprocessors/preprocessors.py +98 -36
  45. mx_bluesky/hyperion/__main__.py +55 -22
  46. mx_bluesky/hyperion/baton_handler.py +24 -64
  47. mx_bluesky/hyperion/blueapi_config.yaml +17 -0
  48. mx_bluesky/hyperion/blueapi_dev_config.yaml +16 -0
  49. mx_bluesky/hyperion/blueapi_plans/__init__.py +96 -0
  50. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +9 -7
  51. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +1 -1
  52. mx_bluesky/hyperion/device_setup_plans/utils.py +1 -1
  53. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +3 -1
  54. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +1 -0
  55. mx_bluesky/hyperion/experiment_plans/hyperion_grid_detect_then_xray_centre_plan.py +2 -2
  56. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +3 -1
  57. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +17 -6
  58. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +2 -5
  59. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +3 -3
  60. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +14 -128
  61. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +4 -4
  62. mx_bluesky/hyperion/experiment_plans/udc_default_state.py +19 -6
  63. mx_bluesky/hyperion/external_interaction/agamemnon.py +3 -8
  64. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +121 -47
  65. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +3 -1
  66. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +3 -1
  67. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +6 -3
  68. mx_bluesky/hyperion/external_interaction/callbacks/stomp/__init__.py +0 -0
  69. mx_bluesky/hyperion/external_interaction/callbacks/stomp/dispatcher.py +33 -0
  70. mx_bluesky/hyperion/in_process_runner.py +132 -0
  71. mx_bluesky/hyperion/parameters/cli.py +43 -4
  72. mx_bluesky/hyperion/parameters/components.py +13 -0
  73. mx_bluesky/hyperion/parameters/constants.py +2 -9
  74. mx_bluesky/hyperion/parameters/device_composites.py +1 -1
  75. mx_bluesky/hyperion/parameters/load_centre_collect.py +3 -1
  76. mx_bluesky/hyperion/plan_runner.py +45 -66
  77. mx_bluesky/hyperion/plan_runner_api.py +3 -4
  78. mx_bluesky/hyperion/supervisor/__init__.py +3 -0
  79. mx_bluesky/hyperion/supervisor/_supervisor.py +116 -0
  80. mx_bluesky/hyperion/supervisor/client_config.yaml +6 -0
  81. mx_bluesky/hyperion/supervisor/supervisor_config.yaml +10 -0
  82. mx_bluesky/hyperion/supervisor/supervisor_dev_config.yaml +9 -0
  83. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.6.3.dist-info}/METADATA +3 -31
  84. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.6.3.dist-info}/RECORD +88 -68
  85. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.6.3.dist-info}/WHEEL +1 -1
  86. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.6.3.dist-info}/entry_points.txt +0 -0
  87. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.6.3.dist-info}/licenses/LICENSE +0 -0
  88. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.6.3.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.15'
32
- __version_tuple__ = version_tuple = (1, 5, 15)
31
+ __version__ = version = '1.6.3'
32
+ __version_tuple__ = version_tuple = (1, 6, 3)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,4 +1,4 @@
1
- from dodal.devices.i02_1.fast_grid_scan import ZebraGridScanParamsTwoD
1
+ from dodal.devices.beamlines.i02_1.fast_grid_scan import ZebraGridScanParamsTwoD
2
2
  from scanspec.specs import Product
3
3
 
4
4
  from mx_bluesky.common.parameters.components import SplitScan, WithOptionalEnergyChange
@@ -2,6 +2,8 @@ from mx_bluesky.beamlines.i04.experiment_plans.i04_grid_detect_then_xray_centre_
2
2
  i04_default_grid_detect_and_xray_centre,
3
3
  )
4
4
  from mx_bluesky.beamlines.i04.oav_centering_plans.oav_imaging import (
5
+ find_beam_centres,
6
+ optimise_transmission_with_oav,
5
7
  take_oav_image_with_scintillator_in,
6
8
  )
7
9
  from mx_bluesky.beamlines.i04.thawing_plan import (
@@ -16,4 +18,6 @@ __all__ = [
16
18
  "i04_default_grid_detect_and_xray_centre",
17
19
  "thaw_and_murko_centre",
18
20
  "take_oav_image_with_scintillator_in",
21
+ "optimise_transmission_with_oav",
22
+ "find_beam_centres",
19
23
  ]
@@ -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}"
@@ -9,6 +9,8 @@ from dodal.common import inject
9
9
  from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue
10
10
  from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
11
11
  from dodal.devices.backlight import Backlight
12
+ from dodal.devices.beamlines.i04.beamsize import Beamsize
13
+ from dodal.devices.beamlines.i04.transfocator import Transfocator
12
14
  from dodal.devices.common_dcm import DoubleCrystalMonochromator
13
15
  from dodal.devices.detector.detector_motion import DetectorMotion
14
16
  from dodal.devices.eiger import EigerDetector
@@ -17,8 +19,6 @@ from dodal.devices.fast_grid_scan import (
17
19
  set_fast_grid_scan_params,
18
20
  )
19
21
  from dodal.devices.flux import Flux
20
- from dodal.devices.i04.beamsize import Beamsize
21
- from dodal.devices.i04.transfocator import Transfocator
22
22
  from dodal.devices.mx_phase1.beamstop import Beamstop
23
23
  from dodal.devices.oav.oav_detector import OAV
24
24
  from dodal.devices.oav.pin_image_recognition import PinTipDetection
@@ -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,
@@ -291,6 +291,7 @@ def construct_i04_specific_features(
291
291
  xrc_composite.eiger.bit_depth,
292
292
  xrc_composite.beamsize,
293
293
  xrc_composite.eiger.cam.roi_mode,
294
+ xrc_composite.eiger.ispyb_detector_id,
294
295
  ]
295
296
 
296
297
  tidy_plan = partial(
@@ -6,8 +6,10 @@ from bluesky.utils import MsgGenerator
6
6
  from dodal.common import inject
7
7
  from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
8
8
  from dodal.devices.backlight import Backlight
9
+ from dodal.devices.beamlines.i04.beam_centre import CentreEllipseMethod
10
+ from dodal.devices.beamlines.i04.max_pixel import MaxPixel
9
11
  from dodal.devices.mx_phase1.beamstop import Beamstop, BeamstopPositions
10
- from dodal.devices.oav.oav_detector import OAV
12
+ from dodal.devices.oav.oav_detector import OAV, ZoomControllerWithBeamCentres
11
13
  from dodal.devices.robot import BartRobot, PinMounted
12
14
  from dodal.devices.scintillator import InOut, Scintillator
13
15
  from dodal.devices.xbpm_feedback import XBPMFeedback
@@ -19,8 +21,9 @@ from dodal.devices.zebra.zebra_controlled_shutter import (
19
21
  from ophyd_async.core import InOut as core_INOUT
20
22
 
21
23
  from mx_bluesky.common.utils.exceptions import BeamlineStateError
24
+ from mx_bluesky.common.utils.log import LOGGER
22
25
 
23
- initial_wait_group = "Wait for scint to move in"
26
+ OAV_PREPARE_BEAMLINE_FOR_SCINT_WAIT = "Wait for scint to move in"
24
27
 
25
28
 
26
29
  def take_oav_image_with_scintillator_in(
@@ -48,21 +51,36 @@ def take_oav_image_with_scintillator_in(
48
51
  defaults are always correct.
49
52
  """
50
53
 
54
+ LOGGER.info("Preparing beamline to take scintillator images...")
51
55
  yield from _prepare_beamline_for_scintillator_images(
52
- robot, beamstop, backlight, scintillator, xbpm_feedback, initial_wait_group
56
+ robot,
57
+ beamstop,
58
+ backlight,
59
+ scintillator,
60
+ xbpm_feedback,
61
+ shutter,
62
+ OAV_PREPARE_BEAMLINE_FOR_SCINT_WAIT,
63
+ )
64
+ yield from bps.abs_set(
65
+ attenuator, transmission, group=OAV_PREPARE_BEAMLINE_FOR_SCINT_WAIT
53
66
  )
54
-
55
- yield from bps.abs_set(attenuator, transmission, group=initial_wait_group)
56
67
 
57
68
  if image_name is None:
58
69
  image_name = f"{time.time_ns()}ATT{transmission * 100}"
70
+ LOGGER.info(f"Using image name {image_name}")
71
+ LOGGER.info("Waiting for prepare beamline plan to complete...")
72
+ yield from bps.wait(OAV_PREPARE_BEAMLINE_FOR_SCINT_WAIT)
59
73
 
60
- yield from bps.wait(initial_wait_group)
74
+ LOGGER.info("Opening shutter...")
61
75
 
62
76
  yield from bps.abs_set(shutter.control_mode, ZebraShutterControl.MANUAL, wait=True)
63
77
  yield from bps.abs_set(shutter, ZebraShutterState.OPEN, wait=True)
64
78
 
65
- take_and_save_oav_image(file_path=image_path, file_name=image_name, oav=oav)
79
+ LOGGER.info("Taking image...")
80
+
81
+ yield from take_and_save_oav_image(
82
+ file_path=image_path, file_name=image_name, oav=oav
83
+ )
66
84
 
67
85
 
68
86
  def _prepare_beamline_for_scintillator_images(
@@ -71,11 +89,13 @@ def _prepare_beamline_for_scintillator_images(
71
89
  backlight: Backlight,
72
90
  scintillator: Scintillator,
73
91
  xbpm_feedback: XBPMFeedback,
92
+ shutter: ZebraShutter,
74
93
  group: str,
75
94
  ) -> MsgGenerator:
76
95
  """
77
- Prepares the beamline for oav image by making sure the pin is NOT mounted and
78
- the beam is on (feedback check). Finally, the scintillator is moved in.
96
+ Prepares the beamline for oav image by making sure the pin is not mounted and
97
+ the beam is on (feedback check). Finally, the scintillator is moved in and the
98
+ shutter opened.
79
99
  """
80
100
  pin_mounted = yield from bps.rd(robot.gonio_pin_sensor)
81
101
  if pin_mounted == PinMounted.PIN_MOUNTED:
@@ -90,6 +110,9 @@ def _prepare_beamline_for_scintillator_images(
90
110
  yield from bps.abs_set(backlight, core_INOUT.OUT, group=group)
91
111
 
92
112
  yield from bps.abs_set(scintillator.selected_pos, InOut.IN, group=group)
113
+ # Not waiting for control mode to be set before opening shutter can result in timeout error
114
+ yield from bps.abs_set(shutter.control_mode, ZebraShutterControl.MANUAL, wait=True)
115
+ yield from bps.abs_set(shutter, ZebraShutterState.OPEN, group=group)
93
116
 
94
117
 
95
118
  def take_and_save_oav_image(
@@ -109,7 +132,198 @@ def take_and_save_oav_image(
109
132
  if not os.path.exists(full_file_path):
110
133
  yield from bps.abs_set(oav.snapshot.filename, file_name, group=group)
111
134
  yield from bps.abs_set(oav.snapshot.directory, file_path, group=group)
112
- yield from bps.wait(group)
135
+ yield from bps.wait(group, timeout=60)
113
136
  yield from bps.trigger(oav.snapshot, wait=True)
114
137
  else:
115
138
  raise FileExistsError("OAV image file path already exists")
139
+
140
+
141
+ def _max_pixel_at_transmission(
142
+ max_pixel: MaxPixel,
143
+ attenuator: BinaryFilterAttenuator,
144
+ xbpm_feedback: XBPMFeedback,
145
+ transmission: float,
146
+ ):
147
+ # Potential controls issue on XBPM device means it can mark
148
+ # itself as stable before it really is
149
+ yield from bps.trigger(xbpm_feedback, wait=True)
150
+ yield from bps.mv(attenuator, transmission)
151
+ yield from bps.trigger(max_pixel, wait=True)
152
+ return (yield from bps.rd(max_pixel.max_pixel_val))
153
+
154
+
155
+ def optimise_transmission_with_oav(
156
+ upper_bound: float = 100,
157
+ lower_bound: float = 0,
158
+ target_brightness_fraction: float = 0.75,
159
+ min_transmission_change: float = 5,
160
+ max_iterations: int = 10,
161
+ max_pixel: MaxPixel = inject("max_pixel"),
162
+ attenuator: BinaryFilterAttenuator = inject("attenuator"),
163
+ xbpm_feedback: XBPMFeedback = inject("xbpm_feedback"),
164
+ ) -> MsgGenerator:
165
+ """
166
+ Plan to find the optimal oav transmission. First the brightest pixel at 100%
167
+ transmission is taken. A fraction of this (target_brightness_fraction) is taken
168
+ as the target - as in the optimal transmission will have it's max pixel as the set
169
+ target. A binary search is used to reach this.
170
+ Args:
171
+ upper_bound: Maximum transmission which will be searched. In percent.
172
+ lower_bound: Minimum transmission which will be searched. In percent.
173
+ target_brightness_fraction: Fraction of the brightest pixel at 100%
174
+ transmission which should be used as the target max pixel brightness.
175
+ min_transmission_change: If the next search point would require a transmission
176
+ change less than this then we stop.
177
+ max_iterations: Maximum amount of iterations.
178
+ """
179
+
180
+ if upper_bound < lower_bound:
181
+ raise ValueError(
182
+ f"Upper bound ({upper_bound}) must be higher than lower bound {lower_bound}"
183
+ )
184
+
185
+ brightest_pixel_at_full_beam = yield from _max_pixel_at_transmission(
186
+ max_pixel, attenuator, xbpm_feedback, 1
187
+ )
188
+
189
+ if brightest_pixel_at_full_beam == 0:
190
+ raise ValueError("No beam found at full transmission")
191
+
192
+ target_pixel_brightness = brightest_pixel_at_full_beam * target_brightness_fraction
193
+ LOGGER.info(
194
+ f"Optimising until max pixel in image has a value of {target_pixel_brightness}"
195
+ )
196
+
197
+ iterations = 0
198
+
199
+ while iterations < max_iterations:
200
+ mid = round((upper_bound + lower_bound) / 2, 2) # limit to 2 dp
201
+ LOGGER.info(f"On iteration {iterations}")
202
+
203
+ current_transmission = yield from bps.rd(attenuator.actual_transmission)
204
+ transmission_change_percent = abs(mid - current_transmission * 100)
205
+ if transmission_change_percent < min_transmission_change:
206
+ LOGGER.info(
207
+ f"Next transmission change would be small ({transmission_change_percent}%) so stopping at {current_transmission}"
208
+ )
209
+ return
210
+
211
+ brightest_pixel = yield from _max_pixel_at_transmission(
212
+ max_pixel, attenuator, xbpm_feedback, mid / 100
213
+ )
214
+
215
+ LOGGER.info(f"Upper bound is: {upper_bound}, Lower bound is: {lower_bound}")
216
+ LOGGER.info(
217
+ f"Testing transmission {mid}, brightest pixel found {brightest_pixel}"
218
+ )
219
+
220
+ if target_pixel_brightness == brightest_pixel:
221
+ mid = round(mid, 0)
222
+ LOGGER.info(f"\nOptimal transmission found: {mid}")
223
+ return
224
+
225
+ # condition for too low so want to try higher
226
+ elif brightest_pixel < target_pixel_brightness:
227
+ LOGGER.info("Result: Too low \n")
228
+ lower_bound = mid
229
+
230
+ # condition for too high so want to try lower
231
+ elif brightest_pixel > target_pixel_brightness:
232
+ LOGGER.info("Result: Too high \n")
233
+ upper_bound = mid
234
+ iterations += 1
235
+ raise StopIteration("Max iterations reached")
236
+
237
+
238
+ def _get_all_zoom_levels(
239
+ zoom_controller: ZoomControllerWithBeamCentres,
240
+ ) -> MsgGenerator[tuple[str]]:
241
+ zoom_levels = []
242
+ level_signals = [
243
+ centring_device.level_name
244
+ for centring_device in zoom_controller.beam_centres.values()
245
+ ]
246
+ for signal in level_signals:
247
+ level_name = yield from bps.rd(signal)
248
+ if level_name:
249
+ zoom_levels.append(level_name)
250
+ return tuple(zoom_levels)
251
+
252
+
253
+ def find_beam_centres(
254
+ zoom_levels_to_centre: tuple[str, ...] | None = None,
255
+ zoom_levels_to_optimise_transmission: tuple[str, ...] = ("1.0x", "7.5x"),
256
+ robot: BartRobot = inject("robot"),
257
+ beamstop: Beamstop = inject("beamstop"),
258
+ backlight: Backlight = inject("backlight"),
259
+ scintillator: Scintillator = inject("scintillator"),
260
+ xbpm_feedback: XBPMFeedback = inject("xbpm_feedback"),
261
+ max_pixel: MaxPixel = inject("max_pixel"),
262
+ centre_ellipse: CentreEllipseMethod = inject("beam_centre"),
263
+ attenuator: BinaryFilterAttenuator = inject("attenuator"),
264
+ zoom_controller: ZoomControllerWithBeamCentres = inject("zoom_controller"),
265
+ shutter: ZebraShutter = inject("sample_shutter"),
266
+ ) -> MsgGenerator:
267
+ """
268
+ Finds beam centres at the zoom levels given by zoom_levels_to_centre, first
269
+ optimising transmission if the zoom level is in zoom_levels_to_optimise_transmission.
270
+
271
+ Note that the previous beam centre values are used to draw an ROI box around the
272
+ OAV image when finding updated beam centre. If the previous values are very wrong,
273
+ this plan may fail or give inaccurate results.
274
+
275
+ zoom_levels_to_centre: The levels to do centring at, by default runs at all known
276
+ zoom levels.
277
+ zoom_levels_to_optimise_transmission: The levels to optimise transmission at,
278
+ defaults to 1x and 7.5x
279
+ """
280
+
281
+ all_zooms = yield from _get_all_zoom_levels(zoom_controller)
282
+ if zoom_levels_to_centre is None:
283
+ zoom_levels_to_centre = all_zooms
284
+
285
+ for zoom in [*zoom_levels_to_optimise_transmission, *zoom_levels_to_centre]:
286
+ if zoom not in all_zooms:
287
+ raise ValueError(f"Unknown zoom ({zoom}). Known zooms are {all_zooms}")
288
+
289
+ LOGGER.info("Preparing beamline for images...")
290
+ yield from _prepare_beamline_for_scintillator_images(
291
+ robot,
292
+ beamstop,
293
+ backlight,
294
+ scintillator,
295
+ xbpm_feedback,
296
+ shutter,
297
+ OAV_PREPARE_BEAMLINE_FOR_SCINT_WAIT,
298
+ )
299
+ LOGGER.info("Waiting for prepare beamline plan to complete...")
300
+ yield from bps.wait(OAV_PREPARE_BEAMLINE_FOR_SCINT_WAIT)
301
+
302
+ for centring_device in zoom_controller.beam_centres.values():
303
+ zoom_name = yield from bps.rd(centring_device.level_name)
304
+ if zoom_name in zoom_levels_to_centre:
305
+ LOGGER.info(f"Moving to zoom level {zoom_name}")
306
+ yield from bps.abs_set(zoom_controller, zoom_name, wait=True)
307
+ if zoom_name in zoom_levels_to_optimise_transmission:
308
+ LOGGER.info(f"Optimising transmission at zoom level {zoom_name}")
309
+ yield from optimise_transmission_with_oav(
310
+ 100,
311
+ 0,
312
+ max_pixel=max_pixel,
313
+ attenuator=attenuator,
314
+ xbpm_feedback=xbpm_feedback,
315
+ )
316
+
317
+ yield from bps.trigger(centre_ellipse, wait=True)
318
+ centre_x = yield from bps.rd(centre_ellipse.center_x_val)
319
+ centre_y = yield from bps.rd(centre_ellipse.center_y_val)
320
+ centre_x = round(centre_x)
321
+ centre_y = round(centre_y)
322
+ LOGGER.info(
323
+ f"Writing centre values ({centre_x}, {centre_y}) to OAV PVs at zoom level {zoom_name}"
324
+ )
325
+ yield from bps.mv(
326
+ centring_device.x_centre, centre_x, centring_device.y_centre, centre_y
327
+ )
328
+
329
+ LOGGER.info("Find beam centre plan completed!")
@@ -8,8 +8,11 @@ from typing import TypedDict
8
8
 
9
9
  import numpy as np
10
10
  import zmq
11
- from dodal.devices.i04.constants import RedisConstants
12
- from dodal.devices.i04.murko_results import RESULTS_COMPLETE_MESSAGE, MurkoResult
11
+ from dodal.devices.beamlines.i04.constants import RedisConstants
12
+ from dodal.devices.beamlines.i04.murko_results import (
13
+ RESULTS_COMPLETE_MESSAGE,
14
+ MurkoResult,
15
+ )
13
16
  from numpy.typing import NDArray
14
17
  from PIL import Image
15
18
  from redis import StrictRedis
@@ -3,8 +3,8 @@ import bluesky.preprocessors as bpp
3
3
  from bluesky.preprocessors import contingency_decorator, run_decorator, subs_decorator
4
4
  from bluesky.utils import MsgGenerator
5
5
  from dodal.common import inject
6
- from dodal.devices.i04.constants import RedisConstants
7
- from dodal.devices.i04.murko_results import MurkoResultsDevice
6
+ from dodal.devices.beamlines.i04.constants import RedisConstants
7
+ from dodal.devices.beamlines.i04.murko_results import MurkoResultsDevice
8
8
  from dodal.devices.oav.oav_to_redis_forwarder import OAVToRedisForwarder, Source
9
9
  from dodal.devices.robot import BartRobot
10
10
  from dodal.devices.smargon import Smargon
@@ -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.beamlines.i24.aperture import Aperture
6
+ from dodal.devices.beamlines.i24.beamstop import Beamstop
7
+ from dodal.devices.beamlines.i24.commissioning_jungfrau import CommissioningJungfrau
8
+ from dodal.devices.beamlines.i24.dcm import DCM
9
+ from dodal.devices.beamlines.i24.dual_backlight import DualBacklight
10
+ from dodal.devices.beamlines.i24.vgonio import VerticalGoniometer
11
+ from dodal.devices.hutch_shutter import HutchShutter
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