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
@@ -55,12 +55,12 @@ def get_zebra_settings_for_extruder(
55
55
 
56
56
 
57
57
  def arm_zebra(zebra: Zebra):
58
- yield from bps.abs_set(zebra.pc.arm, ArmDemand.ARM, wait=True) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
58
+ yield from bps.abs_set(zebra.pc.arm, ArmDemand.ARM, wait=True)
59
59
  SSX_LOGGER.info("Zebra armed.")
60
60
 
61
61
 
62
62
  def disarm_zebra(zebra: Zebra):
63
- yield from bps.abs_set(zebra.pc.arm, ArmDemand.DISARM, wait=True) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
63
+ yield from bps.abs_set(zebra.pc.arm, ArmDemand.DISARM, wait=True)
64
64
  SSX_LOGGER.info("Zebra disarmed.")
65
65
 
66
66
 
@@ -272,7 +272,7 @@ def setup_zebra_for_fastchip_plan(
272
272
  det_type: str,
273
273
  num_gates: int,
274
274
  num_exposures: int,
275
- exposure_time: float,
275
+ exposure_time_s: float,
276
276
  start_time_offset: float = 0.0,
277
277
  group: str = "setup_zebra_for_fastchip",
278
278
  wait: bool = True,
@@ -303,7 +303,7 @@ def setup_zebra_for_fastchip_plan(
303
303
  det_type (str): Detector in use, current choices are Eiger or Pilatus.
304
304
  num_gates (int): Number of apertures to visit in a chip.
305
305
  num_exposures (int): Number of times data is collected in each aperture.
306
- exposure_time (float): Exposure time for each shot.
306
+ exposure_time_s (float): Exposure time for each shot.
307
307
  start_time_offset (float): Delay on the start of the position compare. \
308
308
  Defaults to 0.0 (standard chip collection).
309
309
  """
@@ -341,10 +341,12 @@ def setup_zebra_for_fastchip_plan(
341
341
  )
342
342
 
343
343
  # Square wave - needs a small drop to make it work for eiger
344
- pulse_width = exposure_time - 0.0001 if det_type == "eiger" else exposure_time / 2
344
+ pulse_width = (
345
+ exposure_time_s - 0.0001 if det_type == "eiger" else exposure_time_s / 2
346
+ )
345
347
 
346
348
  # 100us buffer needed to avoid missing some of the triggers
347
- exptime_buffer = exposure_time + 0.0001
349
+ exptime_buffer = exposure_time_s + 0.0001
348
350
 
349
351
  # Number of gates is the number of windows collected
350
352
  yield from bps.abs_set(zebra.pc.num_gates, num_gates, group=group)
@@ -362,7 +364,7 @@ def setup_zebra_for_fastchip_plan(
362
364
  def open_fast_shutter_at_each_position_plan(
363
365
  zebra: Zebra,
364
366
  num_exposures: int,
365
- exposure_time: float,
367
+ exposure_time_s: float,
366
368
  group: str = "fast_shutter_control",
367
369
  wait: bool = True,
368
370
  ):
@@ -384,7 +386,7 @@ def open_fast_shutter_at_each_position_plan(
384
386
  Args:
385
387
  zebra (Zebra): The zebra ophyd device.
386
388
  num_exposures (int): Number of times data is collected in each aperture.
387
- exposure_time (float): Exposure time for each shot.
389
+ exposure_time_s (float): Exposure time for each shot.
388
390
  """
389
391
  SSX_LOGGER.info(
390
392
  "ZEBRA setup for fastchip collection with long delays between exposures."
@@ -395,7 +397,7 @@ def open_fast_shutter_at_each_position_plan(
395
397
  zebra.output.pulse_2.input, zebra.mapping.sources.PC_GATE, group=group
396
398
  )
397
399
  yield from bps.abs_set(zebra.output.pulse_2.delay, 0.0, group=group)
398
- pulse2_width = num_exposures * exposure_time + SHUTTER_OPEN_TIME
400
+ pulse2_width = num_exposures * exposure_time_s + SHUTTER_OPEN_TIME
399
401
  yield from bps.abs_set(zebra.output.pulse_2.width, pulse2_width, group=group)
400
402
 
401
403
  # Fast shutter
@@ -0,0 +1,109 @@
1
+ # from collections.abc import Sequence
2
+ from typing import Literal
3
+
4
+ import bluesky.plan_stubs as bps
5
+ import bluesky.preprocessors as bpp
6
+ from blueapi.core import MsgGenerator
7
+ from dodal.beamlines import i24
8
+
9
+ from mx_bluesky.beamlines.i24.serial.fixed_target.ft_utils import (
10
+ ChipType,
11
+ MappingType,
12
+ PumpProbeSetting,
13
+ )
14
+ from mx_bluesky.beamlines.i24.serial.fixed_target.i24ssx_moveonclick import (
15
+ _move_on_mouse_click_plan,
16
+ )
17
+ from mx_bluesky.beamlines.i24.serial.log import _read_visit_directory_from_file
18
+ from mx_bluesky.beamlines.i24.serial.parameters import (
19
+ FixedTargetParameters,
20
+ get_chip_format,
21
+ )
22
+ from mx_bluesky.beamlines.i24.serial.setup_beamline import pv
23
+ from mx_bluesky.beamlines.i24.serial.setup_beamline.ca import caput
24
+ from mx_bluesky.beamlines.i24.serial.setup_beamline.pv_abstract import Eiger, Pilatus
25
+ from mx_bluesky.beamlines.i24.serial.setup_beamline.setup_detector import (
26
+ _move_detector_stage,
27
+ get_detector_type,
28
+ )
29
+
30
+
31
+ @bpp.run_decorator()
32
+ def gui_stage_move_on_click(position_px: tuple[int, int]) -> MsgGenerator:
33
+ oav = i24.oav()
34
+ pmac = i24.pmac()
35
+ yield from _move_on_mouse_click_plan(oav, pmac, position_px)
36
+
37
+
38
+ @bpp.run_decorator()
39
+ def gui_gonio_move_on_click(position_px: tuple[int, int]) -> MsgGenerator:
40
+ oav = i24.oav()
41
+ gonio = i24.vgonio()
42
+
43
+ x_pixels_per_micron = yield from bps.rd(oav.microns_per_pixel_x)
44
+ y_pixels_per_micron = yield from bps.rd(oav.microns_per_pixel_y)
45
+
46
+ x_um = position_px[0] * x_pixels_per_micron
47
+ y_um = position_px[1] * y_pixels_per_micron
48
+
49
+ # gonio is in mm?
50
+ yield from bps.mv(gonio.x, x_um / 1000, gonio.yh, y_um / 1000) # type: ignore
51
+
52
+
53
+ # See https://github.com/DiamondLightSource/mx-bluesky/issues/853
54
+ @bpp.run_decorator()
55
+ def gui_sleep(sec: int) -> MsgGenerator:
56
+ for _ in range(sec):
57
+ yield from bps.sleep(1)
58
+
59
+
60
+ @bpp.run_decorator()
61
+ def gui_move_detector(det: Literal["eiger", "pilatus"]) -> MsgGenerator:
62
+ detector_stage = i24.detector_motion()
63
+ det_y_target = Eiger.det_y_target if det == "eiger" else Pilatus.det_y_target
64
+ yield from _move_detector_stage(detector_stage, det_y_target)
65
+ # Make the output readable
66
+ caput(pv.me14e_gp101, det)
67
+
68
+
69
+ @bpp.run_decorator()
70
+ def gui_set_parameters(
71
+ sub_dir: str,
72
+ chip_name: str,
73
+ exp_time: float,
74
+ det_dist: float,
75
+ transmission: float,
76
+ n_shots: int,
77
+ chip_type: str,
78
+ checker_pattern: bool,
79
+ pump_probe: str,
80
+ laser_dwell: float,
81
+ laser_delay: float,
82
+ pre_pump: float,
83
+ ) -> MsgGenerator:
84
+ # NOTE still a work in progress, adding to it as the ui grows
85
+ detector_stage = i24.detector_motion()
86
+ det_type = yield from get_detector_type(detector_stage)
87
+ chip_params = get_chip_format(ChipType[chip_type])
88
+
89
+ params = {
90
+ "visit": _read_visit_directory_from_file().as_posix(), # noqa
91
+ "directory": sub_dir,
92
+ "filename": chip_name,
93
+ "exposure_time_s": exp_time,
94
+ "detector_distance_mm": det_dist,
95
+ "detector_name": str(det_type),
96
+ "num_exposures": n_shots,
97
+ "transmission": transmission,
98
+ "chip": chip_params,
99
+ "map_type": MappingType.NoMap,
100
+ "chip_map": [],
101
+ "pump_repeat": PumpProbeSetting[pump_probe], # pump_repeat,
102
+ "laser_dwell_s": laser_dwell,
103
+ "laser_delay_s": laser_delay,
104
+ "checker_pattern": checker_pattern,
105
+ "pre_pump_exposure_s": pre_pump,
106
+ }
107
+ print(FixedTargetParameters(**params))
108
+ # This will then run the run_fixed_target plan
109
+ yield from bps.sleep(0.5)
@@ -4,6 +4,7 @@ import pprint
4
4
  import time
5
5
  from datetime import datetime
6
6
 
7
+ import bluesky.plan_stubs as bps
7
8
  import requests
8
9
 
9
10
  from mx_bluesky.beamlines.i24.serial.fixed_target.ft_utils import ChipType, MappingType
@@ -12,7 +13,7 @@ from mx_bluesky.beamlines.i24.serial.parameters import (
12
13
  ExtruderParameters,
13
14
  FixedTargetParameters,
14
15
  )
15
- from mx_bluesky.beamlines.i24.serial.setup_beamline import Eiger, caget
16
+ from mx_bluesky.beamlines.i24.serial.setup_beamline import Eiger, caget, cagetstring
16
17
 
17
18
 
18
19
  def call_nexgen(
@@ -53,7 +54,7 @@ def call_nexgen(
53
54
  total_numb_imgs = parameters.num_images
54
55
  pump_status = parameters.pump_status
55
56
 
56
- filename_prefix = parameters.filename
57
+ filename_prefix = cagetstring(Eiger.pv.filenameRBV)
57
58
  meta_h5 = parameters.visit / parameters.directory / f"{filename_prefix}_meta.h5"
58
59
  t0 = time.time()
59
60
  max_wait = 60 # seconds
@@ -61,10 +62,10 @@ def call_nexgen(
61
62
  while time.time() - t0 < max_wait:
62
63
  if meta_h5.exists():
63
64
  SSX_LOGGER.info(f"Found {meta_h5} after {time.time() - t0:.1f} seconds")
64
- time.sleep(5)
65
+ yield from bps.sleep(5)
65
66
  break
66
67
  SSX_LOGGER.debug(f"Waiting for {meta_h5}")
67
- time.sleep(1)
68
+ yield from bps.sleep(1)
68
69
  if not meta_h5.exists():
69
70
  SSX_LOGGER.warning(f"Giving up waiting for {meta_h5} after {max_wait} seconds")
70
71
  return
@@ -0,0 +1,45 @@
1
+ from bluesky import plan_stubs as bps
2
+ from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
3
+ from dodal.devices.xbpm_feedback import Pause, XBPMFeedback
4
+
5
+ from mx_bluesky.common.utils.log import LOGGER
6
+
7
+
8
+ def unpause_xbpm_feedback_and_set_transmission_to_1(
9
+ xbpm_feedback: XBPMFeedback, attenuator: BinaryFilterAttenuator
10
+ ):
11
+ """Turns the XBPM feedback back on and sets transmission to 1 so that it keeps the
12
+ beam aligned whilst not collecting.
13
+
14
+ Args:
15
+ xbpm_feedback (XBPMFeedback): The XBPM device that is responsible for keeping
16
+ the beam in position
17
+ attenuator (BinaryFilterAttenuator): The attenuator used to set transmission
18
+ """
19
+ yield from bps.mv(xbpm_feedback.pause_feedback, Pause.RUN, attenuator, 1.0) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
20
+
21
+
22
+ def check_and_pause_feedback(
23
+ xbpm_feedback: XBPMFeedback,
24
+ attenuator: BinaryFilterAttenuator,
25
+ desired_transmission_fraction: float,
26
+ ):
27
+ """Checks that the xbpm is in position before then turning it off and setting a new
28
+ transmission.
29
+
30
+ Args:
31
+ xbpm_feedback (XBPMFeedback): The XBPM device that is responsible for keeping
32
+ the beam in position
33
+ attenuator (BinaryFilterAttenuator): The attenuator used to set transmission
34
+ desired_transmission_fraction (float): The desired transmission to set after
35
+ turning XBPM feedback off.
36
+
37
+ """
38
+ yield from bps.mv(attenuator, 1.0) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
39
+ LOGGER.info("Waiting for XBPM feedback to be stable")
40
+ yield from bps.trigger(xbpm_feedback, wait=True)
41
+ LOGGER.info(
42
+ f"XPBM feedback in position, pausing and setting transmission to {desired_transmission_fraction}"
43
+ )
44
+ yield from bps.mv(xbpm_feedback.pause_feedback, Pause.PAUSE) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
45
+ yield from bps.mv(attenuator, desired_transmission_fraction) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
@@ -121,10 +121,8 @@ class BaseISPyBCallback(PlanReactiveCallback):
121
121
  ISPYB_ZOCALO_CALLBACK_LOGGER.info(
122
122
  "ISPyB handler received event from read hardware"
123
123
  )
124
- assert isinstance(
125
- synchrotron_mode := doc["data"]["synchrotron-synchrotron_mode"],
126
- SynchrotronMode,
127
- )
124
+ synchrotron_mode = doc["data"]["synchrotron-synchrotron_mode"]
125
+ assert isinstance(synchrotron_mode, SynchrotronMode)
128
126
 
129
127
  hwscan_data_collection_info = DataCollectionInfo(
130
128
  undulator_gap1=doc["data"]["undulator-current_gap"],
@@ -35,7 +35,7 @@ def populate_remaining_data_collection_info(
35
35
  data_collection_info.detector_id = I03_EIGER_DETECTOR
36
36
  data_collection_info.comments = comment
37
37
  data_collection_info.detector_distance = params.detector_params.detector_distance
38
- data_collection_info.exp_time = params.detector_params.exposure_time
38
+ data_collection_info.exp_time = params.detector_params.exposure_time_s
39
39
  data_collection_info.imgdir = params.detector_params.directory
40
40
  data_collection_info.imgprefix = params.detector_params.prefix
41
41
  data_collection_info.imgsuffix = EIGER_FILE_SUFFIX
@@ -36,7 +36,7 @@ class PlanReactiveCallback(CallbackBase):
36
36
  super().__init__(emit=emit)
37
37
  self.emit_cb = emit # to avoid GC; base class only holds a WeakRef
38
38
  self.active = False
39
- self.activity_uid = 0
39
+ self.activity_uid = ""
40
40
  self.log = log
41
41
 
42
42
  def _run_activity_gated(self, name: str, func, doc, override=False):
@@ -76,7 +76,7 @@ class PlanReactiveCallback(CallbackBase):
76
76
  do_stop = self.active
77
77
  if doc.get("run_start") == self.activity_uid:
78
78
  self.active = False
79
- self.activity_uid = 0
79
+ self.activity_uid = ""
80
80
  return (
81
81
  self._run_activity_gated(
82
82
  "stop", self.activity_gated_stop, doc, override=True
@@ -18,10 +18,11 @@ if TYPE_CHECKING:
18
18
 
19
19
  class ZocaloCallback(CallbackBase):
20
20
  """Callback class to handle the triggering of Zocalo processing.
21
- Sends zocalo a run_start signal on receiving a start document for the specified
22
- sub-plan, and sends a run_end signal on receiving a stop document for the same plan.
21
+ Will start listening for collections when {triggering_plan} has been started.
23
22
 
24
- The metadata of the sub-plan this starts on must include a zocalo_environment.
23
+ For every ispyb deposition that occurs inside this run the callback will send zocalo
24
+ a run_start signal. Once the {triggering_plan} has ended the callback will send a
25
+ run_end signal for all collections.
25
26
 
26
27
  Shouldn't be subscribed directly to the RunEngine, instead should be passed to the
27
28
  `emit` argument of an ISPyB callback which appends DCIDs to the relevant start doc.
@@ -30,7 +31,9 @@ class ZocaloCallback(CallbackBase):
30
31
  def _reset_state(self):
31
32
  self.run_uid: str | None = None
32
33
  self.zocalo_info: list[ZocaloStartInfo] = []
34
+ self._started_zocalo_collections: list[ZocaloStartInfo] = []
33
35
  self.descriptors: dict[str, EventDescriptor] = {}
36
+ self.start_frame = 0
34
37
 
35
38
  def __init__(self, triggering_plan: str, zocalo_environment: str):
36
39
  super().__init__()
@@ -42,26 +45,21 @@ class ZocaloCallback(CallbackBase):
42
45
  ISPYB_ZOCALO_CALLBACK_LOGGER.info("Zocalo handler received start document.")
43
46
  if self.triggering_plan and doc.get("subplan_name") == self.triggering_plan:
44
47
  self.run_uid = doc.get("uid")
45
- assert isinstance(scan_points := doc.get("scan_points"), list)
48
+ if self.run_uid:
46
49
  if (
47
- isinstance(ispyb_ids := doc.get("ispyb_dcids"), tuple)
50
+ isinstance(scan_points := doc.get("scan_points"), list)
51
+ and isinstance(ispyb_ids := doc.get("ispyb_dcids"), tuple)
48
52
  and len(ispyb_ids) > 0
49
53
  ):
50
54
  ISPYB_ZOCALO_CALLBACK_LOGGER.info(f"Zocalo triggering for {ispyb_ids}")
51
55
  ids_and_shape = list(zip(ispyb_ids, scan_points, strict=False))
52
- start_frame = 0
53
- self.zocalo_info = []
54
56
  for idx, id_and_shape in enumerate(ids_and_shape):
55
57
  id, shape = id_and_shape
56
58
  num_frames = number_of_frames_from_scan_spec(shape)
57
59
  self.zocalo_info.append(
58
- ZocaloStartInfo(id, None, start_frame, num_frames, idx)
60
+ ZocaloStartInfo(id, None, self.start_frame, num_frames, idx)
59
61
  )
60
- start_frame += num_frames
61
- else:
62
- raise ISPyBDepositionNotMade(
63
- f"No ISPyB IDs received by the start of {self.triggering_plan=}"
64
- )
62
+ self.start_frame += num_frames
65
63
 
66
64
  def descriptor(self, doc: EventDescriptor):
67
65
  self.descriptors[doc["uid"]] = doc
@@ -73,6 +71,8 @@ class ZocaloCallback(CallbackBase):
73
71
  for start_info in self.zocalo_info:
74
72
  start_info.filename = filename
75
73
  self.zocalo_interactor.run_start(start_info)
74
+ self._started_zocalo_collections.append(start_info)
75
+ self.zocalo_info = []
76
76
  return doc
77
77
 
78
78
  def stop(self, doc: RunStop):
@@ -80,7 +80,10 @@ class ZocaloCallback(CallbackBase):
80
80
  ISPYB_ZOCALO_CALLBACK_LOGGER.info(
81
81
  f"Zocalo handler received stop document, for run {doc.get('run_start')}."
82
82
  )
83
- assert self.zocalo_interactor is not None
84
- for info in self.zocalo_info:
83
+ if not self._started_zocalo_collections:
84
+ raise ISPyBDepositionNotMade(
85
+ f"No ISPyB IDs received by the end of {self.triggering_plan=}"
86
+ )
87
+ for info in self._started_zocalo_collections:
85
88
  self.zocalo_interactor.run_end(info.ispyb_dcid)
86
89
  self._reset_state()
@@ -15,30 +15,43 @@ class SampleHandlingCallback(PlanReactiveCallback):
15
15
  """Intercepts exceptions from experiment plans and updates the ISPyB BLSampleStatus
16
16
  field according to the type of exception raised."""
17
17
 
18
- def __init__(self):
18
+ def __init__(self, record_loaded_on_success=False):
19
19
  super().__init__(log=ISPYB_ZOCALO_CALLBACK_LOGGER)
20
20
  self._sample_id: int | None = None
21
21
  self._descriptor: str | None = None
22
+ self._run_id: str | None = None
23
+
24
+ # Record 'sample loaded' if document successfully stops
25
+ self.record_loaded_on_success = record_loaded_on_success
22
26
 
23
27
  def activity_gated_start(self, doc: RunStart):
24
- if not self._sample_id:
28
+ if not self._sample_id and self.active:
25
29
  sample_id = doc.get("metadata", {}).get("sample_id")
26
30
  self.log.info(f"Recording sample ID at run start {sample_id}")
27
31
  self._sample_id = sample_id
32
+ self._run_id = self.activity_uid
28
33
 
29
34
  def activity_gated_stop(self, doc: RunStop) -> RunStop:
30
- if doc["exit_status"] != "success":
31
- exception_type, message = SampleException.type_and_message_from_reason(
32
- doc.get("reason", "")
33
- )
34
- self.log.info(
35
- f"Sample handling callback intercepted exception of type {exception_type}: {message}"
36
- )
37
- self._record_exception(exception_type)
35
+ if self._run_id == doc.get("run_start"):
36
+ expeye = ExpeyeInteraction()
37
+ if doc["exit_status"] != "success":
38
+ exception_type, message = SampleException.type_and_message_from_reason(
39
+ doc.get("reason", "")
40
+ )
41
+ self.log.info(
42
+ f"Sample handling callback intercepted exception of type {exception_type}: {message}"
43
+ )
44
+ self._record_exception(exception_type, expeye)
45
+
46
+ elif self.record_loaded_on_success:
47
+ self._record_loaded(expeye)
48
+
49
+ self._sample_id = None
50
+ self._run_id = None
51
+
38
52
  return doc
39
53
 
40
- def _record_exception(self, exception_type: str):
41
- expeye = ExpeyeInteraction()
54
+ def _record_exception(self, exception_type: str, expeye: ExpeyeInteraction):
42
55
  assert self._sample_id, "Unable to record exception due to no sample ID"
43
56
  sample_status = self._decode_sample_status(exception_type)
44
57
  expeye.update_sample_status(self._sample_id, sample_status)
@@ -48,3 +61,7 @@ class SampleHandlingCallback(PlanReactiveCallback):
48
61
  case SampleException.__name__ | CrystalNotFoundException.__name__:
49
62
  return BLSampleStatus.ERROR_SAMPLE
50
63
  return BLSampleStatus.ERROR_BEAMLINE
64
+
65
+ def _record_loaded(self, expeye: ExpeyeInteraction):
66
+ assert self._sample_id, "Unable to record loaded state due to no sample ID"
67
+ expeye.update_sample_status(self._sample_id, BLSampleStatus.LOADED)
@@ -29,6 +29,7 @@ from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping
29
29
  )
30
30
  from mx_bluesky.common.external_interaction.ispyb.data_model import (
31
31
  DataCollectionGridInfo,
32
+ DataCollectionGroupInfo,
32
33
  DataCollectionInfo,
33
34
  DataCollectionPositionInfo,
34
35
  Orientation,
@@ -52,6 +53,9 @@ from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, set_dcgid_
52
53
  if TYPE_CHECKING:
53
54
  from event_model import Event, RunStart, RunStop
54
55
 
56
+ T = TypeVar("T", bound="GridCommon")
57
+ ASSERT_START_BEFORE_EVENT_DOC_MESSAGE = f"No data collection group info - event document has been emitted before a {PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN} start document"
58
+
55
59
 
56
60
  def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters):
57
61
  return bpp.set_run_key_wrapper(
@@ -67,9 +71,6 @@ def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters):
67
71
  )
68
72
 
69
73
 
70
- T = TypeVar("T", bound="GridCommon")
71
-
72
-
73
74
  class GridscanISPyBCallback(BaseISPyBCallback):
74
75
  """Callback class to handle the deposition of experiment parameters into the ISPyB
75
76
  database. Listens for 'event' and 'descriptor' documents. Creates the ISpyB entry on
@@ -97,6 +98,7 @@ class GridscanISPyBCallback(BaseISPyBCallback):
97
98
  self.param_type = param_type
98
99
  self._start_of_fgs_uid: str | None = None
99
100
  self._processing_start_time: float | None = None
101
+ self.data_collection_group_info: DataCollectionGroupInfo | None
100
102
 
101
103
  def activity_gated_start(self, doc: RunStart):
102
104
  if doc.get("subplan_name") == PlanNameConstants.DO_FGS:
@@ -111,12 +113,14 @@ class GridscanISPyBCallback(BaseISPyBCallback):
111
113
  assert isinstance(mx_bluesky_parameters, str)
112
114
  self.params = self.param_type.model_validate_json(mx_bluesky_parameters)
113
115
  self.ispyb = StoreInIspyb(self.ispyb_config)
114
- data_collection_group_info = populate_data_collection_group(self.params)
116
+ self.data_collection_group_info = populate_data_collection_group(
117
+ self.params
118
+ )
115
119
 
116
120
  scan_data_infos = [
117
121
  ScanDataInfo(
118
122
  data_collection_info=populate_remaining_data_collection_info(
119
- None,
123
+ "MX-Bluesky: Xray centring 1 -",
120
124
  None,
121
125
  populate_xy_data_collection_info(
122
126
  self.params.detector_params,
@@ -126,7 +130,7 @@ class GridscanISPyBCallback(BaseISPyBCallback):
126
130
  ),
127
131
  ScanDataInfo(
128
132
  data_collection_info=populate_remaining_data_collection_info(
129
- None,
133
+ "MX-Bluesky: Xray centring 2 -",
130
134
  None,
131
135
  populate_xz_data_collection_info(self.params.detector_params),
132
136
  self.params,
@@ -135,12 +139,13 @@ class GridscanISPyBCallback(BaseISPyBCallback):
135
139
  ]
136
140
 
137
141
  self.ispyb_ids = self.ispyb.begin_deposition(
138
- data_collection_group_info, scan_data_infos
142
+ self.data_collection_group_info, scan_data_infos
139
143
  )
140
144
  set_dcgid_tag(self.ispyb_ids.data_collection_group_id)
141
145
  return super().activity_gated_start(doc)
142
146
 
143
147
  def activity_gated_event(self, doc: Event):
148
+ assert self.data_collection_group_info, ASSERT_START_BEFORE_EVENT_DOC_MESSAGE
144
149
  doc = super().activity_gated_event(doc)
145
150
 
146
151
  descriptor_name = self.descriptors[doc["descriptor"]].get("name")
@@ -151,10 +156,14 @@ class GridscanISPyBCallback(BaseISPyBCallback):
151
156
  self.ispyb_ids = self.ispyb.update_deposition(
152
157
  self.ispyb_ids, scan_data_infos
153
158
  )
159
+ self.ispyb.update_data_collection_group_table(
160
+ self.data_collection_group_info, self.ispyb_ids.data_collection_group_id
161
+ )
154
162
 
155
163
  return doc
156
164
 
157
165
  def _handle_zocalo_read_event(self, doc):
166
+ assert self.data_collection_group_info, ASSERT_START_BEFORE_EVENT_DOC_MESSAGE
158
167
  crystal_summary = ""
159
168
  if self._processing_start_time is not None:
160
169
  proc_time = time() - self._processing_start_time
@@ -185,6 +194,11 @@ class GridscanISPyBCallback(BaseISPyBCallback):
185
194
  assert self.ispyb_ids.data_collection_ids, (
186
195
  "No data collection to add results to"
187
196
  )
197
+
198
+ self.data_collection_group_info.comments = (
199
+ self.data_collection_group_info.comments or ""
200
+ ) + crystal_summary
201
+
188
202
  self.ispyb.append_to_comment(
189
203
  self.ispyb_ids.data_collection_ids[0], crystal_summary
190
204
  )
@@ -192,6 +206,7 @@ class GridscanISPyBCallback(BaseISPyBCallback):
192
206
  def _handle_oav_grid_snapshot_triggered(self, doc) -> Sequence[ScanDataInfo]:
193
207
  assert self.ispyb_ids.data_collection_ids, "No current data collection"
194
208
  assert self.params, "ISPyB handler didn't receive parameters!"
209
+ assert self.data_collection_group_info, "No data collection group"
195
210
  data = doc["data"]
196
211
  data_collection_id = None
197
212
  data_collection_info = DataCollectionInfo(
@@ -220,6 +235,18 @@ class GridscanISPyBCallback(BaseISPyBCallback):
220
235
  data_collection_info.comments = construct_comment_for_gridscan(
221
236
  data_collection_grid_info
222
237
  )
238
+
239
+ if self.data_collection_group_info.comments:
240
+ self.data_collection_group_info.comments += (
241
+ f"by {data_collection_grid_info.steps_y}."
242
+ )
243
+ else:
244
+ self.data_collection_group_info.comments = (
245
+ f"Diffraction grid scan of "
246
+ f"{data_collection_grid_info.steps_x} "
247
+ f"by {data_collection_grid_info.steps_y} "
248
+ )
249
+
223
250
  if len(self.ispyb_ids.data_collection_ids) > self._oav_snapshot_event_idx:
224
251
  data_collection_id = self.ispyb_ids.data_collection_ids[
225
252
  self._oav_snapshot_event_idx
@@ -275,6 +302,9 @@ class GridscanISPyBCallback(BaseISPyBCallback):
275
302
  return scan_data_infos
276
303
 
277
304
  def activity_gated_stop(self, doc: RunStop) -> RunStop:
305
+ assert self.data_collection_group_info, (
306
+ f"No data collection group info - stop document has been emitted before a {PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN} start document"
307
+ )
278
308
  if doc.get("run_start") == self._start_of_fgs_uid:
279
309
  self._processing_start_time = time()
280
310
  if doc.get("run_start") == self.uid_to_finalize_on:
@@ -289,5 +319,11 @@ class GridscanISPyBCallback(BaseISPyBCallback):
289
319
  )
290
320
  if exception_type:
291
321
  doc["reason"] = message
322
+ self.data_collection_group_info.comments = message
323
+ self.ispyb.update_data_collection_group_table(
324
+ self.data_collection_group_info,
325
+ self.ispyb_ids.data_collection_group_id,
326
+ )
327
+ self.data_collection_group_info = None
292
328
  return super().activity_gated_stop(doc)
293
329
  return self._tag_doc(doc)
@@ -43,7 +43,7 @@ def construct_comment_for_gridscan(grid_info: DataCollectionGridInfo) -> str:
43
43
  grid_info.microns_per_pixel_y,
44
44
  )
45
45
  return (
46
- "MX-Bluesky: Xray centring - Diffraction grid scan of "
46
+ "Diffraction grid scan of "
47
47
  f"{grid_info.steps_x} by "
48
48
  f"{grid_info.steps_y} images in "
49
49
  f"{(grid_info.dx_in_mm * 1e3):.1f} um by "
@@ -79,7 +79,8 @@ class GridscanNexusFileCallback(PlanReactiveCallback):
79
79
  self.descriptors[doc["uid"]] = doc
80
80
 
81
81
  def activity_gated_event(self, doc: Event) -> Event | None:
82
- assert (event_descriptor := self.descriptors.get(doc["descriptor"])) is not None
82
+ event_descriptor = self.descriptors.get(doc["descriptor"])
83
+ assert event_descriptor is not None
83
84
  if event_descriptor.get("name") == DocDescriptorNames.HARDWARE_READ_DURING:
84
85
  data = doc["data"]
85
86
  for nexus_writer in [self.nexus_writer_1, self.nexus_writer_2]:
@@ -13,6 +13,7 @@ class DataCollectionGroupInfo:
13
13
  experiment_type: str
14
14
  sample_id: int | None
15
15
  sample_barcode: str | None = None
16
+ comments: str | None = None
16
17
 
17
18
 
18
19
  @dataclass(kw_only=True)
@@ -2,7 +2,7 @@ import configparser
2
2
  from dataclasses import dataclass
3
3
  from enum import StrEnum
4
4
 
5
- from requests import patch, post
5
+ from requests import JSONDecodeError, patch, post
6
6
  from requests.auth import AuthBase
7
7
 
8
8
  from mx_bluesky.common.external_interaction.ispyb.ispyb_utils import (
@@ -34,7 +34,11 @@ def _get_base_url_and_token() -> tuple[str, str]:
34
34
  def _send_and_get_response(auth, url, data, send_func) -> dict:
35
35
  response = send_func(url, auth=auth, json=data)
36
36
  if not response.ok:
37
- raise ISPyBDepositionNotMade(f"Could not write {data} to {url}: {response}")
37
+ try:
38
+ resp_txt = str(response.json())
39
+ except JSONDecodeError:
40
+ resp_txt = str(response)
41
+ raise ISPyBDepositionNotMade(f"Could not write {data} to {url}: {resp_txt}")
38
42
  return response.json()
39
43
 
40
44