mx-bluesky 1.5.1__py3-none-any.whl → 1.5.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. mx_bluesky/_version.py +16 -3
  2. mx_bluesky/beamlines/i04/__init__.py +8 -1
  3. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +56 -1
  4. mx_bluesky/beamlines/i04/experiment_plans/__init__.py +0 -0
  5. mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +262 -0
  6. mx_bluesky/beamlines/i24/serial/blueapi_config.yaml +2 -2
  7. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +3 -1
  8. mx_bluesky/common/experiment_plans/change_aperture_then_move_plan.py +5 -1
  9. mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +26 -3
  10. mx_bluesky/common/experiment_plans/common_grid_detect_then_xray_centre_plan.py +1 -0
  11. mx_bluesky/common/experiment_plans/inner_plans/do_fgs.py +3 -1
  12. mx_bluesky/common/experiment_plans/oav_grid_detection_plan.py +12 -2
  13. mx_bluesky/common/external_interaction/alerting/__init__.py +13 -0
  14. mx_bluesky/common/external_interaction/alerting/_service.py +82 -0
  15. mx_bluesky/common/external_interaction/alerting/log_based_service.py +57 -0
  16. mx_bluesky/common/external_interaction/callbacks/sample_handling/sample_handling_callback.py +28 -4
  17. mx_bluesky/common/external_interaction/config_server.py +151 -54
  18. mx_bluesky/common/external_interaction/ispyb/ispyb_store.py +11 -6
  19. mx_bluesky/common/parameters/__init__.py +0 -0
  20. mx_bluesky/common/parameters/constants.py +27 -8
  21. mx_bluesky/common/parameters/device_composites.py +1 -1
  22. mx_bluesky/common/parameters/gridscan.py +2 -1
  23. mx_bluesky/hyperion/__main__.py +51 -179
  24. mx_bluesky/hyperion/baton_handler.py +142 -54
  25. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +29 -24
  26. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +4 -93
  27. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +23 -38
  28. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +12 -4
  29. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +1 -1
  30. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +7 -8
  31. mx_bluesky/hyperion/external_interaction/agamemnon.py +128 -73
  32. mx_bluesky/hyperion/external_interaction/alerting/__init__.py +0 -0
  33. mx_bluesky/hyperion/external_interaction/alerting/constants.py +12 -0
  34. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +5 -0
  35. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +2 -2
  36. mx_bluesky/hyperion/external_interaction/config_server.py +12 -31
  37. mx_bluesky/hyperion/parameters/cli.py +15 -3
  38. mx_bluesky/hyperion/parameters/components.py +7 -5
  39. mx_bluesky/hyperion/parameters/constants.py +21 -6
  40. mx_bluesky/hyperion/parameters/gridscan.py +22 -14
  41. mx_bluesky/hyperion/parameters/load_centre_collect.py +1 -14
  42. mx_bluesky/hyperion/parameters/robot_load.py +1 -4
  43. mx_bluesky/hyperion/parameters/rotation.py +1 -2
  44. mx_bluesky/hyperion/plan_runner.py +78 -0
  45. mx_bluesky/hyperion/runner.py +189 -0
  46. mx_bluesky/hyperion/utils/context.py +19 -5
  47. mx_bluesky/phase1_zebra/__init__.py +1 -0
  48. mx_bluesky/phase1_zebra/device_setup_plans/__init__.py +0 -0
  49. mx_bluesky/phase1_zebra/device_setup_plans/setup_zebra.py +112 -0
  50. {mx_bluesky-1.5.1.dist-info → mx_bluesky-1.5.3.dist-info}/METADATA +5 -4
  51. {mx_bluesky-1.5.1.dist-info → mx_bluesky-1.5.3.dist-info}/RECORD +57 -44
  52. {mx_bluesky-1.5.1.dist-info → mx_bluesky-1.5.3.dist-info}/entry_points.txt +0 -2
  53. /mx_bluesky/common/experiment_plans/{read_hardware.py → inner_plans/read_hardware.py} +0 -0
  54. /mx_bluesky/common/experiment_plans/{write_sample_status.py → inner_plans/write_sample_status.py} +0 -0
  55. {mx_bluesky-1.5.1.dist-info → mx_bluesky-1.5.3.dist-info}/WHEEL +0 -0
  56. {mx_bluesky-1.5.1.dist-info → mx_bluesky-1.5.3.dist-info}/licenses/LICENSE +0 -0
  57. {mx_bluesky-1.5.1.dist-info → mx_bluesky-1.5.3.dist-info}/top_level.txt +0 -0
mx_bluesky/_version.py CHANGED
@@ -1,7 +1,14 @@
1
1
  # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
5
12
 
6
13
  TYPE_CHECKING = False
7
14
  if TYPE_CHECKING:
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
9
16
  from typing import Union
10
17
 
11
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
12
20
  else:
13
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
14
23
 
15
24
  version: str
16
25
  __version__: str
17
26
  __version_tuple__: VERSION_TUPLE
18
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
19
30
 
20
- __version__ = version = '1.5.1'
21
- __version_tuple__ = version_tuple = (1, 5, 1)
31
+ __version__ = version = '1.5.3'
32
+ __version_tuple__ = version_tuple = (1, 5, 3)
33
+
34
+ __commit_id__ = commit_id = None
@@ -1,3 +1,10 @@
1
+ from mx_bluesky.beamlines.i04.experiment_plans.i04_grid_detect_then_xray_centre_plan import (
2
+ i04_grid_detect_then_xray_centre,
3
+ )
1
4
  from mx_bluesky.beamlines.i04.thawing_plan import thaw, thaw_and_stream_to_redis
2
5
 
3
- __all__ = ["thaw", "thaw_and_stream_to_redis"]
6
+ __all__ = [
7
+ "thaw",
8
+ "thaw_and_stream_to_redis",
9
+ "i04_grid_detect_then_xray_centre",
10
+ ]
@@ -1,6 +1,7 @@
1
1
  import copy
2
2
  import json
3
3
  from datetime import timedelta
4
+ from typing import TypedDict
4
5
 
5
6
  from bluesky.callbacks import CallbackBase
6
7
  from dodal.log import LOGGER
@@ -8,7 +9,43 @@ from event_model.documents import Event, RunStart, RunStop
8
9
  from redis import StrictRedis
9
10
 
10
11
 
12
+ class OmegaReading(TypedDict):
13
+ value: float
14
+ timestamp: float
15
+
16
+
17
+ def extrapolate_omega(
18
+ latest_omega: OmegaReading, previous_omega: OmegaReading, image_timestamp: float
19
+ ) -> float:
20
+ """Extrapolate an image omega from previous omegas.
21
+
22
+ There are a number of assumptions in this calculation:
23
+ * The speed of the smargon is fixed
24
+ * The timestamps from the two different devices are synchronised and match the data
25
+ exactly
26
+
27
+ These are accepted to be reasonable based on larger errors likely coming from murko
28
+ itself and that the results ultimately will be averaged out.
29
+ """
30
+ omega_per_sec = (latest_omega["value"] - previous_omega["value"]) / (
31
+ latest_omega["timestamp"] - previous_omega["timestamp"]
32
+ )
33
+ time_elapsed = image_timestamp - latest_omega["timestamp"]
34
+ return latest_omega["value"] + time_elapsed * omega_per_sec
35
+
36
+
11
37
  class MurkoCallback(CallbackBase):
38
+ """A callback that triggers murko processing of images.
39
+
40
+ It combines metadata readings from e.g the goniometer rotation with the uuid's given
41
+ to us by an `OAVToRedisForwarder` (which describe the location of images in redis).
42
+ And writes these as a package to redis. A separate service then forwards this to murko.
43
+
44
+ The metadata and image data arrive independently, it is expected that the image data
45
+ is arriving at a faster rate than gonio metadata and so the value of omega for when
46
+ the image arrives is extrapolated based on previous omega readings.
47
+ """
48
+
12
49
  DATA_EXPIRY_DAYS = 7
13
50
 
14
51
  def __init__(self, redis_host: str, redis_password: str, redis_db: int = 0):
@@ -16,6 +53,7 @@ class MurkoCallback(CallbackBase):
16
53
  host=redis_host, password=redis_password, db=redis_db
17
54
  )
18
55
  self.last_uuid = None
56
+ self.previous_omegas: list[OmegaReading] = []
19
57
 
20
58
  def start(self, doc: RunStart) -> RunStart | None:
21
59
  self.sample_id = doc.get("sample_id")
@@ -28,14 +66,31 @@ class MurkoCallback(CallbackBase):
28
66
  "sample_id": self.sample_id,
29
67
  }
30
68
  self.last_uuid = None
69
+ self.previous_omegas = []
31
70
  LOGGER.info(f"Starting to stream metadata to murko under {self.sample_id}")
32
71
  return doc
33
72
 
34
73
  def event(self, doc: Event) -> Event:
35
74
  if latest_omega := doc["data"].get("smargon-omega"):
36
- if self.last_uuid is not None:
75
+ if len(self.previous_omegas) <= 2 and self.last_uuid:
76
+ # For the first few images there's not enough data to extrapolate so we
77
+ # match them one to one
37
78
  self.call_murko(self.last_uuid, latest_omega)
79
+ self.previous_omegas.append(
80
+ OmegaReading(
81
+ value=latest_omega,
82
+ timestamp=doc["timestamps"]["smargon-omega"],
83
+ )
84
+ )
38
85
  elif (uuid := doc["data"].get("oav_to_redis_forwarder-uuid")) is not None:
86
+ if len(self.previous_omegas) >= 2:
87
+ omega = extrapolate_omega(
88
+ self.previous_omegas[-1],
89
+ self.previous_omegas[-2],
90
+ doc["timestamps"]["oav_to_redis_forwarder-uuid"],
91
+ )
92
+ LOGGER.info(f"Using extrapolated omega of {omega}")
93
+ self.call_murko(uuid, omega)
39
94
  self.last_uuid = uuid
40
95
  return doc
41
96
 
File without changes
@@ -0,0 +1,262 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import partial
4
+
5
+ import bluesky.plan_stubs as bps
6
+ import bluesky.preprocessors as bpp
7
+ from blueapi.core import BlueskyContext
8
+ from bluesky.utils import MsgGenerator
9
+ from dodal.common import inject
10
+ from dodal.devices.aperturescatterguard import ApertureScatterguard
11
+ from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
12
+ from dodal.devices.backlight import Backlight
13
+ from dodal.devices.common_dcm import BaseDCM
14
+ from dodal.devices.detector.detector_motion import DetectorMotion
15
+ from dodal.devices.eiger import EigerDetector
16
+ from dodal.devices.fast_grid_scan import (
17
+ ZebraFastGridScan,
18
+ set_fast_grid_scan_params,
19
+ )
20
+ from dodal.devices.flux import Flux
21
+ from dodal.devices.mx_phase1.beamstop import Beamstop
22
+ from dodal.devices.oav.oav_detector import OAV
23
+ from dodal.devices.oav.pin_image_recognition import PinTipDetection
24
+ from dodal.devices.robot import BartRobot
25
+ from dodal.devices.s4_slit_gaps import S4SlitGaps
26
+ from dodal.devices.smargon import Smargon
27
+ from dodal.devices.synchrotron import Synchrotron
28
+ from dodal.devices.undulator import Undulator
29
+ from dodal.devices.xbpm_feedback import XBPMFeedback
30
+ from dodal.devices.zebra.zebra import Zebra
31
+ from dodal.devices.zebra.zebra_controlled_shutter import ZebraShutter
32
+ from dodal.devices.zocalo import ZocaloResults
33
+ from dodal.plans.preprocessors.verify_undulator_gap import (
34
+ verify_undulator_gap_before_run_decorator,
35
+ )
36
+
37
+ from mx_bluesky.common.experiment_plans.common_flyscan_xray_centre_plan import (
38
+ BeamlineSpecificFGSFeatures,
39
+ construct_beamline_specific_FGS_features,
40
+ )
41
+ from mx_bluesky.common.experiment_plans.common_grid_detect_then_xray_centre_plan import (
42
+ grid_detect_then_xray_centre,
43
+ )
44
+ from mx_bluesky.common.experiment_plans.oav_snapshot_plan import (
45
+ setup_beamline_for_OAV,
46
+ )
47
+ from mx_bluesky.common.external_interaction.callbacks.common.zocalo_callback import (
48
+ ZocaloCallback,
49
+ )
50
+ from mx_bluesky.common.external_interaction.callbacks.xray_centre.ispyb_callback import (
51
+ GridscanISPyBCallback,
52
+ )
53
+ from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback import (
54
+ GridscanNexusFileCallback,
55
+ )
56
+ from mx_bluesky.common.parameters.constants import (
57
+ EnvironmentConstants,
58
+ OavConstants,
59
+ PlanGroupCheckpointConstants,
60
+ PlanNameConstants,
61
+ )
62
+ from mx_bluesky.common.parameters.device_composites import (
63
+ GridDetectThenXRayCentreComposite,
64
+ )
65
+ from mx_bluesky.common.parameters.gridscan import GridCommon, SpecifiedThreeDGridScan
66
+ from mx_bluesky.common.preprocessors.preprocessors import (
67
+ transmission_and_xbpm_feedback_for_collection_decorator,
68
+ )
69
+ from mx_bluesky.common.utils.context import device_composite_from_context
70
+ from mx_bluesky.common.utils.log import LOGGER
71
+ from mx_bluesky.phase1_zebra.device_setup_plans.setup_zebra import (
72
+ setup_zebra_for_gridscan,
73
+ tidy_up_zebra_after_gridscan,
74
+ )
75
+
76
+
77
+ def create_devices(
78
+ context: BlueskyContext,
79
+ ) -> GridDetectThenXRayCentreComposite:
80
+ return device_composite_from_context(context, GridDetectThenXRayCentreComposite)
81
+
82
+
83
+ # See https://github.com/DiamondLightSource/blueapi/issues/506 for using device composites
84
+ def i04_grid_detect_then_xray_centre(
85
+ parameters: GridCommon,
86
+ aperture_scatterguard: ApertureScatterguard = inject("aperture_scatterguard"),
87
+ attenuator: BinaryFilterAttenuator = inject("attenuator"),
88
+ backlight: Backlight = inject("backlight"),
89
+ beamstop: Beamstop = inject("beamstop"),
90
+ dcm: BaseDCM = inject("dcm"),
91
+ zebra_fast_grid_scan: ZebraFastGridScan = inject("zebra_fast_grid_scan"),
92
+ flux: Flux = inject("flux"),
93
+ oav: OAV = inject("oav"),
94
+ pin_tip_detection: PinTipDetection = inject("pin_tip_detection"),
95
+ s4_slit_gaps: S4SlitGaps = inject("s4_slit_gaps"),
96
+ undulator: Undulator = inject("undulator"),
97
+ xbpm_feedback: XBPMFeedback = inject("xbpm_feedback"),
98
+ zebra: Zebra = inject("zebra"),
99
+ robot: BartRobot = inject("robot"),
100
+ sample_shutter: ZebraShutter = inject("sample_shutter"),
101
+ eiger: EigerDetector = inject("eiger"),
102
+ synchrotron: Synchrotron = inject("synchrotron"),
103
+ zocalo: ZocaloResults = inject("zocalo"),
104
+ smargon: Smargon = inject("smargon"),
105
+ detector_motion: DetectorMotion = inject("detector_motion"),
106
+ oav_config: str = OavConstants.OAV_CONFIG_JSON,
107
+ udc: bool = False,
108
+ ) -> MsgGenerator:
109
+ """
110
+ A composite plan which:
111
+ - Uses the OAV to draw a virtual grid over the sample and to take snapshots of the sample
112
+ - Scans through the grid to identify the crystal centre
113
+ - Changes the aperture to match the beam size to the crystal size
114
+ - Moves the sample to the crystal centre of mass
115
+
116
+
117
+ i04's implementation of this plan is very similar to Hyperion. However, since i04
118
+ isn't running in a continious Bluesky UDC loop, we take additional steps in beamline
119
+ tidy-up.
120
+ """
121
+
122
+ composite = GridDetectThenXRayCentreComposite(
123
+ eiger,
124
+ synchrotron,
125
+ zocalo,
126
+ smargon,
127
+ aperture_scatterguard,
128
+ attenuator,
129
+ backlight,
130
+ beamstop,
131
+ dcm,
132
+ detector_motion,
133
+ zebra_fast_grid_scan,
134
+ flux,
135
+ oav,
136
+ pin_tip_detection,
137
+ s4_slit_gaps,
138
+ undulator,
139
+ xbpm_feedback,
140
+ zebra,
141
+ robot,
142
+ sample_shutter,
143
+ )
144
+
145
+ def tidy_beamline_if_not_udc():
146
+ if not udc:
147
+ yield from get_ready_for_oav_and_close_shutter(
148
+ composite.smargon,
149
+ composite.backlight,
150
+ composite.aperture_scatterguard,
151
+ composite.detector_motion,
152
+ )
153
+
154
+ @bpp.finalize_decorator(tidy_beamline_if_not_udc)
155
+ def _inner_grid_detect_then_xrc():
156
+ # These callbacks let us talk to ISPyB and Nexgen. They aren't included in the common plan because
157
+ # Hyperion handles its callbacks differently to BlueAPI-managed plans, see
158
+ # https://github.com/DiamondLightSource/mx-bluesky/issues/1117
159
+ callbacks = create_gridscan_callbacks()
160
+
161
+ @bpp.subs_decorator(callbacks)
162
+ @verify_undulator_gap_before_run_decorator(composite)
163
+ @transmission_and_xbpm_feedback_for_collection_decorator(
164
+ composite, parameters.transmission_frac, PlanNameConstants.GRIDSCAN_OUTER
165
+ )
166
+ def grid_detect_then_xray_centre_with_callbacks():
167
+ yield from grid_detect_then_xray_centre(
168
+ composite=composite,
169
+ parameters=parameters,
170
+ xrc_params_type=SpecifiedThreeDGridScan,
171
+ construct_beamline_specific=construct_i04_specific_features,
172
+ oav_config=oav_config,
173
+ )
174
+
175
+ yield from grid_detect_then_xray_centre_with_callbacks()
176
+
177
+ yield from _inner_grid_detect_then_xrc()
178
+
179
+
180
+ def get_ready_for_oav_and_close_shutter(
181
+ smargon: Smargon,
182
+ backlight: Backlight,
183
+ aperture_scatterguard: ApertureScatterguard,
184
+ detector_motion: DetectorMotion,
185
+ ):
186
+ yield from bps.wait(PlanGroupCheckpointConstants.GRID_READY_FOR_DC)
187
+ group = "get_ready_for_oav_and_close_shutter"
188
+ LOGGER.info("Non-udc tidy: Seting up beamline for OAV")
189
+ yield from setup_beamline_for_OAV(
190
+ smargon, backlight, aperture_scatterguard, group=group
191
+ )
192
+ LOGGER.info("Non-udc tidy: Closing detector shutter")
193
+ yield from bps.abs_set(
194
+ detector_motion.shutter,
195
+ 0,
196
+ group=group,
197
+ )
198
+ yield from bps.wait(group)
199
+
200
+
201
+ def create_gridscan_callbacks() -> tuple[
202
+ GridscanNexusFileCallback, GridscanISPyBCallback
203
+ ]:
204
+ return (
205
+ GridscanNexusFileCallback(param_type=SpecifiedThreeDGridScan),
206
+ GridscanISPyBCallback(
207
+ param_type=GridCommon,
208
+ emit=ZocaloCallback(
209
+ PlanNameConstants.DO_FGS, EnvironmentConstants.ZOCALO_ENV
210
+ ),
211
+ ),
212
+ )
213
+
214
+
215
+ def construct_i04_specific_features(
216
+ xrc_composite: GridDetectThenXRayCentreComposite,
217
+ xrc_parameters: SpecifiedThreeDGridScan,
218
+ ) -> BeamlineSpecificFGSFeatures:
219
+ """
220
+ Get all the information needed to do the i04 XRC flyscan.
221
+ """
222
+ signals_to_read_pre_flyscan = [
223
+ xrc_composite.undulator.current_gap,
224
+ xrc_composite.synchrotron.synchrotron_mode,
225
+ xrc_composite.s4_slit_gaps.xgap,
226
+ xrc_composite.s4_slit_gaps.ygap,
227
+ xrc_composite.smargon.x,
228
+ xrc_composite.smargon.y,
229
+ xrc_composite.smargon.z,
230
+ xrc_composite.dcm.energy_in_kev,
231
+ ]
232
+
233
+ signals_to_read_during_collection = [
234
+ xrc_composite.aperture_scatterguard,
235
+ xrc_composite.attenuator.actual_transmission,
236
+ xrc_composite.flux.flux_reading,
237
+ xrc_composite.dcm.energy_in_kev,
238
+ xrc_composite.eiger.bit_depth,
239
+ ]
240
+
241
+ tidy_plan = partial(
242
+ tidy_up_zebra_after_gridscan,
243
+ xrc_composite.zebra,
244
+ xrc_composite.sample_shutter,
245
+ group="flyscan_zebra_tidy",
246
+ wait=True,
247
+ )
248
+ set_flyscan_params_plan = partial(
249
+ set_fast_grid_scan_params,
250
+ xrc_composite.zebra_fast_grid_scan,
251
+ xrc_parameters.FGS_params,
252
+ )
253
+ fgs_motors = xrc_composite.zebra_fast_grid_scan
254
+ return construct_beamline_specific_FGS_features(
255
+ setup_zebra_for_gridscan,
256
+ tidy_plan,
257
+ set_flyscan_params_plan,
258
+ fgs_motors,
259
+ signals_to_read_pre_flyscan,
260
+ signals_to_read_during_collection,
261
+ get_xrc_results_from_zocalo=True,
262
+ )
@@ -7,11 +7,11 @@ env:
7
7
  events:
8
8
  broadcast_status_events: false
9
9
  api:
10
- port: 25565
10
+ url: http://localhost:25565
11
11
  cors:
12
12
  allow_credentials: True
13
13
  origins:
14
14
  - "*"
15
15
  stomp:
16
16
  enabled: true
17
- host: i24-control.diamond.ac.uk
17
+ url: http://i24-control.diamond.ac.uk:61613
@@ -490,7 +490,9 @@ def run_aborted_plan(pmac: PMAC, dcid: DCID, exception: Exception):
490
490
  either by pressing the Abort button or because of a timeout, and to reset the \
491
491
  P variable.
492
492
  """
493
- SSX_LOGGER.warning(f"Data Collection Aborted: {format_exception(exception)}")
493
+ SSX_LOGGER.warning(
494
+ f"Data Collection Aborted: {''.join(format_exception(exception))}"
495
+ )
494
496
  yield from bps.trigger(pmac.abort_program, wait=True)
495
497
 
496
498
  end_time = datetime.now()
@@ -4,6 +4,7 @@ from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureVal
4
4
  from dodal.devices.smargon import Smargon, StubPosition
5
5
 
6
6
  from mx_bluesky.common.device_setup_plans.manipulate_sample import move_x_y_z
7
+ from mx_bluesky.common.parameters.constants import PlanGroupCheckpointConstants
7
8
  from mx_bluesky.common.utils.log import LOGGER
8
9
  from mx_bluesky.common.utils.tracing import TRACER
9
10
  from mx_bluesky.common.xrc_result import XRayCentreResult
@@ -43,6 +44,7 @@ def change_aperture_then_move_to_xtal(
43
44
  def set_aperture_for_bbox_mm(
44
45
  aperture_device: ApertureScatterguard,
45
46
  bbox_size_mm: list[float] | numpy.ndarray,
47
+ group=PlanGroupCheckpointConstants.GRID_READY_FOR_DC,
46
48
  ):
47
49
  """Sets aperture size based on bbox_size.
48
50
 
@@ -71,4 +73,6 @@ def set_aperture_for_bbox_mm(
71
73
  f"Setting aperture to {new_selected_aperture} based on bounding box size {bbox_size_mm}."
72
74
  )
73
75
 
74
- yield from bps.abs_set(aperture_device.selected_aperture, new_selected_aperture)
76
+ yield from bps.abs_set(
77
+ aperture_device.selected_aperture, new_selected_aperture, group=group
78
+ )
@@ -22,7 +22,7 @@ from mx_bluesky.common.experiment_plans.inner_plans.do_fgs import (
22
22
  ZOCALO_STAGE_GROUP,
23
23
  kickoff_and_complete_gridscan,
24
24
  )
25
- from mx_bluesky.common.experiment_plans.read_hardware import (
25
+ from mx_bluesky.common.experiment_plans.inner_plans.read_hardware import (
26
26
  read_hardware_plan,
27
27
  )
28
28
  from mx_bluesky.common.parameters.constants import (
@@ -55,6 +55,25 @@ class BeamlineSpecificFGSFeatures:
55
55
  get_xrc_results_from_zocalo: bool
56
56
 
57
57
 
58
+ def generic_tidy(xrc_composite: FlyScanEssentialDevices, wait=True) -> MsgGenerator:
59
+ """Tidy Zocalo and turn off Eiger dev/shm. Ran after the beamline-specific tidy plan"""
60
+
61
+ LOGGER.info("Tidying up Zocalo")
62
+ group = "generic_tidy"
63
+ # make sure we don't consume any other results
64
+ yield from bps.unstage(xrc_composite.zocalo, group=group)
65
+
66
+ # Turn off dev/shm streaming to avoid filling disk, see https://github.com/DiamondLightSource/hyperion/issues/1395
67
+ LOGGER.info("Turning off Eiger dev/shm streaming")
68
+ # Fix types in ophyd-async (https://github.com/DiamondLightSource/mx-bluesky/issues/855)
69
+ yield from bps.abs_set(
70
+ xrc_composite.eiger.odin.fan.dev_shm_enable, # type: ignore
71
+ 0,
72
+ group=group,
73
+ )
74
+ yield from bps.wait(group)
75
+
76
+
58
77
  def construct_beamline_specific_FGS_features(
59
78
  setup_trigger_plan: Callable[..., MsgGenerator],
60
79
  tidy_plan: Callable[..., MsgGenerator],
@@ -71,7 +90,7 @@ def construct_beamline_specific_FGS_features(
71
90
  Ran directly before kicking off the gridscan.
72
91
 
73
92
  tidy_plan (Callable): Tidy up states of devices. Ran at the end of the flyscan, regardless of
74
- whether or not it finished successfully.
93
+ whether or not it finished successfully. Zocalo and Eiger are cleaned up separately
75
94
 
76
95
  set_flyscan_params_plan (Callable): Set PV's for the relevant Fast Grid Scan dodal device
77
96
 
@@ -135,6 +154,10 @@ def common_flyscan_xray_centre(
135
154
  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
136
155
  """
137
156
 
157
+ def _overall_tidy():
158
+ yield from beamline_specific.tidy_plan()
159
+ yield from generic_tidy(composite)
160
+
138
161
  def _decorated_flyscan():
139
162
  @bpp.set_run_key_decorator(PlanNameConstants.GRIDSCAN_OUTER)
140
163
  @bpp.run_decorator( # attach experiment metadata to the start document
@@ -146,7 +169,7 @@ def common_flyscan_xray_centre(
146
169
  ],
147
170
  }
148
171
  )
149
- @bpp.finalize_decorator(lambda: beamline_specific.tidy_plan(composite))
172
+ @bpp.finalize_decorator(lambda: _overall_tidy())
150
173
  def run_gridscan_and_tidy(
151
174
  fgs_composite: FlyScanEssentialDevices,
152
175
  params: SpecifiedThreeDGridScan,
@@ -111,6 +111,7 @@ def grid_detect_then_xray_centre(
111
111
  )
112
112
 
113
113
 
114
+ # This function should be private but is currently called by Hyperion, see https://github.com/DiamondLightSource/mx-bluesky/issues/1148
114
115
  def detect_grid_and_do_gridscan(
115
116
  composite: GridDetectThenXRayCentreComposite,
116
117
  parameters: GridCommon,
@@ -14,7 +14,9 @@ from dodal.log import LOGGER
14
14
  from dodal.plan_stubs.check_topup import check_topup_and_wait_if_necessary
15
15
  from scanspec.core import AxesPoints, Axis
16
16
 
17
- from mx_bluesky.common.experiment_plans.read_hardware import read_hardware_for_zocalo
17
+ from mx_bluesky.common.experiment_plans.inner_plans.read_hardware import (
18
+ read_hardware_for_zocalo,
19
+ )
18
20
  from mx_bluesky.common.parameters.constants import (
19
21
  PlanNameConstants,
20
22
  )
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
6
6
  import bluesky.plan_stubs as bps
7
7
  import numpy as np
8
8
  from blueapi.core import BlueskyContext
9
+ from bluesky.utils import MsgGenerator
9
10
  from dodal.devices.oav.oav_detector import OAV
10
11
  from dodal.devices.oav.pin_image_recognition import PinTipDetection
11
12
  from dodal.devices.oav.pin_image_recognition.utils import NONE_VALUE
@@ -49,6 +50,16 @@ def get_min_and_max_y_of_pin(
49
50
  return min_y, max_y
50
51
 
51
52
 
53
+ def optimum_grid_detect_angles(smargon: Smargon) -> MsgGenerator[list[float]]:
54
+ """We need to match the 0 and -90 that the fast grid scan performs but the order in
55
+ which we do the grid detection does not matter so we do the closest angle first."""
56
+ current_omega = yield from bps.rd(smargon.omega)
57
+ if current_omega < -45:
58
+ return [-90, 0]
59
+ else:
60
+ return [0, -90]
61
+
62
+
52
63
  def grid_detection_plan(
53
64
  composite: OavGridDetectionComposite,
54
65
  parameters: OAVParameters,
@@ -90,8 +101,7 @@ def grid_detection_plan(
90
101
 
91
102
  grid_width_pixels = int(grid_width_microns / microns_per_pixel_x)
92
103
 
93
- # The FGS uses -90 so we need to match it
94
- for angle in [0, -90]:
104
+ for angle in (yield from optimum_grid_detect_angles(smargon)):
95
105
  yield from bps.mv(smargon.omega, angle)
96
106
  # need to wait for the OAV image to update
97
107
  # See #673 for improvements
@@ -0,0 +1,13 @@
1
+ from mx_bluesky.common.external_interaction.alerting._service import (
2
+ AlertService,
3
+ Metadata,
4
+ get_alerting_service,
5
+ set_alerting_service,
6
+ )
7
+
8
+ __all__ = [
9
+ "AlertService",
10
+ "Metadata",
11
+ "get_alerting_service",
12
+ "set_alerting_service",
13
+ ]
@@ -0,0 +1,82 @@
1
+ from datetime import UTC, datetime, timedelta
2
+ from enum import StrEnum
3
+ from typing import Protocol
4
+ from urllib.parse import quote, urlencode
5
+
6
+
7
+ class Metadata(StrEnum):
8
+ """Metadata fields that can be specified by the caller when raising an alert."""
9
+
10
+ CONTAINER = "container"
11
+ SAMPLE_ID = "sample_id"
12
+ VISIT = "visit"
13
+
14
+
15
+ class ExtraMetadata(StrEnum):
16
+ """Additional metadata fields that are automatically appended by
17
+ the AlertService implementations."""
18
+
19
+ ALERT_CONTENT = "alert_content"
20
+ ALERT_SUMMARY = "alert_summary"
21
+ BEAMLINE = "beamline"
22
+ GRAYLOG_URL = "graylog_url"
23
+ ISPYB_URL = "ispyb_url"
24
+ PROPOSAL = "proposal"
25
+
26
+
27
+ class AlertService(Protocol):
28
+ """
29
+ Implemented by any backend that provides the ability to dispatch alerts to some
30
+ service that is capable of disseminating them via any of a variety of media such
31
+ as email, SMS, instant messaging, etc etc.
32
+ """
33
+
34
+ def raise_alert(self, summary: str, content: str, metadata: dict[Metadata, str]):
35
+ """
36
+ Raise an alert that will be forwarded to beamline support staff, which might
37
+ for example be used as the basis for an incident in an incident reporting system.
38
+ Args:
39
+ summary: One line summary of the alert, that might for instance be used
40
+ in an email subject line.
41
+ content: Plain text content detailing the nature of the incident.
42
+ metadata: A dict of strings that can be included as metadata in the alert for
43
+ those backends that support it. The summary and content will be included
44
+ by default.
45
+ """
46
+ pass
47
+
48
+
49
+ _alert_service: AlertService
50
+
51
+
52
+ def get_alerting_service() -> AlertService:
53
+ """Get the alert service for this instance."""
54
+ return _alert_service
55
+
56
+
57
+ def set_alerting_service(service: AlertService):
58
+ """Set the alert service for this instance, call when the beamline is initialised."""
59
+ global _alert_service
60
+ _alert_service = service
61
+
62
+
63
+ def ispyb_url(sample_id: str):
64
+ return f"https://ispyb.diamond.ac.uk/samples/sid/{quote(sample_id)}"
65
+
66
+
67
+ def graylog_url(stream_id: str):
68
+ now = datetime.now(UTC)
69
+ from_utc = now - timedelta(minutes=5)
70
+ from_timestamp = from_utc.isoformat()
71
+ # Add 1 second for graylog timing jitter
72
+ to_utc = now + timedelta(seconds=1)
73
+ to_timestamp = to_utc.isoformat()
74
+ query_string = urlencode(
75
+ {
76
+ "streams": stream_id,
77
+ "rangetype": "absolute",
78
+ "from": from_timestamp,
79
+ "to": to_timestamp,
80
+ }
81
+ )
82
+ return "https://graylog.diamond.ac.uk/search?" + query_string