mx-bluesky 1.4.6__py3-none-any.whl → 1.4.8__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 (95) hide show
  1. mx_bluesky/_version.py +2 -2
  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 +35 -29
  6. mx_bluesky/beamlines/i04/thawing_plan.py +18 -3
  7. mx_bluesky/beamlines/i23/__init__.py +3 -0
  8. mx_bluesky/beamlines/i23/serial.py +71 -0
  9. mx_bluesky/beamlines/i24/serial/__init__.py +2 -0
  10. mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +12 -12
  11. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +36 -30
  12. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/pumpprobe-py3v1.edl +3 -3
  13. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +15 -66
  14. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +8 -10
  15. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +10 -3
  16. mx_bluesky/beamlines/i24/serial/log.py +9 -9
  17. mx_bluesky/beamlines/i24/serial/parameters/utils.py +36 -7
  18. mx_bluesky/beamlines/i24/serial/set_visit_directory.sh +1 -1
  19. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +16 -17
  20. mx_bluesky/beamlines/i24/serial/setup_beamline/pv_abstract.py +4 -4
  21. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +51 -52
  22. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +3 -2
  23. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +9 -7
  24. mx_bluesky/beamlines/i24/serial/web_gui_plans/general_plans.py +71 -11
  25. mx_bluesky/beamlines/i24/serial/write_nexus.py +6 -5
  26. mx_bluesky/{hyperion → common}/device_setup_plans/check_beamstop.py +1 -1
  27. mx_bluesky/{hyperion → common}/device_setup_plans/manipulate_sample.py +1 -1
  28. mx_bluesky/{hyperion → common}/device_setup_plans/setup_oav.py +12 -6
  29. mx_bluesky/common/device_setup_plans/xbpm_feedback.py +45 -0
  30. mx_bluesky/{hyperion → common}/experiment_plans/change_aperture_then_move_plan.py +13 -29
  31. mx_bluesky/{hyperion → common}/experiment_plans/oav_grid_detection_plan.py +6 -6
  32. mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +8 -9
  33. mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +1 -1
  34. mx_bluesky/common/external_interaction/callbacks/common/zocalo_callback.py +18 -15
  35. mx_bluesky/{hyperion → common}/external_interaction/callbacks/sample_handling/sample_handling_callback.py +16 -4
  36. mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py +50 -45
  37. mx_bluesky/common/external_interaction/callbacks/xray_centre/nexus_callback.py +2 -1
  38. mx_bluesky/common/external_interaction/ispyb/data_model.py +1 -0
  39. mx_bluesky/common/external_interaction/ispyb/ispyb_store.py +18 -2
  40. mx_bluesky/common/external_interaction/ispyb/ispyb_utils.py +4 -4
  41. mx_bluesky/common/external_interaction/nexus/nexus_utils.py +1 -1
  42. mx_bluesky/common/parameters/components.py +22 -2
  43. mx_bluesky/common/parameters/constants.py +6 -16
  44. mx_bluesky/common/parameters/gridscan.py +36 -32
  45. mx_bluesky/common/plans/common_flyscan_xray_centre_plan.py +316 -0
  46. mx_bluesky/common/plans/inner_plans/__init__ .py +0 -0
  47. mx_bluesky/common/plans/read_hardware.py +3 -3
  48. mx_bluesky/common/plans/write_sample_status.py +46 -0
  49. mx_bluesky/common/preprocessors/__init__.py +0 -0
  50. mx_bluesky/common/preprocessors/preprocessors.py +105 -0
  51. mx_bluesky/common/protocols/__init__.py +0 -0
  52. mx_bluesky/common/protocols/protocols.py +10 -0
  53. mx_bluesky/common/utils/log.py +15 -12
  54. mx_bluesky/hyperion/__main__.py +5 -24
  55. mx_bluesky/hyperion/baton_handler.py +84 -0
  56. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +4 -4
  57. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +5 -1
  58. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +0 -33
  59. mx_bluesky/hyperion/device_setup_plans/utils.py +4 -4
  60. mx_bluesky/hyperion/experiment_plans/__init__.py +0 -10
  61. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +0 -16
  62. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +71 -88
  63. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +183 -0
  64. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +17 -8
  65. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +29 -8
  66. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +4 -4
  67. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +6 -4
  68. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +11 -3
  69. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +9 -34
  70. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +35 -68
  71. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +27 -8
  72. mx_bluesky/hyperion/external_interaction/agamemnon.py +140 -10
  73. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +17 -9
  74. mx_bluesky/hyperion/external_interaction/callbacks/snapshot_callback.py +259 -0
  75. mx_bluesky/hyperion/parameters/cli.py +2 -10
  76. mx_bluesky/hyperion/parameters/constants.py +0 -5
  77. mx_bluesky/hyperion/parameters/device_composites.py +40 -5
  78. mx_bluesky/hyperion/parameters/gridscan.py +9 -58
  79. mx_bluesky/hyperion/parameters/rotation.py +1 -5
  80. mx_bluesky/hyperion/utils/context.py +2 -5
  81. mx_bluesky/hyperion/utils/validation.py +13 -10
  82. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.8.dist-info}/METADATA +10 -9
  83. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.8.dist-info}/RECORD +92 -79
  84. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.8.dist-info}/WHEEL +1 -1
  85. mx_bluesky/common/external_interaction/callbacks/common/aperture_change_callback.py +0 -22
  86. mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +0 -103
  87. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +0 -466
  88. /mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/{short1-laser.png → s1l.png} +0 -0
  89. /mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/{short2-laser.png → s2l.png} +0 -0
  90. /mx_bluesky/{hyperion → common}/device_setup_plans/position_detector.py +0 -0
  91. /mx_bluesky/{hyperion → common}/external_interaction/callbacks/sample_handling/__init__.py +0 -0
  92. /mx_bluesky/common/plans/{do_fgs.py → inner_plans/do_fgs.py} +0 -0
  93. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.8.dist-info}/entry_points.txt +0 -0
  94. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.8.dist-info/licenses}/LICENSE +0 -0
  95. {mx_bluesky-1.4.6.dist-info → mx_bluesky-1.4.8.dist-info}/top_level.txt +0 -0
@@ -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
@@ -1,37 +1,31 @@
1
1
  import bluesky.plan_stubs as bps
2
- import bluesky.preprocessors as bpp
3
2
  import numpy
4
3
  from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue
5
4
  from dodal.devices.smargon import Smargon, StubPosition
6
5
 
6
+ from mx_bluesky.common.device_setup_plans.manipulate_sample import move_x_y_z
7
7
  from mx_bluesky.common.utils.log import LOGGER
8
8
  from mx_bluesky.common.utils.tracing import TRACER
9
9
  from mx_bluesky.common.xrc_result import XRayCentreResult
10
- from mx_bluesky.hyperion.device_setup_plans.manipulate_sample import move_x_y_z
11
- from mx_bluesky.hyperion.parameters.gridscan import HyperionSpecifiedThreeDGridScan
12
10
 
13
11
 
14
12
  def change_aperture_then_move_to_xtal(
15
13
  best_hit: XRayCentreResult,
16
14
  smargon: Smargon,
17
15
  aperture_scatterguard: ApertureScatterguard,
18
- parameters: HyperionSpecifiedThreeDGridScan | None = None,
16
+ set_stub_offsets: bool | None = None,
19
17
  ):
20
18
  """For the given x-ray centring result,
21
19
  * Change the aperture so that the beam size is comparable to the crystal size
22
20
  * Centre on the centre-of-mass
23
21
  * Reset the stub offsets if specified by params"""
24
- if best_hit.bounding_box_mm is not None:
25
- bounding_box_size = numpy.abs(
26
- best_hit.bounding_box_mm[1] - best_hit.bounding_box_mm[0]
27
- )
28
- with TRACER.start_span("change_aperture"):
29
- yield from set_aperture_for_bbox_mm(
30
- aperture_scatterguard,
31
- bounding_box_size,
32
- )
33
- else:
34
- LOGGER.warning("No bounding box size received")
22
+ bounding_box_size = numpy.abs(
23
+ best_hit.bounding_box_mm[1] - best_hit.bounding_box_mm[0]
24
+ )
25
+ yield from set_aperture_for_bbox_mm(
26
+ aperture_scatterguard,
27
+ bounding_box_size,
28
+ )
35
29
 
36
30
  # once we have the results, go to the appropriate position
37
31
  LOGGER.info("Moving to centre of mass.")
@@ -41,7 +35,7 @@ def change_aperture_then_move_to_xtal(
41
35
 
42
36
  # TODO support for setting stub offsets in multipin
43
37
  # https://github.com/DiamondLightSource/mx-bluesky/issues/552
44
- if parameters and parameters.FGS_params.set_stub_offsets:
38
+ if set_stub_offsets:
45
39
  LOGGER.info("Recentring smargon co-ordinate system to this point.")
46
40
  yield from bps.mv(smargon.stub_offsets, StubPosition.CURRENT_AS_CENTER)
47
41
 
@@ -52,12 +46,12 @@ def set_aperture_for_bbox_mm(
52
46
  ):
53
47
  """Sets aperture size based on bbox_size.
54
48
 
55
- This function determines the aperture size needed to accomodate the bounding box
49
+ This function determines the aperture size needed to accommodate the bounding box
56
50
  of a crystal. The x-axis length of the bounding box is used, setting the aperture
57
51
  to Medium if this is less than 50um, and Large otherwise.
58
52
 
59
53
  Args:
60
- aperture_device: The aperture scatter gaurd device we are controlling.
54
+ aperture_device: The aperture scatter guard device we are controlling.
61
55
  bbox_size_mm: The [x,y,z] lengths, in mm, of a bounding box
62
56
  containing a crystal. This describes (in no particular order):
63
57
  * The maximum width a crystal occupies
@@ -77,14 +71,4 @@ def set_aperture_for_bbox_mm(
77
71
  f"Setting aperture to {new_selected_aperture} based on bounding box size {bbox_size_mm}."
78
72
  )
79
73
 
80
- @bpp.set_run_key_decorator("change_aperture")
81
- @bpp.run_decorator(
82
- md={
83
- "subplan_name": "change_aperture",
84
- "aperture_size": new_selected_aperture.value,
85
- }
86
- )
87
- def set_aperture():
88
- yield from bps.abs_set(aperture_device, new_selected_aperture)
89
-
90
- yield from set_aperture()
74
+ yield from bps.abs_set(aperture_device.selected_aperture, new_selected_aperture)
@@ -14,13 +14,13 @@ from dodal.devices.oav.pin_image_recognition.utils import NONE_VALUE
14
14
  from dodal.devices.oav.utils import PinNotFoundException, wait_for_tip_to_be_found
15
15
  from dodal.devices.smargon import Smargon
16
16
 
17
+ from mx_bluesky.common.device_setup_plans.setup_oav import (
18
+ pre_centring_setup_oav,
19
+ )
20
+ from mx_bluesky.common.parameters.constants import DocDescriptorNames, HardwareConstants
17
21
  from mx_bluesky.common.utils.context import device_composite_from_context
18
22
  from mx_bluesky.common.utils.exceptions import catch_exception_and_warn
19
23
  from mx_bluesky.common.utils.log import LOGGER
20
- from mx_bluesky.hyperion.device_setup_plans.setup_oav import (
21
- pre_centring_setup_oav,
22
- )
23
- from mx_bluesky.hyperion.parameters.constants import CONST
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  from dodal.devices.oav.oav_parameters import OAVParameters
@@ -103,7 +103,7 @@ def grid_detection_plan(
103
103
  yield from bps.mv(smargon.omega, angle)
104
104
  # need to wait for the OAV image to update
105
105
  # See #673 for improvements
106
- yield from bps.sleep(CONST.HARDWARE.OAV_REFRESH_DELAY)
106
+ yield from bps.sleep(HardwareConstants.OAV_REFRESH_DELAY)
107
107
 
108
108
  tip_x_px, tip_y_px = yield from catch_exception_and_warn(
109
109
  PinNotFoundException, wait_for_tip_to_be_found, pin_tip_detection
@@ -163,7 +163,7 @@ def grid_detection_plan(
163
163
  yield from bps.abs_set(oav.grid_snapshot.filename, snapshot_filename)
164
164
  yield from bps.abs_set(oav.grid_snapshot.directory, snapshot_dir)
165
165
  yield from bps.trigger(oav.grid_snapshot, wait=True)
166
- yield from bps.create(CONST.DESCRIPTORS.OAV_GRID_SNAPSHOT_TRIGGERED)
166
+ yield from bps.create(DocDescriptorNames.OAV_GRID_SNAPSHOT_TRIGGERED)
167
167
 
168
168
  yield from bps.read(oav)
169
169
  yield from bps.read(smargon)
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from abc import abstractmethod
4
4
  from collections.abc import Callable, Sequence
5
+ from pathlib import Path
5
6
  from typing import TYPE_CHECKING, Any, TypeVar, cast
6
7
 
7
8
  from dodal.beamline_specific_utils.i03 import beam_size_from_aperture
@@ -26,7 +27,7 @@ from mx_bluesky.common.external_interaction.ispyb.ispyb_store import (
26
27
  )
27
28
  from mx_bluesky.common.external_interaction.ispyb.ispyb_utils import get_ispyb_config
28
29
  from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample
29
- from mx_bluesky.common.parameters.constants import DocDescriptorNames, SimConstants
30
+ from mx_bluesky.common.parameters.constants import DocDescriptorNames
30
31
  from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, set_dcgid_tag
31
32
  from mx_bluesky.common.utils.utils import convert_eV_to_angstrom
32
33
 
@@ -70,10 +71,10 @@ class BaseISPyBCallback(PlanReactiveCallback):
70
71
  self.ispyb: StoreInIspyb
71
72
  self.descriptors: dict[str, EventDescriptor] = {}
72
73
  self.ispyb_config = get_ispyb_config()
73
- if (
74
- self.ispyb_config == SimConstants.ISPYB_CONFIG
75
- or self.ispyb_config == SimConstants.DEV_ISPYB_DATABASE_CFG
76
- ):
74
+ ISPYB_ZOCALO_CALLBACK_LOGGER.info(
75
+ f"Using ISPyB configuration from {self.ispyb_config}"
76
+ )
77
+ if not self.ispyb_config or not Path(self.ispyb_config).is_absolute():
77
78
  ISPYB_ZOCALO_CALLBACK_LOGGER.warning(
78
79
  f"{self.__class__} using dev ISPyB config: {self.ispyb_config}. If you"
79
80
  "want to use the real database, please set the ISPYB_CONFIG_PATH "
@@ -121,10 +122,8 @@ class BaseISPyBCallback(PlanReactiveCallback):
121
122
  ISPYB_ZOCALO_CALLBACK_LOGGER.info(
122
123
  "ISPyB handler received event from read hardware"
123
124
  )
124
- assert isinstance(
125
- synchrotron_mode := doc["data"]["synchrotron-synchrotron_mode"],
126
- SynchrotronMode,
127
- )
125
+ synchrotron_mode = doc["data"]["synchrotron-synchrotron_mode"]
126
+ assert isinstance(synchrotron_mode, SynchrotronMode)
128
127
 
129
128
  hwscan_data_collection_info = DataCollectionInfo(
130
129
  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
@@ -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,12 +15,15 @@ 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
22
  self._run_id: str | None = None
23
23
 
24
+ # Record 'sample loaded' if document successfully stops
25
+ self.record_loaded_on_success = record_loaded_on_success
26
+
24
27
  def activity_gated_start(self, doc: RunStart):
25
28
  if not self._sample_id and self.active:
26
29
  sample_id = doc.get("metadata", {}).get("sample_id")
@@ -30,6 +33,7 @@ class SampleHandlingCallback(PlanReactiveCallback):
30
33
 
31
34
  def activity_gated_stop(self, doc: RunStop) -> RunStop:
32
35
  if self._run_id == doc.get("run_start"):
36
+ expeye = ExpeyeInteraction()
33
37
  if doc["exit_status"] != "success":
34
38
  exception_type, message = SampleException.type_and_message_from_reason(
35
39
  doc.get("reason", "")
@@ -37,13 +41,17 @@ class SampleHandlingCallback(PlanReactiveCallback):
37
41
  self.log.info(
38
42
  f"Sample handling callback intercepted exception of type {exception_type}: {message}"
39
43
  )
40
- self._record_exception(exception_type)
44
+ self._record_exception(exception_type, expeye)
45
+
46
+ elif self.record_loaded_on_success:
47
+ self._record_loaded(expeye)
48
+
41
49
  self._sample_id = None
42
50
  self._run_id = None
51
+
43
52
  return doc
44
53
 
45
- def _record_exception(self, exception_type: str):
46
- expeye = ExpeyeInteraction()
54
+ def _record_exception(self, exception_type: str, expeye: ExpeyeInteraction):
47
55
  assert self._sample_id, "Unable to record exception due to no sample ID"
48
56
  sample_status = self._decode_sample_status(exception_type)
49
57
  expeye.update_sample_status(self._sample_id, sample_status)
@@ -53,3 +61,7 @@ class SampleHandlingCallback(PlanReactiveCallback):
53
61
  case SampleException.__name__ | CrystalNotFoundException.__name__:
54
62
  return BLSampleStatus.ERROR_SAMPLE
55
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)
@@ -4,13 +4,8 @@ from collections.abc import Callable, Sequence
4
4
  from time import time
5
5
  from typing import TYPE_CHECKING, Any, TypeVar
6
6
 
7
- import numpy as np
8
7
  from bluesky import preprocessors as bpp
9
- from bluesky.utils import MsgGenerator
10
- from dodal.devices.zocalo.zocalo_results import (
11
- ZOCALO_READING_PLAN_NAME,
12
- get_processing_results_from_event,
13
- )
8
+ from bluesky.utils import MsgGenerator, make_decorator
14
9
 
15
10
  from mx_bluesky.common.external_interaction.callbacks.common.ispyb_callback_base import (
16
11
  BaseISPyBCallback,
@@ -19,9 +14,6 @@ from mx_bluesky.common.external_interaction.callbacks.common.ispyb_mapping impor
19
14
  populate_data_collection_group,
20
15
  populate_remaining_data_collection_info,
21
16
  )
22
- from mx_bluesky.common.external_interaction.callbacks.common.logging_callback import (
23
- format_doc_for_log,
24
- )
25
17
  from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping import (
26
18
  construct_comment_for_gridscan,
27
19
  populate_xy_data_collection_info,
@@ -29,6 +21,7 @@ from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_mapping
29
21
  )
30
22
  from mx_bluesky.common.external_interaction.ispyb.data_model import (
31
23
  DataCollectionGridInfo,
24
+ DataCollectionGroupInfo,
32
25
  DataCollectionInfo,
33
26
  DataCollectionPositionInfo,
34
27
  Orientation,
@@ -52,6 +45,9 @@ from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER, set_dcgid_
52
45
  if TYPE_CHECKING:
53
46
  from event_model import Event, RunStart, RunStop
54
47
 
48
+ T = TypeVar("T", bound="GridCommon")
49
+ 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"
50
+
55
51
 
56
52
  def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters):
57
53
  return bpp.set_run_key_wrapper(
@@ -67,7 +63,7 @@ def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters):
67
63
  )
68
64
 
69
65
 
70
- T = TypeVar("T", bound="GridCommon")
66
+ ispyb_activation_decorator = make_decorator(ispyb_activation_wrapper)
71
67
 
72
68
 
73
69
  class GridscanISPyBCallback(BaseISPyBCallback):
@@ -97,10 +93,12 @@ class GridscanISPyBCallback(BaseISPyBCallback):
97
93
  self.param_type = param_type
98
94
  self._start_of_fgs_uid: str | None = None
99
95
  self._processing_start_time: float | None = None
96
+ self.data_collection_group_info: DataCollectionGroupInfo | None
100
97
 
101
98
  def activity_gated_start(self, doc: RunStart):
102
99
  if doc.get("subplan_name") == PlanNameConstants.DO_FGS:
103
100
  self._start_of_fgs_uid = doc.get("uid")
101
+
104
102
  if doc.get("subplan_name") == PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN:
105
103
  self.uid_to_finalize_on = doc.get("uid")
106
104
  ISPYB_ZOCALO_CALLBACK_LOGGER.info(
@@ -111,7 +109,9 @@ class GridscanISPyBCallback(BaseISPyBCallback):
111
109
  assert isinstance(mx_bluesky_parameters, str)
112
110
  self.params = self.param_type.model_validate_json(mx_bluesky_parameters)
113
111
  self.ispyb = StoreInIspyb(self.ispyb_config)
114
- data_collection_group_info = populate_data_collection_group(self.params)
112
+ self.data_collection_group_info = populate_data_collection_group(
113
+ self.params
114
+ )
115
115
 
116
116
  scan_data_infos = [
117
117
  ScanDataInfo(
@@ -135,56 +135,37 @@ class GridscanISPyBCallback(BaseISPyBCallback):
135
135
  ]
136
136
 
137
137
  self.ispyb_ids = self.ispyb.begin_deposition(
138
- data_collection_group_info, scan_data_infos
138
+ self.data_collection_group_info, scan_data_infos
139
139
  )
140
140
  set_dcgid_tag(self.ispyb_ids.data_collection_group_id)
141
141
  return super().activity_gated_start(doc)
142
142
 
143
143
  def activity_gated_event(self, doc: Event):
144
+ assert self.data_collection_group_info, ASSERT_START_BEFORE_EVENT_DOC_MESSAGE
145
+
144
146
  doc = super().activity_gated_event(doc)
145
147
 
146
148
  descriptor_name = self.descriptors[doc["descriptor"]].get("name")
147
- if descriptor_name == ZOCALO_READING_PLAN_NAME:
148
- self._handle_zocalo_read_event(doc)
149
- elif descriptor_name == DocDescriptorNames.OAV_GRID_SNAPSHOT_TRIGGERED:
149
+ if descriptor_name == DocDescriptorNames.OAV_GRID_SNAPSHOT_TRIGGERED:
150
150
  scan_data_infos = self._handle_oav_grid_snapshot_triggered(doc)
151
151
  self.ispyb_ids = self.ispyb.update_deposition(
152
152
  self.ispyb_ids, scan_data_infos
153
153
  )
154
+ self.ispyb.update_data_collection_group_table(
155
+ self.data_collection_group_info, self.ispyb_ids.data_collection_group_id
156
+ )
154
157
 
155
158
  return doc
156
159
 
157
- def _handle_zocalo_read_event(self, doc):
158
- crystal_summary = ""
159
- if self._processing_start_time is not None:
160
- proc_time = time() - self._processing_start_time
161
- crystal_summary = f"Zocalo processing took {proc_time:.2f} s. "
162
- bboxes: list[np.ndarray] = []
163
- ISPYB_ZOCALO_CALLBACK_LOGGER.info(
164
- f"Amending comment based on Zocalo reading doc: {format_doc_for_log(doc)}"
165
- )
160
+ def _add_processing_time_to_comment(self, processing_start_time: float):
161
+ assert self.data_collection_group_info, ASSERT_START_BEFORE_EVENT_DOC_MESSAGE
162
+ proc_time = time() - processing_start_time
163
+ crystal_summary = f"Zocalo processing took {proc_time:.2f} s."
164
+
165
+ self.data_collection_group_info.comments = (
166
+ self.data_collection_group_info.comments or ""
167
+ ) + crystal_summary
166
168
 
167
- raw_results = get_processing_results_from_event("zocalo", doc)
168
- if len(raw_results) > 0:
169
- for n, res in enumerate(raw_results):
170
- bb = res["bounding_box"]
171
- diff = np.array(bb[1]) - np.array(bb[0])
172
- bboxes.append(diff)
173
-
174
- nicely_formatted_com = [
175
- f"{np.round(com, 2)}" for com in res["centre_of_mass"]
176
- ]
177
- crystal_summary += (
178
- f"Crystal {n + 1}: "
179
- f"Strength {res['total_count']}; "
180
- f"Position (grid boxes) {nicely_formatted_com}; "
181
- f"Size (grid boxes) {bboxes[n]}; "
182
- )
183
- else:
184
- crystal_summary += "Zocalo found no crystals in this gridscan."
185
- assert self.ispyb_ids.data_collection_ids, (
186
- "No data collection to add results to"
187
- )
188
169
  self.ispyb.append_to_comment(
189
170
  self.ispyb_ids.data_collection_ids[0], crystal_summary
190
171
  )
@@ -192,6 +173,7 @@ class GridscanISPyBCallback(BaseISPyBCallback):
192
173
  def _handle_oav_grid_snapshot_triggered(self, doc) -> Sequence[ScanDataInfo]:
193
174
  assert self.ispyb_ids.data_collection_ids, "No current data collection"
194
175
  assert self.params, "ISPyB handler didn't receive parameters!"
176
+ assert self.data_collection_group_info, "No data collection group"
195
177
  data = doc["data"]
196
178
  data_collection_id = None
197
179
  data_collection_info = DataCollectionInfo(
@@ -220,6 +202,18 @@ class GridscanISPyBCallback(BaseISPyBCallback):
220
202
  data_collection_info.comments = construct_comment_for_gridscan(
221
203
  data_collection_grid_info
222
204
  )
205
+
206
+ if self.data_collection_group_info.comments:
207
+ self.data_collection_group_info.comments += (
208
+ f"by {data_collection_grid_info.steps_y}."
209
+ )
210
+ else:
211
+ self.data_collection_group_info.comments = (
212
+ f"Diffraction grid scan of "
213
+ f"{data_collection_grid_info.steps_x} "
214
+ f"by {data_collection_grid_info.steps_y} "
215
+ )
216
+
223
217
  if len(self.ispyb_ids.data_collection_ids) > self._oav_snapshot_event_idx:
224
218
  data_collection_id = self.ispyb_ids.data_collection_ids[
225
219
  self._oav_snapshot_event_idx
@@ -275,6 +269,9 @@ class GridscanISPyBCallback(BaseISPyBCallback):
275
269
  return scan_data_infos
276
270
 
277
271
  def activity_gated_stop(self, doc: RunStop) -> RunStop:
272
+ assert self.data_collection_group_info, (
273
+ f"No data collection group info - stop document has been emitted before a {PlanNameConstants.GRID_DETECT_AND_DO_GRIDSCAN} start document"
274
+ )
278
275
  if doc.get("run_start") == self._start_of_fgs_uid:
279
276
  self._processing_start_time = time()
280
277
  if doc.get("run_start") == self.uid_to_finalize_on:
@@ -289,5 +286,13 @@ class GridscanISPyBCallback(BaseISPyBCallback):
289
286
  )
290
287
  if exception_type:
291
288
  doc["reason"] = message
289
+ self.data_collection_group_info.comments = message
290
+ elif self._processing_start_time:
291
+ self._add_processing_time_to_comment(self._processing_start_time)
292
+ self.ispyb.update_data_collection_group_table(
293
+ self.data_collection_group_info,
294
+ self.ispyb_ids.data_collection_group_id,
295
+ )
296
+ self.data_collection_group_info = None
292
297
  return super().activity_gated_stop(doc)
293
298
  return self._tag_doc(doc)
@@ -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)
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  import ispyb
8
8
  import ispyb.sqlalchemy
9
+ import numpy as np
9
10
  from ispyb.connector.mysqlsp.main import ISPyBMySQLSPConnector as Connector
10
11
  from ispyb.sp.mxacquisition import MXAcquisition
11
12
  from ispyb.strictordereddict import StrictOrderedDict
@@ -77,7 +78,7 @@ class StoreInIspyb:
77
78
  scan_data_infos,
78
79
  ) -> IspybIds:
79
80
  with ispyb.open(self.ISPYB_CONFIG_PATH) as conn:
80
- assert conn is not None, "Failed to connect to ISPyB"
81
+ assert conn, "Failed to connect to ISPyB"
81
82
  if data_collection_group_info:
82
83
  ispyb_ids.data_collection_group_id = (
83
84
  self._store_data_collection_group_table(
@@ -152,6 +153,19 @@ class StoreInIspyb:
152
153
  data_collection_id, comment, delimiter
153
154
  )
154
155
 
156
+ def update_data_collection_group_table(
157
+ self,
158
+ dcg_info: DataCollectionGroupInfo,
159
+ data_collection_group_id: int | None = None,
160
+ ) -> None:
161
+ with ispyb.open(self.ISPYB_CONFIG_PATH) as conn:
162
+ assert conn is not None, "Failed to connect to ISPyB!"
163
+ self._store_data_collection_group_table(
164
+ conn,
165
+ dcg_info,
166
+ data_collection_group_id,
167
+ )
168
+
155
169
  def _update_scan_with_end_time_and_status(
156
170
  self,
157
171
  end_time: str,
@@ -267,7 +281,9 @@ class StoreInIspyb:
267
281
  conn, data_collection_info.visit_string
268
282
  )
269
283
  params |= {
270
- k: v for k, v in asdict(data_collection_info).items() if k != "visit_string"
284
+ k: v.item() if isinstance(v, np.generic) else v # Convert to native types
285
+ for k, v in asdict(data_collection_info).items()
286
+ if k != "visit_string"
271
287
  }
272
288
 
273
289
  return params
@@ -7,11 +7,11 @@ from ispyb import NoResult
7
7
  from ispyb.connector.mysqlsp.main import ISPyBMySQLSPConnector as Connector
8
8
  from ispyb.sp.core import Core
9
9
 
10
- from mx_bluesky.common.parameters.constants import SimConstants
11
10
 
12
-
13
- def get_ispyb_config():
14
- return os.environ.get("ISPYB_CONFIG_PATH", SimConstants.ISPYB_CONFIG)
11
+ def get_ispyb_config() -> str:
12
+ ispyb_config = os.environ.get("ISPYB_CONFIG_PATH")
13
+ assert ispyb_config, "ISPYB_CONFIG_PATH must be set"
14
+ return ispyb_config
15
15
 
16
16
 
17
17
  def get_session_id_from_visit(conn: Connector, visit: str):
@@ -144,7 +144,7 @@ def create_detector_parameters(detector_params: DetectorParams) -> Detector:
144
144
  list(
145
145
  detector_params.get_beam_position_pixels(detector_params.detector_distance)
146
146
  ),
147
- detector_params.exposure_time,
147
+ detector_params.exposure_time_s,
148
148
  [(-1.0, 0.0, 0.0), (0.0, -1.0, 0.0)],
149
149
  )
150
150