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
@@ -5,7 +5,7 @@ from abc import abstractmethod
5
5
  from collections.abc import Sequence
6
6
  from enum import StrEnum
7
7
  from pathlib import Path
8
- from typing import Literal, SupportsInt, cast
8
+ from typing import Literal, Self, SupportsInt, cast
9
9
 
10
10
  from dodal.devices.aperturescatterguard import ApertureValue
11
11
  from dodal.devices.detector import (
@@ -114,12 +114,32 @@ class MxBlueskyParameters(BaseModel):
114
114
 
115
115
 
116
116
  class WithSnapshot(BaseModel):
117
+ """
118
+ Configures how snapshot images are created.
119
+ Attributes:
120
+ snapshot_directory: Path to the directory where snapshot images will be stored
121
+ snapshot_omegas_deg: list of omega values at which snapshots will be taken. For
122
+ gridscans, this attribute is ignored.
123
+ use_grid_snapshots: This may be specified for rotation snapshots to speed up rotation
124
+ execution. If set to True then rotation snapshots are generated from the
125
+ previously captured grid snapshots. Otherwise they are captured using
126
+ freshly captured snapshots during the rotation plan.
127
+ """
128
+
117
129
  snapshot_directory: Path
118
130
  snapshot_omegas_deg: list[float] | None = None
131
+ use_grid_snapshots: bool = False
119
132
 
120
133
  @property
121
134
  def take_snapshots(self) -> bool:
122
- return bool(self.snapshot_omegas_deg)
135
+ return bool(self.snapshot_omegas_deg) or self.use_grid_snapshots
136
+
137
+ @model_validator(mode="after")
138
+ def _validate_omegas_with_grid_snapshots(self) -> Self:
139
+ assert not self.use_grid_snapshots or self.snapshot_omegas_deg is None, (
140
+ "snapshot_omegas may not be specified with use_grid_snapshots"
141
+ )
142
+ return self
123
143
 
124
144
 
125
145
  class WithOptionalEnergyChange(BaseModel):
@@ -55,9 +55,12 @@ class PlanNameConstants:
55
55
  ROBOT_LOAD_AND_SNAPSHOTS = "robot_load_and_snapshots"
56
56
  # Rotation scan
57
57
  ROTATION_MULTI = "multi_rotation_wrapper"
58
+ ROTATION_MULTI_OUTER = "multi_rotation_outer"
58
59
  ROTATION_OUTER = "rotation_scan_with_cleanup"
59
60
  ROTATION_MAIN = "rotation_scan_main"
60
61
  FLYSCAN_RESULTS = "xray_centre_results"
62
+ SET_ENERGY = "set_energy"
63
+ UNNAMED_RUN = "unnamed_run"
61
64
 
62
65
 
63
66
  @dataclass(frozen=True)
@@ -65,17 +68,12 @@ class EnvironmentConstants:
65
68
  ZOCALO_ENV = ZOCALO_ENV_FROM_DODAL
66
69
 
67
70
 
68
- @dataclass(frozen=True)
69
- class TriggerConstants:
70
- ZOCALO = "trigger_zocalo_on"
71
-
72
-
73
71
  @dataclass(frozen=True)
74
72
  class HardwareConstants:
75
73
  OAV_REFRESH_DELAY = 0.3
76
74
  PANDA_FGS_RUN_UP_DEFAULT = 0.17
77
75
  CRYOJET_MARGIN_MM = 0.2
78
- THAWING_TIME = 20
76
+ THAWING_TIME = 40
79
77
  TIP_OFFSET_UM = 0
80
78
 
81
79
  # Value quoted in https://www.dectris.com/en/detectors/x-ray-detectors/eiger2/eiger2-for-synchrotrons/eiger2-x/,
@@ -92,6 +90,7 @@ class GridscanParamConstants:
92
90
  OMEGA_1 = 0.0
93
91
  OMEGA_2 = 90.0
94
92
  PANDA_RUN_UP_DISTANCE_MM = 0.2
93
+ ZOCALO_MIN_TOTAL_COUNT_THRESHOLD = 3
95
94
 
96
95
 
97
96
  @dataclass(frozen=True)
@@ -123,6 +122,7 @@ class PlanGroupCheckpointConstants:
123
122
  ROTATION_READY_FOR_DC = "rotation_ready_for_data_collection"
124
123
  MOVE_GONIO_TO_START = "move_gonio_to_start"
125
124
  READY_FOR_OAV = "ready_for_oav"
125
+ PREPARE_APERTURE = "prepare_aperture"
126
126
 
127
127
 
128
128
  # Eventually replace below with https://github.com/DiamondLightSource/mx-bluesky/issues/798
@@ -134,16 +134,6 @@ class DeviceSettingsConstants:
134
134
  )
135
135
 
136
136
 
137
- @dataclass(frozen=True)
138
- class SimConstants:
139
- BEAMLINE = "BL03S"
140
- INSERTION_PREFIX = "SR03S"
141
- # this one is for unit tests
142
- ISPYB_CONFIG = "tests/test_data/test_config.cfg"
143
- # this one is for system tests
144
- DEV_ISPYB_DATABASE_CFG = "/dls_sw/dasc/mariadb/credentials/ispyb-hyperion-dev.cfg"
145
-
146
-
147
137
  class Actions(Enum):
148
138
  START = "start"
149
139
  STOP = "stop"
@@ -26,7 +26,11 @@ from mx_bluesky.common.parameters.constants import (
26
26
  HardwareConstants,
27
27
  )
28
28
 
29
- DETECTOR_SIZE_PER_BEAMLINE = {"i02-1": EIGER2_X_9M_SIZE, "dev": EIGER2_X_16M_SIZE}
29
+ DETECTOR_SIZE_PER_BEAMLINE = {
30
+ "i02-1": EIGER2_X_9M_SIZE,
31
+ "dev": EIGER2_X_16M_SIZE,
32
+ "i03": EIGER2_X_16M_SIZE,
33
+ }
30
34
 
31
35
 
32
36
  class GridCommon(
@@ -50,6 +54,37 @@ class GridCommon(
50
54
 
51
55
  tip_offset_um: float = Field(default=HardwareConstants.TIP_OFFSET_UM)
52
56
 
57
+ @property
58
+ def detector_params(self):
59
+ self.det_dist_to_beam_converter_path = (
60
+ self.det_dist_to_beam_converter_path
61
+ or DetectorParamConstants.BEAM_XY_LUT_PATH
62
+ )
63
+ optional_args = {}
64
+ if self.run_number:
65
+ optional_args["run_number"] = self.run_number
66
+ assert self.detector_distance_mm is not None, (
67
+ "Detector distance must be filled before generating DetectorParams"
68
+ )
69
+ return DetectorParams(
70
+ detector_size_constants=DETECTOR_SIZE_PER_BEAMLINE[
71
+ get_beamline_name("dev")
72
+ ],
73
+ expected_energy_ev=self.demand_energy_ev,
74
+ exposure_time_s=self.exposure_time_s,
75
+ directory=self.storage_directory,
76
+ prefix=self.file_name,
77
+ detector_distance=self.detector_distance_mm,
78
+ omega_start=self.omega_start_deg or 0,
79
+ omega_increment=0,
80
+ num_images_per_trigger=1,
81
+ num_triggers=self.num_images,
82
+ use_roi_mode=self.use_roi_mode,
83
+ det_dist_to_beam_converter_path=self.det_dist_to_beam_converter_path,
84
+ trigger_mode=self.trigger_mode,
85
+ **optional_args,
86
+ )
87
+
53
88
 
54
89
  class SpecifiedGrid(XyzStarts, WithScan):
55
90
  """A specified grid is one which has defined values for the start position,
@@ -150,34 +185,3 @@ class SpecifiedThreeDGridScan(
150
185
  @property
151
186
  def num_images(self) -> int:
152
187
  return len(self.scan_points["sam_x"])
153
-
154
- @property
155
- def detector_params(self):
156
- self.det_dist_to_beam_converter_path = (
157
- self.det_dist_to_beam_converter_path
158
- or DetectorParamConstants.BEAM_XY_LUT_PATH
159
- )
160
- optional_args = {}
161
- if self.run_number:
162
- optional_args["run_number"] = self.run_number
163
- assert self.detector_distance_mm is not None, (
164
- "Detector distance must be filled before generating DetectorParams"
165
- )
166
- return DetectorParams(
167
- detector_size_constants=DETECTOR_SIZE_PER_BEAMLINE[
168
- get_beamline_name("dev")
169
- ],
170
- expected_energy_ev=self.demand_energy_ev,
171
- exposure_time=self.exposure_time_s,
172
- directory=self.storage_directory,
173
- prefix=self.file_name,
174
- detector_distance=self.detector_distance_mm,
175
- omega_start=self.omega_start_deg or 0,
176
- omega_increment=0,
177
- num_images_per_trigger=1,
178
- num_triggers=self.num_images,
179
- use_roi_mode=self.use_roi_mode,
180
- det_dist_to_beam_converter_path=self.det_dist_to_beam_converter_path,
181
- trigger_mode=self.trigger_mode,
182
- **optional_args,
183
- )
@@ -0,0 +1,316 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ from collections.abc import Callable, Sequence
5
+ from functools import partial
6
+
7
+ import bluesky.plan_stubs as bps
8
+ import bluesky.preprocessors as bpp
9
+ import numpy as np
10
+ import pydantic
11
+ from bluesky.protocols import Readable
12
+ from bluesky.utils import MsgGenerator
13
+ from dodal.devices.eiger import EigerDetector
14
+ from dodal.devices.fast_grid_scan import (
15
+ FastGridScanCommon,
16
+ )
17
+ from dodal.devices.smargon import Smargon
18
+ from dodal.devices.synchrotron import Synchrotron
19
+ from dodal.devices.zocalo import ZocaloResults
20
+ from dodal.devices.zocalo.zocalo_results import (
21
+ XrcResult,
22
+ get_full_processing_results,
23
+ )
24
+
25
+ from mx_bluesky.common.parameters.constants import (
26
+ DocDescriptorNames,
27
+ GridscanParamConstants,
28
+ PlanGroupCheckpointConstants,
29
+ PlanNameConstants,
30
+ )
31
+ from mx_bluesky.common.parameters.gridscan import SpecifiedThreeDGridScan
32
+ from mx_bluesky.common.plans.inner_plans.do_fgs import (
33
+ ZOCALO_STAGE_GROUP,
34
+ kickoff_and_complete_gridscan,
35
+ )
36
+ from mx_bluesky.common.plans.read_hardware import (
37
+ read_hardware_plan,
38
+ )
39
+ from mx_bluesky.common.utils.exceptions import (
40
+ CrystalNotFoundException,
41
+ SampleException,
42
+ )
43
+ from mx_bluesky.common.utils.log import LOGGER
44
+ from mx_bluesky.common.utils.tracing import TRACER
45
+ from mx_bluesky.common.xrc_result import XRayCentreResult
46
+
47
+
48
+ @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
49
+ class FlyScanEssentialDevices:
50
+ eiger: EigerDetector
51
+ synchrotron: Synchrotron
52
+ zocalo: ZocaloResults
53
+ smargon: Smargon
54
+
55
+
56
+ @dataclasses.dataclass
57
+ class BeamlineSpecificFGSFeatures:
58
+ setup_trigger_plan: Callable[..., MsgGenerator]
59
+ tidy_plan: Callable[..., MsgGenerator]
60
+ set_flyscan_params_plan: Callable[..., MsgGenerator]
61
+ fgs_motors: FastGridScanCommon
62
+ read_pre_flyscan_plan: Callable[
63
+ ..., MsgGenerator
64
+ ] # Eventually replace with https://github.com/DiamondLightSource/mx-bluesky/issues/819
65
+ read_during_collection_plan: Callable[..., MsgGenerator]
66
+ get_xrc_results_from_zocalo: bool
67
+
68
+
69
+ def construct_beamline_specific_FGS_features(
70
+ setup_trigger_plan: Callable[..., MsgGenerator],
71
+ tidy_plan: Callable[..., MsgGenerator],
72
+ set_flyscan_params_plan: Callable[..., MsgGenerator],
73
+ fgs_motors: FastGridScanCommon,
74
+ signals_to_read_pre_flyscan: list[Readable],
75
+ signals_to_read_during_collection: list[Readable],
76
+ get_xrc_results_from_zocalo: bool = False,
77
+ ) -> BeamlineSpecificFGSFeatures:
78
+ """Construct the class needed to do beamline-specific parts of the XRC FGS
79
+
80
+ Args:
81
+ setup_trigger_plan (Callable): Configure triggering, for example with the Zebra or PandA device.
82
+ Ran directly before kicking off the gridscan.
83
+
84
+ tidy_plan (Callable): Tidy up states of devices. Ran at the end of the flyscan, regardless of
85
+ whether or not it finished successfully.
86
+
87
+ set_flyscan_params_plan (Callable): Set PV's for the relevant Fast Grid Scan dodal device
88
+
89
+ fgs_motors (Callable): Composite device representing the fast grid scan's motion program parameters.
90
+
91
+ signals_to_read_pre_flyscan (Callable): Signals which will be read and saved as a bluesky event document
92
+ after all configuration, but before the gridscan.
93
+
94
+ signals_to_read_during_collection (Callable): Signals which will be read and saved as a bluesky event
95
+ document whilst the gridscan motion is in progress
96
+
97
+ get_xrc_results_from_zocalo (bool): If true, fetch grid scan results from zocalo after completion, as well as
98
+ update the ispyb comment field with information about the results. See _fetch_xrc_results_from_zocalo
99
+ """
100
+ read_pre_flyscan_plan = partial(
101
+ read_hardware_plan,
102
+ signals_to_read_pre_flyscan,
103
+ DocDescriptorNames.HARDWARE_READ_PRE,
104
+ )
105
+
106
+ read_during_collection_plan = partial(
107
+ read_hardware_plan,
108
+ signals_to_read_during_collection,
109
+ DocDescriptorNames.HARDWARE_READ_DURING,
110
+ )
111
+
112
+ return BeamlineSpecificFGSFeatures(
113
+ setup_trigger_plan,
114
+ tidy_plan,
115
+ set_flyscan_params_plan,
116
+ fgs_motors,
117
+ read_pre_flyscan_plan,
118
+ read_during_collection_plan,
119
+ get_xrc_results_from_zocalo,
120
+ )
121
+
122
+
123
+ def common_flyscan_xray_centre(
124
+ composite: FlyScanEssentialDevices,
125
+ parameters: SpecifiedThreeDGridScan,
126
+ beamline_specific: BeamlineSpecificFGSFeatures,
127
+ ) -> MsgGenerator:
128
+ """Main entry point of the MX-Bluesky x-ray centering flyscan
129
+
130
+ Args:
131
+ composite (FlyScanEssentialDevices): Devices required to perform this plan.
132
+
133
+ parameters (SpecifiedThreeDGridScan): Parameters required to perform this plan.
134
+
135
+ beamline_specific (BeamlineSpecificFGSFeatures): Configure the beamline-specific version
136
+ of this plan: For example triggering setup and tidy up plans, as well as what to do with the
137
+ centering results.
138
+
139
+ With a minimum set of devices and parameters, prepares for; performs; and tidies up a flyscan
140
+ x-ray-center plan. This includes: Configuring desired triggering; writing nexus files; triggering zocalo;
141
+ reading hardware before and during the scan; and tidying up devices after
142
+ the plan is complete. Optionally fetch results from zocalo after completing the grid scan.
143
+
144
+ This plan will also push data to ispyb when used with the ispyb_activation_decorator.
145
+
146
+ There are a few other useful decorators to use with this plan, see: verify_undulator_gap_before_run_decorator, transmission_and_xbpm_feedback_for_collection_decorator
147
+ """
148
+
149
+ def _decorated_flyscan():
150
+ @bpp.set_run_key_decorator(PlanNameConstants.GRIDSCAN_OUTER)
151
+ @bpp.run_decorator( # attach experiment metadata to the start document
152
+ md={
153
+ "subplan_name": PlanNameConstants.GRIDSCAN_OUTER,
154
+ "mx_bluesky_parameters": parameters.model_dump_json(),
155
+ "activate_callbacks": [
156
+ "GridscanNexusFileCallback",
157
+ ],
158
+ }
159
+ )
160
+ @bpp.finalize_decorator(lambda: beamline_specific.tidy_plan(composite))
161
+ def run_gridscan_and_tidy(
162
+ fgs_composite: FlyScanEssentialDevices,
163
+ params: SpecifiedThreeDGridScan,
164
+ beamline_specific: BeamlineSpecificFGSFeatures,
165
+ ) -> MsgGenerator:
166
+ yield from beamline_specific.setup_trigger_plan(fgs_composite, parameters)
167
+
168
+ LOGGER.info("Starting grid scan")
169
+ yield from bps.stage(
170
+ fgs_composite.zocalo, group=ZOCALO_STAGE_GROUP
171
+ ) # connect to zocalo and make sure the queue is clear
172
+ yield from run_gridscan(fgs_composite, params, beamline_specific)
173
+
174
+ LOGGER.info("Grid scan finished")
175
+
176
+ if beamline_specific.get_xrc_results_from_zocalo:
177
+ yield from _fetch_xrc_results_from_zocalo(composite.zocalo, parameters)
178
+
179
+ yield from run_gridscan_and_tidy(composite, parameters, beamline_specific)
180
+
181
+ composite.eiger.set_detector_parameters(parameters.detector_params)
182
+ yield from _decorated_flyscan()
183
+
184
+
185
+ def _fetch_xrc_results_from_zocalo(
186
+ zocalo_results: ZocaloResults,
187
+ parameters: SpecifiedThreeDGridScan,
188
+ ) -> MsgGenerator:
189
+ """
190
+ Get XRC results from the ZocaloResults device which was staged during a grid scan,
191
+ and store them in XRayCentreEventHandler.xray_centre_results by firing an event.
192
+
193
+ The RunEngine must be subscribed to XRayCentreEventHandler for this plan to work.
194
+ """
195
+
196
+ LOGGER.info("Getting X-ray center Zocalo results...")
197
+
198
+ yield from bps.trigger(zocalo_results)
199
+ LOGGER.info("Zocalo triggered and read, interpreting results.")
200
+ xrc_results = yield from get_full_processing_results(zocalo_results)
201
+ LOGGER.info(f"Got xray centres, top 5: {xrc_results[:5]}")
202
+ filtered_results = [
203
+ result
204
+ for result in xrc_results
205
+ if result["total_count"]
206
+ >= GridscanParamConstants.ZOCALO_MIN_TOTAL_COUNT_THRESHOLD
207
+ ]
208
+ discarded_count = len(xrc_results) - len(filtered_results)
209
+ if discarded_count > 0:
210
+ LOGGER.info(f"Removed {discarded_count} results because below threshold")
211
+ if filtered_results:
212
+ flyscan_results = [
213
+ _xrc_result_in_boxes_to_result_in_mm(xr, parameters)
214
+ for xr in filtered_results
215
+ ]
216
+ else:
217
+ LOGGER.warning("No X-ray centre received")
218
+ raise CrystalNotFoundException()
219
+ yield from _fire_xray_centre_result_event(flyscan_results)
220
+
221
+
222
+ @bpp.set_run_key_decorator(PlanNameConstants.GRIDSCAN_MAIN)
223
+ @bpp.run_decorator(md={"subplan_name": PlanNameConstants.GRIDSCAN_MAIN})
224
+ def run_gridscan(
225
+ fgs_composite: FlyScanEssentialDevices,
226
+ parameters: SpecifiedThreeDGridScan,
227
+ beamline_specific: BeamlineSpecificFGSFeatures,
228
+ ):
229
+ # Currently gridscan only works for omega 0, see https://github.com/DiamondLightSource/mx-bluesky/issues/410
230
+ with TRACER.start_span("moving_omega_to_0"):
231
+ yield from bps.abs_set(fgs_composite.smargon.omega, 0)
232
+
233
+ with TRACER.start_span("ispyb_hardware_readings"):
234
+ yield from beamline_specific.read_pre_flyscan_plan()
235
+
236
+ LOGGER.info("Setting fgs params")
237
+ yield from beamline_specific.set_flyscan_params_plan()
238
+
239
+ LOGGER.info("Waiting for gridscan validity check")
240
+ yield from wait_for_gridscan_valid(beamline_specific.fgs_motors)
241
+
242
+ LOGGER.info("Waiting for arming to finish")
243
+ yield from bps.wait(PlanGroupCheckpointConstants.GRID_READY_FOR_DC)
244
+ yield from bps.stage(fgs_composite.eiger) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
245
+
246
+ yield from kickoff_and_complete_gridscan(
247
+ beamline_specific.fgs_motors,
248
+ fgs_composite.eiger,
249
+ fgs_composite.synchrotron,
250
+ [parameters.scan_points_first_grid, parameters.scan_points_second_grid],
251
+ parameters.scan_indices,
252
+ plan_during_collection=beamline_specific.read_during_collection_plan,
253
+ )
254
+ yield from bps.abs_set(beamline_specific.fgs_motors.z_steps, 0, wait=False)
255
+
256
+
257
+ def wait_for_gridscan_valid(fgs_motors: FastGridScanCommon, timeout=0.5):
258
+ LOGGER.info("Waiting for valid fgs_params")
259
+ SLEEP_PER_CHECK = 0.1
260
+ times_to_check = int(timeout / SLEEP_PER_CHECK)
261
+ for _ in range(times_to_check):
262
+ scan_invalid = yield from bps.rd(fgs_motors.scan_invalid)
263
+ pos_counter = yield from bps.rd(fgs_motors.position_counter)
264
+ LOGGER.debug(
265
+ f"Scan invalid: {scan_invalid} and position counter: {pos_counter}"
266
+ )
267
+ if not scan_invalid and pos_counter == 0:
268
+ LOGGER.info("Gridscan scan valid and position counter reset")
269
+ return
270
+ yield from bps.sleep(SLEEP_PER_CHECK)
271
+ raise SampleException("Scan invalid - pin too long/short/bent and out of range")
272
+
273
+
274
+ def _xrc_result_in_boxes_to_result_in_mm(
275
+ xrc_result: XrcResult, parameters: SpecifiedThreeDGridScan
276
+ ) -> XRayCentreResult:
277
+ fgs_params = parameters.FGS_params
278
+ xray_centre = fgs_params.grid_position_to_motor_position(
279
+ np.array(xrc_result["centre_of_mass"])
280
+ )
281
+ # A correction is applied to the bounding box to map discrete grid coordinates to
282
+ # the corners of the box in motor-space; we do not apply this correction
283
+ # to the xray-centre as it is already in continuous space and the conversion has
284
+ # been performed already
285
+ # In other words, xrc_result["bounding_box"] contains the position of the box centre,
286
+ # so we subtract half a box to get the corner of the box
287
+ return XRayCentreResult(
288
+ centre_of_mass_mm=xray_centre,
289
+ bounding_box_mm=(
290
+ fgs_params.grid_position_to_motor_position(
291
+ np.array(xrc_result["bounding_box"][0]) - 0.5
292
+ ),
293
+ fgs_params.grid_position_to_motor_position(
294
+ np.array(xrc_result["bounding_box"][1]) - 0.5
295
+ ),
296
+ ),
297
+ max_count=xrc_result["max_count"],
298
+ total_count=xrc_result["total_count"],
299
+ )
300
+
301
+
302
+ def _fire_xray_centre_result_event(results: Sequence[XRayCentreResult]):
303
+ def empty_plan():
304
+ return iter([])
305
+
306
+ yield from bpp.set_run_key_wrapper(
307
+ bpp.run_wrapper(
308
+ empty_plan(),
309
+ md={
310
+ PlanNameConstants.FLYSCAN_RESULTS: [
311
+ dataclasses.asdict(r) for r in results
312
+ ]
313
+ },
314
+ ),
315
+ PlanNameConstants.FLYSCAN_RESULTS,
316
+ )
File without changes
@@ -4,7 +4,7 @@ import bluesky.plan_stubs as bps
4
4
  from bluesky.protocols import Readable
5
5
  from dodal.devices.aperturescatterguard import ApertureScatterguard
6
6
  from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
7
- from dodal.devices.dcm import DCM
7
+ from dodal.devices.common_dcm import BaseDCM
8
8
  from dodal.devices.eiger import EigerDetector
9
9
  from dodal.devices.flux import Flux
10
10
  from dodal.devices.s4_slit_gaps import S4SlitGaps
@@ -43,7 +43,7 @@ def standard_read_hardware_pre_collection(
43
43
  undulator: Undulator,
44
44
  synchrotron: Synchrotron,
45
45
  s4_slit_gaps: S4SlitGaps,
46
- dcm: DCM,
46
+ dcm: BaseDCM,
47
47
  smargon: Smargon,
48
48
  ):
49
49
  LOGGER.info("Reading status of beamline for callbacks, pre collection.")
@@ -63,7 +63,7 @@ def standard_read_hardware_during_collection(
63
63
  aperture_scatterguard: ApertureScatterguard,
64
64
  attenuator: BinaryFilterAttenuator,
65
65
  flux: Flux,
66
- dcm: DCM,
66
+ dcm: BaseDCM,
67
67
  detector: EigerDetector,
68
68
  ):
69
69
  signals_to_read_during_collection = [
@@ -0,0 +1,46 @@
1
+ from enum import StrEnum
2
+
3
+ import bluesky.plan_stubs as bps
4
+ import bluesky.preprocessors as bpp
5
+
6
+ from mx_bluesky.common.external_interaction.callbacks.sample_handling.sample_handling_callback import (
7
+ SampleHandlingCallback,
8
+ )
9
+ from mx_bluesky.common.utils.exceptions import SampleException
10
+
11
+
12
+ class SampleStatusExceptionType(StrEnum):
13
+ BEAMLINE = "Beamline"
14
+ SAMPLE = "Sample"
15
+
16
+
17
+ @bpp.subs_decorator(SampleHandlingCallback())
18
+ def deposit_sample_error(exception_type: SampleStatusExceptionType, sample_id: int):
19
+ @bpp.run_decorator(
20
+ md={
21
+ "metadata": {"sample_id": sample_id},
22
+ "activate_callbacks": ["SampleHandlingCallback"],
23
+ }
24
+ )
25
+ def _inner():
26
+ yield from bps.null()
27
+ if exception_type == SampleStatusExceptionType.BEAMLINE:
28
+ raise AssertionError()
29
+ elif exception_type == SampleStatusExceptionType.SAMPLE:
30
+ raise SampleException
31
+
32
+ yield from _inner()
33
+
34
+
35
+ @bpp.subs_decorator(SampleHandlingCallback(record_loaded_on_success=True))
36
+ def deposit_loaded_sample(sample_id: int):
37
+ @bpp.run_decorator(
38
+ md={
39
+ "metadata": {"sample_id": sample_id},
40
+ "activate_callbacks": ["SampleHandlingCallback"],
41
+ }
42
+ )
43
+ def _inner():
44
+ yield from bps.null()
45
+
46
+ yield from _inner()
File without changes
@@ -0,0 +1,105 @@
1
+ from bluesky import preprocessors as bpp
2
+ from bluesky.preprocessors import plan_mutator
3
+ from bluesky.utils import Msg, MsgGenerator, make_decorator
4
+
5
+ from mx_bluesky.common.device_setup_plans.xbpm_feedback import (
6
+ check_and_pause_feedback,
7
+ unpause_xbpm_feedback_and_set_transmission_to_1,
8
+ )
9
+ from mx_bluesky.common.parameters.constants import PlanNameConstants
10
+ from mx_bluesky.common.protocols.protocols import (
11
+ XBPMPauseDevices,
12
+ )
13
+
14
+
15
+ def transmission_and_xbpm_feedback_for_collection_wrapper(
16
+ plan: MsgGenerator,
17
+ devices: XBPMPauseDevices,
18
+ desired_transmission_fraction: float,
19
+ run_key_to_wrap: PlanNameConstants | None = None,
20
+ ):
21
+ """
22
+ Sets the transmission for the data collection, ensuring the xbpm feedback is valid, then resets it immediately after
23
+ the run has finished.
24
+
25
+ This wrapper should be attached to the entry point of any beamline-specific plan that may disrupt the XBPM feedback,
26
+ such as a data collection or an x-ray center grid scan.
27
+ This wrapper will do nothing if no runs are seen.
28
+
29
+ XBPM feedback isn't reliable during collections due to:
30
+ * Objects (e.g. attenuator) crossing the beam can cause large (incorrect) feedback movements
31
+ * Lower transmissions/higher energies are less reliable for the xbpm
32
+
33
+ So we need to keep the transmission at 100% and the feedback on when not collecting
34
+ and then turn it off and set the correct transmission for collection. The feedback
35
+ mostly accounts for slow thermal drift so it is safe to assume that the beam is
36
+ stable during a collection.
37
+
38
+ Args:
39
+ plan: The plan performing the data collection.
40
+ devices (XBPMPauseDevices): Composite device including The XBPM device that is responsible for keeping
41
+ the beam in position, and attenuator
42
+ desired_transmission_fraction (float): The desired transmission for the collection
43
+ run_key_to_wrap: (str | None): Pausing XBPM and setting transmission is inserted after the 'open_run' message is seen with
44
+ the matching run key, and unpausing and resetting transmission is inserted after the corresponding 'close_run' message is
45
+ seen. If not specified, instead wrap the first run encountered.
46
+ """
47
+
48
+ _wrapped_run_name: None | str = None
49
+
50
+ def head(msg: Msg):
51
+ yield from check_and_pause_feedback(
52
+ devices.xbpm_feedback,
53
+ devices.attenuator,
54
+ desired_transmission_fraction,
55
+ )
56
+
57
+ # Allow 'open_run' message to pass through
58
+ yield msg
59
+
60
+ def tail():
61
+ yield from unpause_xbpm_feedback_and_set_transmission_to_1(
62
+ devices.xbpm_feedback, devices.attenuator
63
+ )
64
+
65
+ def insert_plans(msg: Msg):
66
+ # Wrap the specified run, or, if none specified, wrap the first run encountered
67
+ nonlocal _wrapped_run_name
68
+
69
+ match msg.command:
70
+ case "open_run":
71
+ # If we specified a run key, did we encounter it
72
+ # If we didn't specify, then insert the plans and track the name of the run
73
+ if (
74
+ not (run_key_to_wrap or _wrapped_run_name)
75
+ or run_key_to_wrap is msg.run
76
+ ):
77
+ _wrapped_run_name = msg.run if msg.run else "unnamed_run"
78
+ return head(msg), None
79
+ case "close_run":
80
+ # Check if the run tracked from above was closed
81
+ # An exception is raised in the RunEngine if two unnamed runs are opened
82
+ # at the same time, so we are safe from unpausing on the wrong run
83
+ if (_wrapped_run_name == "unnamed_run" and not msg.run) or (
84
+ msg.run and _wrapped_run_name and _wrapped_run_name is msg.run
85
+ ):
86
+ return None, tail()
87
+
88
+ return None, None
89
+
90
+ # Contingency wrapper can cause unpausing to occur on exception and again on close_run.
91
+ # Not needed after https://github.com/bluesky/bluesky/issues/1891
92
+ return (
93
+ yield from bpp.contingency_wrapper(
94
+ plan_mutator(plan, insert_plans),
95
+ except_plan=lambda _: unpause_xbpm_feedback_and_set_transmission_to_1(
96
+ devices.xbpm_feedback,
97
+ devices.attenuator,
98
+ ),
99
+ )
100
+ )
101
+
102
+
103
+ transmission_and_xbpm_feedback_for_collection_decorator = make_decorator(
104
+ transmission_and_xbpm_feedback_for_collection_wrapper
105
+ )
File without changes