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
@@ -1,35 +1,16 @@
1
1
  from functools import cache
2
2
 
3
- from daq_config_server.client import ConfigServer
3
+ from mx_bluesky.common.external_interaction.config_server import MXConfigClient
4
+ from mx_bluesky.hyperion.parameters.constants import (
5
+ HyperionFeatureSetting,
6
+ HyperionFeatureSettingources,
7
+ )
4
8
 
5
- from mx_bluesky.common.external_interaction.config_server import FeatureFlags
6
- from mx_bluesky.common.utils.log import LOGGER
7
- from mx_bluesky.hyperion.parameters.constants import CONST
8
9
 
9
-
10
- class HyperionFeatureFlags(FeatureFlags):
11
- """
12
- Feature flags specific to Hyperion.
13
-
14
- Attributes:
15
- use_panda_for_gridscan: If True then the PandA is used for gridscans, otherwise the zebra is used
16
- use_gpu_results: If True then GPU result processing is enabled
17
- and the GPU result is taken.
18
- set_stub_offsets: If True then set the stub offsets after moving to the crystal (ignored for
19
- multi-centre)
20
- omega_flip: If True then invert the smargon omega motor rotation commands with respect to
21
- the hyperion request. See "Hyperion Coordinate Systems" in the documentation.
22
- alternate_rotation_direction: If True then the for multi-sample pins the rotation direction of
23
- successive rotation scans is alternated between positive and negative.
24
- """
25
-
26
- @staticmethod
27
- @cache
28
- def get_config_server() -> ConfigServer:
29
- return ConfigServer(CONST.CONFIG_SERVER_URL, LOGGER)
30
-
31
- use_panda_for_gridscan: bool = CONST.I03.USE_PANDA_FOR_GRIDSCAN
32
- use_gpu_results: bool = CONST.I03.USE_GPU_RESULTS
33
- set_stub_offsets: bool = CONST.I03.SET_STUB_OFFSETS
34
- omega_flip: bool = CONST.I03.OMEGA_FLIP
35
- alternate_rotation_direction: bool = CONST.I03.ALTERNATE_ROTATION_DIRECTION
10
+ @cache
11
+ def get_hyperion_config_client() -> MXConfigClient[HyperionFeatureSetting]:
12
+ return MXConfigClient(
13
+ feature_sources=HyperionFeatureSettingources,
14
+ feature_dc=HyperionFeatureSetting,
15
+ url="https://daq-config.diamond.ac.uk",
16
+ )
@@ -1,12 +1,19 @@
1
1
  import argparse
2
+ from enum import StrEnum
2
3
 
3
4
  from pydantic.dataclasses import dataclass
4
5
 
5
6
  from mx_bluesky._version import version
6
7
 
7
8
 
9
+ class HyperionMode(StrEnum):
10
+ GDA = "gda"
11
+ UDC = "udc"
12
+
13
+
8
14
  @dataclass
9
15
  class HyperionArgs:
16
+ mode: HyperionMode
10
17
  dev_mode: bool = False
11
18
 
12
19
 
@@ -39,7 +46,12 @@ def parse_cli_args() -> HyperionArgs:
39
46
  action="version",
40
47
  version=version,
41
48
  )
42
- args = parser.parse_args()
43
- return HyperionArgs(
44
- dev_mode=args.dev or False,
49
+ parser.add_argument(
50
+ "--mode",
51
+ help="Launch in the specified mode (default is 'gda')",
52
+ default=HyperionMode.GDA,
53
+ type=HyperionMode,
54
+ choices=HyperionMode.__members__.values(),
45
55
  )
56
+ args = parser.parse_args()
57
+ return HyperionArgs(dev_mode=args.dev or False, mode=args.mode)
@@ -1,8 +1,10 @@
1
- from pydantic import Field
1
+ from mx_bluesky.common.parameters.components import MxBlueskyParameters
2
2
 
3
- from mx_bluesky.common.parameters.components import WithPandaGridScan
4
- from mx_bluesky.hyperion.external_interaction.config_server import HyperionFeatureFlags
5
3
 
4
+ class Wait(MxBlueskyParameters):
5
+ """Represents an instruction from Agamemnon for Hyperion to wait for a specified time
6
+ Attributes:
7
+ duration_s: duration to wait in seconds
8
+ """
6
9
 
7
- class WithHyperionUDCFeatures(WithPandaGridScan):
8
- features: HyperionFeatureFlags = Field(default=HyperionFeatureFlags())
10
+ duration_s: float
@@ -8,6 +8,8 @@ from mx_bluesky.common.parameters.constants import (
8
8
  DocDescriptorNames,
9
9
  EnvironmentConstants,
10
10
  ExperimentParamConstants,
11
+ FeatureSetting,
12
+ FeatureSettingources,
11
13
  HardwareConstants,
12
14
  OavConstants,
13
15
  PlanGroupCheckpointConstants,
@@ -24,18 +26,30 @@ class I03Constants:
24
26
  INSERTION_PREFIX = "SR03S" if TEST_MODE else "SR03I"
25
27
  OAV_CENTRING_FILE = OavConstants.OAV_CONFIG_JSON
26
28
  SHUTTER_TIME_S = 0.06
27
- USE_PANDA_FOR_GRIDSCAN = False
28
- SET_STUB_OFFSETS = False
29
+ USE_GPU_RESULTS = True
29
30
  OMEGA_FLIP = True
30
31
  ALTERNATE_ROTATION_DIRECTION = True
31
32
 
32
- # Turns on GPU processing for zocalo and uses the results that come back
33
- USE_GPU_RESULTS = True
33
+
34
+ # These currently exist in GDA domain.properties
35
+ class HyperionFeatureSettingources(FeatureSettingources):
36
+ USE_GPU_RESULTS = "gda.mx.hyperion.xrc.use_gpu_results"
37
+ USE_PANDA_FOR_GRIDSCAN = "gda.mx.hyperion.use_panda_for_gridscans"
38
+ SET_STUB_OFFSETS = "gda.mx.hyperion.do_stub_offsets"
39
+ PANDA_RUNUP_DISTANCE_MM = "gda.mx.hyperion.panda_runup_distance_mm"
40
+
41
+
42
+ # Use these defaults if we can't read from the config server
43
+ @dataclass
44
+ class HyperionFeatureSetting(FeatureSetting):
45
+ USE_GPU_RESULTS: bool = True
46
+ USE_PANDA_FOR_GRIDSCAN: bool = False
47
+ SET_STUB_OFFSETS: bool = False
48
+ PANDA_RUNUP_DISTANCE_MM: float = 0.16
34
49
 
35
50
 
36
51
  @dataclass(frozen=True)
37
52
  class HyperionConstants:
38
- DESCRIPTORS = DocDescriptorNames()
39
53
  ZOCALO_ENV = EnvironmentConstants.ZOCALO_ENV
40
54
  HARDWARE = HardwareConstants()
41
55
  I03 = I03Constants()
@@ -49,7 +63,8 @@ class HyperionConstants:
49
63
  if TEST_MODE
50
64
  else "https://daq-config.diamond.ac.uk/api"
51
65
  )
52
- GRAYLOG_PORT = 12232
66
+ GRAYLOG_PORT = 12232 # Hyperion stream
67
+ GRAYLOG_STREAM_ID = "66264f5519ccca6d1c9e4e03"
53
68
  PARAMETER_SCHEMA_DIRECTORY = "src/hyperion/parameters/schemas/"
54
69
  LOG_FILE_NAME = "hyperion.log"
55
70
  DEVICE_SETTINGS_CONSTANTS = DeviceSettingsConstants()
@@ -9,10 +9,12 @@ from mx_bluesky.common.parameters.gridscan import (
9
9
  GridCommon,
10
10
  SpecifiedThreeDGridScan,
11
11
  )
12
- from mx_bluesky.hyperion.parameters.components import WithHyperionUDCFeatures
12
+ from mx_bluesky.hyperion.external_interaction.config_server import (
13
+ get_hyperion_config_client,
14
+ )
13
15
 
14
16
 
15
- class GridCommonWithHyperionDetectorParams(GridCommon, WithHyperionUDCFeatures):
17
+ class GridCommonWithHyperionDetectorParams(GridCommon):
16
18
  """Used by models which require detector parameters but have no specifications of the grid"""
17
19
 
18
20
  # These detector params only exist so that we can properly select enable_dev_shm. Remove in
@@ -20,11 +22,13 @@ class GridCommonWithHyperionDetectorParams(GridCommon, WithHyperionUDCFeatures):
20
22
  @property
21
23
  def detector_params(self):
22
24
  params = super().detector_params
23
- params.enable_dev_shm = self.features.use_gpu_results
25
+ params.enable_dev_shm = (
26
+ get_hyperion_config_client().get_feature_flags().USE_GPU_RESULTS
27
+ )
24
28
  return params
25
29
 
26
30
 
27
- class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGridScan):
31
+ class HyperionSpecifiedThreeDGridScan(SpecifiedThreeDGridScan):
28
32
  """Hyperion's 3D grid scan deviates from the common class due to: optionally using a PandA, optionally using dev_shm for GPU analysis, and using a config server for features"""
29
33
 
30
34
  # These detector params only exist so that we can properly select enable_dev_shm. Remove in
@@ -33,7 +37,9 @@ class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGr
33
37
  @property
34
38
  def detector_params(self):
35
39
  params = super().detector_params
36
- params.enable_dev_shm = self.features.use_gpu_results
40
+ params.enable_dev_shm = (
41
+ get_hyperion_config_client().get_feature_flags().USE_GPU_RESULTS
42
+ )
37
43
  return params
38
44
 
39
45
  # Relative to common grid scan, stub offsets are defined by config server
@@ -51,7 +57,9 @@ class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGr
51
57
  z1_start_mm=self.z_start_um / 1000,
52
58
  y2_start_mm=self.y2_start_um / 1000,
53
59
  z2_start_mm=self.z2_start_um / 1000,
54
- set_stub_offsets=self.features.set_stub_offsets,
60
+ set_stub_offsets=get_hyperion_config_client()
61
+ .get_feature_flags()
62
+ .SET_STUB_OFFSETS,
55
63
  dwell_time_ms=self.exposure_time_s * 1000,
56
64
  transmission_fraction=self.transmission_frac,
57
65
  )
@@ -75,8 +83,12 @@ class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGr
75
83
  z1_start_mm=self.z_start_um / 1000,
76
84
  y2_start_mm=self.y2_start_um / 1000,
77
85
  z2_start_mm=self.z2_start_um / 1000,
78
- set_stub_offsets=self.features.set_stub_offsets,
79
- run_up_distance_mm=self.panda_runup_distance_mm,
86
+ set_stub_offsets=get_hyperion_config_client()
87
+ .get_feature_flags()
88
+ .SET_STUB_OFFSETS,
89
+ run_up_distance_mm=get_hyperion_config_client()
90
+ .get_feature_flags()
91
+ .PANDA_RUNUP_DISTANCE_MM,
80
92
  transmission_fraction=self.transmission_frac,
81
93
  )
82
94
 
@@ -84,13 +96,9 @@ class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGr
84
96
  class OddYStepsException(Exception): ...
85
97
 
86
98
 
87
- class PinTipCentreThenXrayCentre(
88
- GridCommonWithHyperionDetectorParams, WithHyperionUDCFeatures
89
- ):
99
+ class PinTipCentreThenXrayCentre(GridCommonWithHyperionDetectorParams):
90
100
  tip_offset_um: float = 0
91
101
 
92
102
 
93
- class GridScanWithEdgeDetect(
94
- GridCommonWithHyperionDetectorParams, WithHyperionUDCFeatures
95
- ):
103
+ class GridScanWithEdgeDetect(GridCommonWithHyperionDetectorParams):
96
104
  pass
@@ -8,7 +8,6 @@ from mx_bluesky.common.parameters.components import (
8
8
  WithSample,
9
9
  WithVisit,
10
10
  )
11
- from mx_bluesky.hyperion.parameters.components import WithHyperionUDCFeatures
12
11
  from mx_bluesky.hyperion.parameters.robot_load import (
13
12
  RobotLoadThenCentre,
14
13
  )
@@ -28,7 +27,6 @@ class LoadCentreCollect(
28
27
  WithVisit,
29
28
  WithSample,
30
29
  WithCentreSelection,
31
- WithHyperionUDCFeatures,
32
30
  ):
33
31
  """Experiment parameters to perform the combined robot load,
34
32
  pin-tip centre and rotation scan operations."""
@@ -39,6 +37,7 @@ class LoadCentreCollect(
39
37
  @model_validator(mode="before")
40
38
  @classmethod
41
39
  def validate_model(cls, values):
40
+ values = values.copy()
42
41
  allowed_keys = (
43
42
  LoadCentreCollect.model_fields.keys()
44
43
  | RobotLoadThenCentre.model_fields.keys()
@@ -50,12 +49,6 @@ class LoadCentreCollect(
50
49
  f"Unexpected fields found in LoadCentreCollect {disallowed_keys}"
51
50
  )
52
51
 
53
- assert "features" not in values["robot_load_then_centre"], (
54
- "Features flags must be specified at top-level in LoadCentreCollect"
55
- )
56
- assert "features" not in values["multi_rotation_scan"], (
57
- "Features flags must be specified at top-level in LoadCentreCollect"
58
- )
59
52
  keys_from_outer_load_centre_collect = (
60
53
  MxBlueskyParameters.model_fields.keys()
61
54
  | WithSample.model_fields.keys()
@@ -89,12 +82,6 @@ class LoadCentreCollect(
89
82
  values["robot_load_then_centre"] = new_robot_load_then_centre_params
90
83
  return values
91
84
 
92
- @model_validator(mode="after")
93
- def _ensure_features_are_internally_consistent(self) -> Self:
94
- self.robot_load_then_centre.features = self.features
95
- self.multi_rotation_scan.features = self.features
96
- return self
97
-
98
85
  @model_validator(mode="after")
99
86
  def _check_rotation_start_xyz_is_not_specified(self) -> Self:
100
87
  for scan in self.multi_rotation_scan.single_rotation_scans:
@@ -10,7 +10,6 @@ from mx_bluesky.common.parameters.components import (
10
10
  from mx_bluesky.common.parameters.constants import (
11
11
  HardwareConstants,
12
12
  )
13
- from mx_bluesky.hyperion.parameters.components import WithHyperionUDCFeatures
14
13
  from mx_bluesky.hyperion.parameters.gridscan import (
15
14
  GridCommonWithHyperionDetectorParams,
16
15
  PinTipCentreThenXrayCentre,
@@ -23,9 +22,7 @@ class RobotLoadAndEnergyChange(
23
22
  thawing_time: float = Field(default=HardwareConstants.THAWING_TIME)
24
23
 
25
24
 
26
- class RobotLoadThenCentre(
27
- GridCommonWithHyperionDetectorParams, WithHyperionUDCFeatures
28
- ):
25
+ class RobotLoadThenCentre(GridCommonWithHyperionDetectorParams):
29
26
  thawing_time: float = Field(default=HardwareConstants.THAWING_TIME)
30
27
 
31
28
  @property
@@ -28,7 +28,6 @@ from mx_bluesky.common.parameters.components import (
28
28
  WithSample,
29
29
  WithScan,
30
30
  )
31
- from mx_bluesky.hyperion.parameters.components import WithHyperionUDCFeatures
32
31
  from mx_bluesky.hyperion.parameters.constants import (
33
32
  CONST,
34
33
  I03Constants,
@@ -56,7 +55,7 @@ class RotationScanPerSweep(OptionalGonioAngleStarts, OptionalXyzStarts, WithSamp
56
55
  nexus_vds_start_img: int = Field(default=0, ge=0)
57
56
 
58
57
 
59
- class RotationExperiment(DiffractionExperiment, WithHyperionUDCFeatures):
58
+ class RotationExperiment(DiffractionExperiment):
60
59
  shutter_opening_time_s: float = Field(default=CONST.I03.SHUTTER_TIME_S)
61
60
  rotation_increment_deg: float = Field(default=0.1, gt=0)
62
61
  ispyb_experiment_type: IspybExperimentType = Field(
@@ -0,0 +1,78 @@
1
+ import threading
2
+ from collections.abc import Callable
3
+
4
+ from blueapi.core import BlueskyContext
5
+ from bluesky.utils import MsgGenerator, RequestAbort
6
+
7
+ from mx_bluesky.common.parameters.constants import Status
8
+ from mx_bluesky.common.utils.exceptions import WarningException
9
+ from mx_bluesky.common.utils.log import LOGGER
10
+ from mx_bluesky.hyperion.runner import BaseRunner
11
+
12
+
13
+ class PlanException(Exception):
14
+ """Identifies an exception that was encountered during plan execution."""
15
+
16
+ pass
17
+
18
+
19
+ class PlanRunner(BaseRunner):
20
+ """Runner that executes experiments from inside a running Bluesky plan"""
21
+
22
+ def __init__(
23
+ self,
24
+ context: BlueskyContext,
25
+ ) -> None:
26
+ super().__init__(context)
27
+ self.current_status: Status = Status.IDLE
28
+
29
+ def execute_plan(
30
+ self,
31
+ experiment: Callable[[], MsgGenerator],
32
+ ) -> MsgGenerator:
33
+ """Execute the specified experiment plan.
34
+ Args:
35
+ experiment: The experiment to run
36
+ Raises:
37
+ PlanException: If the plan raised an exception
38
+ RequestAbort: If the RunEngine aborted during execution"""
39
+
40
+ self.current_status = Status.BUSY
41
+
42
+ try:
43
+ yield from experiment()
44
+ self.current_status = Status.IDLE
45
+ except WarningException as e:
46
+ LOGGER.warning("Plan failed with warning", exc_info=e)
47
+ self.current_status = Status.FAILED
48
+ except RequestAbort:
49
+ # This will occur when the run engine processes an abort when we shut down
50
+ LOGGER.info("UDC Runner aborting")
51
+ raise
52
+ except Exception as e:
53
+ LOGGER.error("Plan failed with exception", exc_info=e)
54
+ self.current_status = Status.FAILED
55
+ raise PlanException("Exception thrown in plan execution") from e
56
+
57
+ def shutdown(self):
58
+ """Performs a prompt shutdown. Aborts the run engine and terminates the loop
59
+ waiting for messages."""
60
+
61
+ def issue_abort():
62
+ try:
63
+ # abort() causes the run engine to throw a RequestAbort exception
64
+ # inside the plan, which will propagate through the contingency wrappers.
65
+ # When the plan returns, the run engine will raise RunEngineInterrupted
66
+ self.RE.abort()
67
+ except Exception as e:
68
+ LOGGER.warning(
69
+ "Exception encountered when issuing abort() to RunEngine:",
70
+ exc_info=e,
71
+ )
72
+
73
+ LOGGER.info("Shutting down: Stopping the run engine gracefully")
74
+ if self.current_status != Status.ABORTING:
75
+ self.current_status = Status.ABORTING
76
+ stopping_thread = threading.Thread(target=issue_abort)
77
+ stopping_thread.start()
78
+ return
@@ -0,0 +1,189 @@
1
+ import threading
2
+ from abc import abstractmethod
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+ from queue import Empty, Queue
6
+ from typing import Any
7
+
8
+ from blueapi.core import BlueskyContext
9
+ from bluesky.callbacks.zmq import Publisher
10
+ from bluesky.utils import MsgGenerator
11
+
12
+ from mx_bluesky.common.external_interaction.callbacks.common.log_uid_tag_callback import (
13
+ LogUidTaggingCallback,
14
+ )
15
+ from mx_bluesky.common.parameters.components import MxBlueskyParameters
16
+ from mx_bluesky.common.parameters.constants import Actions, Status
17
+ from mx_bluesky.common.utils.exceptions import WarningException
18
+ from mx_bluesky.common.utils.log import LOGGER
19
+ from mx_bluesky.common.utils.tracing import TRACER
20
+ from mx_bluesky.hyperion.experiment_plans.experiment_registry import PLAN_REGISTRY
21
+ from mx_bluesky.hyperion.parameters.constants import CONST
22
+
23
+
24
+ @dataclass
25
+ class Command:
26
+ action: Actions
27
+ devices: Any | None = None
28
+ experiment: Callable[[Any, Any], MsgGenerator] | None = None
29
+
30
+ def __str__(self):
31
+ return f"Command({self.action}, {self.parameters}"
32
+
33
+ parameters: MxBlueskyParameters | None = None
34
+
35
+
36
+ @dataclass
37
+ class StatusAndMessage:
38
+ status: str
39
+ message: str = ""
40
+
41
+ def __init__(self, status: Status, message: str = "") -> None:
42
+ self.status = status.value
43
+ self.message = message
44
+
45
+
46
+ @dataclass
47
+ class ErrorStatusAndMessage(StatusAndMessage):
48
+ exception_type: str = ""
49
+
50
+
51
+ def make_error_status_and_message(exception: Exception):
52
+ return ErrorStatusAndMessage(
53
+ status=Status.FAILED.value,
54
+ message=repr(exception),
55
+ exception_type=type(exception).__name__,
56
+ )
57
+
58
+
59
+ class BaseRunner:
60
+ @abstractmethod
61
+ def shutdown(self):
62
+ """Performs orderly prompt shutdown.
63
+ Aborts the run engine and terminates the loop waiting for messages."""
64
+ pass
65
+
66
+ def __init__(self, context: BlueskyContext):
67
+ self.context: BlueskyContext = context
68
+ self.RE = context.run_engine
69
+ # These references are necessary to maintain liveness of callbacks because RE
70
+ # only keeps a weakref
71
+ self._logging_uid_tag_callback = LogUidTaggingCallback()
72
+ self._publisher = Publisher(f"localhost:{CONST.CALLBACK_0MQ_PROXY_PORTS[0]}")
73
+
74
+ self.RE.subscribe(self._logging_uid_tag_callback)
75
+ LOGGER.info("Connecting to external callback ZMQ proxy...")
76
+ self.RE.subscribe(self._publisher)
77
+
78
+
79
+ class GDARunner(BaseRunner):
80
+ """Runner that executes plans submitted by Flask requests from GDA."""
81
+
82
+ def __init__(
83
+ self,
84
+ context: BlueskyContext,
85
+ ) -> None:
86
+ super().__init__(context)
87
+ self.current_status: StatusAndMessage = StatusAndMessage(Status.IDLE)
88
+ self._last_run_aborted: bool = False
89
+ self._command_queue: Queue[Command] = Queue()
90
+
91
+ def start(
92
+ self,
93
+ experiment: Callable,
94
+ parameters: MxBlueskyParameters,
95
+ plan_name: str | None = None,
96
+ ) -> StatusAndMessage:
97
+ """Start a new bluesky plan
98
+ Args:
99
+ experiment: A bluesky plan
100
+ parameters: The parameters to be submitted
101
+ plan_name: Name of the plan that will be used to resolve the composite factory
102
+ to supply devices for the plan, if any are needed"""
103
+ LOGGER.info(f"Started with parameters: {parameters.model_dump_json(indent=2)}")
104
+
105
+ devices: Any = (
106
+ PLAN_REGISTRY[plan_name]["setup"](self.context) if plan_name else None
107
+ )
108
+
109
+ if (
110
+ self.current_status.status == Status.BUSY.value
111
+ or self.current_status.status == Status.ABORTING.value
112
+ ):
113
+ return StatusAndMessage(Status.FAILED, "Bluesky already running")
114
+ else:
115
+ self.current_status = StatusAndMessage(Status.BUSY)
116
+ self._command_queue.put(
117
+ Command(
118
+ action=Actions.START,
119
+ devices=devices,
120
+ experiment=experiment,
121
+ parameters=parameters,
122
+ )
123
+ )
124
+ return StatusAndMessage(Status.SUCCESS)
125
+
126
+ def stop(self) -> StatusAndMessage:
127
+ """Stop the currently executing plan."""
128
+ if self.current_status.status == Status.ABORTING.value:
129
+ return StatusAndMessage(Status.FAILED, "Bluesky already stopping")
130
+ else:
131
+ self.current_status = StatusAndMessage(Status.ABORTING)
132
+ stopping_thread = threading.Thread(target=self._stopping_thread)
133
+ stopping_thread.start()
134
+ self._last_run_aborted = True
135
+ return StatusAndMessage(Status.ABORTING)
136
+
137
+ def shutdown(self):
138
+ """Stops the run engine and the loop waiting for messages."""
139
+ print("Shutting down: Stopping the run engine gracefully")
140
+ self.stop()
141
+ self._command_queue.put(Command(action=Actions.SHUTDOWN))
142
+
143
+ def _stopping_thread(self):
144
+ try:
145
+ # abort() causes the run engine to throw a RequestAbort exception
146
+ # inside the plan, which will propagate through the contingency wrappers.
147
+ # When the plan returns, the run engine will raise RunEngineInterrupted
148
+ self.RE.abort()
149
+ self.current_status = StatusAndMessage(Status.IDLE)
150
+ except Exception as e:
151
+ self.current_status = make_error_status_and_message(e)
152
+
153
+ def fetch_next_command(self) -> Command:
154
+ """Fetch the next command from the queue, blocks if queue is empty."""
155
+ return self._command_queue.get()
156
+
157
+ def try_fetch_next_command(self) -> Command | None:
158
+ """Fetch the next command from the queue or return None if no command available."""
159
+ try:
160
+ return self._command_queue.get(block=False)
161
+ except Empty:
162
+ return None
163
+
164
+ def wait_on_queue(self):
165
+ while True:
166
+ command = self.fetch_next_command()
167
+ if command.action == Actions.SHUTDOWN:
168
+ return
169
+ elif command.action == Actions.START:
170
+ if command.experiment is None:
171
+ raise ValueError("No experiment provided for START")
172
+ try:
173
+ with TRACER.start_span("do_run"):
174
+ self.RE(command.experiment(command.devices, command.parameters))
175
+
176
+ self.current_status = StatusAndMessage(Status.IDLE)
177
+
178
+ self._last_run_aborted = False
179
+ except WarningException as exception:
180
+ LOGGER.warning("Warning Exception", exc_info=True)
181
+ self.current_status = make_error_status_and_message(exception)
182
+ except Exception as exception:
183
+ LOGGER.error("Exception on running plan", exc_info=True)
184
+
185
+ if self._last_run_aborted:
186
+ # Aborting will cause an exception here that we want to swallow
187
+ self._last_run_aborted = False
188
+ else:
189
+ self.current_status = make_error_status_and_message(exception)
@@ -1,5 +1,6 @@
1
1
  from blueapi.core import BlueskyContext
2
- from dodal.utils import get_beamline_based_on_environment_variable
2
+ from dodal.common.beamlines.beamline_utils import clear_devices
3
+ from dodal.utils import collect_factories, get_beamline_based_on_environment_variable
3
4
 
4
5
  import mx_bluesky.hyperion.experiment_plans as hyperion_plans
5
6
  from mx_bluesky.common.utils.log import LOGGER
@@ -9,11 +10,24 @@ def setup_context(dev_mode: bool = False) -> BlueskyContext:
9
10
  context = BlueskyContext()
10
11
  context.with_plan_module(hyperion_plans)
11
12
 
12
- context.with_dodal_module(
13
- get_beamline_based_on_environment_variable(),
14
- mock=dev_mode,
15
- )
13
+ setup_devices(context, dev_mode)
16
14
 
17
15
  LOGGER.info(f"Plans found in context: {context.plan_functions.keys()}")
18
16
 
19
17
  return context
18
+
19
+
20
+ def clear_all_device_caches(context: BlueskyContext):
21
+ context.unregister_all_devices()
22
+ clear_devices()
23
+
24
+ for f in collect_factories(get_beamline_based_on_environment_variable()).values():
25
+ if hasattr(f, "cache_clear"):
26
+ f.cache_clear() # type: ignore
27
+
28
+
29
+ def setup_devices(context: BlueskyContext, dev_mode: bool):
30
+ context.with_dodal_module(
31
+ get_beamline_based_on_environment_variable(),
32
+ mock=dev_mode,
33
+ )
@@ -0,0 +1 @@
1
+ """The zebra on i03 and i04 (phase1 beamlines) are configured in a specific way which are compatible to the plans in this module"""
File without changes