mx-bluesky 1.4.0__py3-none-any.whl → 1.4.1a0__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 (63) hide show
  1. mx_bluesky/_version.py +2 -2
  2. mx_bluesky/beamlines/i04/thawing_plan.py +1 -1
  3. mx_bluesky/beamlines/i24/serial/dcid.py +19 -21
  4. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +2 -2
  5. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +1 -4
  6. mx_bluesky/beamlines/i24/serial/fixed_target/ft_utils.py +0 -1
  7. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +14 -24
  8. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +18 -76
  9. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +0 -199
  10. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +4 -6
  11. mx_bluesky/beamlines/i24/serial/log.py +1 -1
  12. mx_bluesky/beamlines/i24/serial/parameters/constants.py +0 -1
  13. mx_bluesky/beamlines/i24/serial/run_fixed_target.sh +4 -3
  14. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +78 -80
  15. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +1 -2
  16. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +24 -26
  17. mx_bluesky/beamlines/i24/serial/write_nexus.py +11 -11
  18. mx_bluesky/common/external_interaction/config_server.py +46 -0
  19. mx_bluesky/common/parameters/components.py +52 -15
  20. mx_bluesky/common/parameters/constants.py +6 -1
  21. mx_bluesky/common/parameters/gridscan.py +94 -0
  22. mx_bluesky/{hyperion → common}/parameters/robot_load.py +2 -2
  23. mx_bluesky/common/plans/do_fgs.py +2 -2
  24. mx_bluesky/common/utils/log.py +2 -0
  25. mx_bluesky/hyperion/__main__.py +2 -1
  26. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +4 -4
  27. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +1 -1
  28. mx_bluesky/hyperion/experiment_plans/__init__.py +4 -0
  29. mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py +83 -0
  30. mx_bluesky/hyperion/experiment_plans/common/xrc_result.py +47 -0
  31. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +9 -9
  32. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +131 -89
  33. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +50 -18
  34. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +52 -10
  35. mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +2 -2
  36. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +3 -9
  37. mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +2 -2
  38. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +36 -17
  39. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +2 -2
  40. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +6 -10
  41. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +46 -11
  42. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +18 -3
  43. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +2 -3
  44. mx_bluesky/hyperion/external_interaction/callbacks/common/abstract_event.py +66 -0
  45. mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py +15 -15
  46. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +3 -1
  47. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +5 -3
  48. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +5 -2
  49. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py +5 -4
  50. mx_bluesky/hyperion/external_interaction/config_server.py +8 -37
  51. mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +1 -1
  52. mx_bluesky/hyperion/parameters/components.py +4 -9
  53. mx_bluesky/hyperion/parameters/constants.py +0 -1
  54. mx_bluesky/hyperion/parameters/gridscan.py +33 -76
  55. mx_bluesky/hyperion/parameters/load_centre_collect.py +14 -9
  56. mx_bluesky/hyperion/parameters/rotation.py +15 -6
  57. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1a0.dist-info}/METADATA +35 -34
  58. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1a0.dist-info}/RECORD +62 -58
  59. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1a0.dist-info}/WHEEL +1 -1
  60. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Mapping_py3v1.py +0 -150
  61. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1a0.dist-info}/LICENSE +0 -0
  62. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1a0.dist-info}/entry_points.txt +0 -0
  63. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1a0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,46 @@
1
+ from abc import ABC, abstractmethod
2
+ from functools import cache
3
+
4
+ from daq_config_server.client import ConfigServer
5
+ from pydantic import BaseModel, Field, model_validator
6
+
7
+
8
+ class FeatureFlags(BaseModel, ABC):
9
+ """Abstract class to use ConfigServer to toggle features for an experiment
10
+
11
+ A module wanting to use FeatureFlags should inherit this class, add boolean features
12
+ as attributes, and implement a get_config_server method, which returns a cached creation of
13
+ ConfigServer. See HyperionFeatureFlags for an example
14
+
15
+ Values supplied upon class instantiation will always take priority over the config server. If connection to the server cannot
16
+ be made AND values were not supplied, attributes will use their default values
17
+ """
18
+
19
+ # Feature values supplied at construction will override values from the config server
20
+ overriden_features: dict = Field(default_factory=dict, exclude=True)
21
+
22
+ @staticmethod
23
+ @cache
24
+ @abstractmethod
25
+ def get_config_server() -> ConfigServer: ...
26
+
27
+ @model_validator(mode="before")
28
+ @classmethod
29
+ def mark_overridden_features(cls, values):
30
+ assert isinstance(values, dict)
31
+ values["overriden_features"] = values.copy()
32
+ return values
33
+
34
+ def _get_flags(self):
35
+ flags = type(self).get_config_server().best_effort_get_all_feature_flags()
36
+ return {f: flags[f] for f in flags if f in self.model_fields.keys()}
37
+
38
+ def update_self_from_server(self):
39
+ """Used to update the feature flags from the server during a plan. Where there are flags which were explicitly set from externally supplied parameters, these values will be used instead."""
40
+ for flag, value in self._get_flags().items():
41
+ updated_value = (
42
+ value
43
+ if flag not in self.overriden_features.keys()
44
+ else self.overriden_features[flag]
45
+ )
46
+ setattr(self, flag, updated_value)
@@ -1,21 +1,33 @@
1
- import json
1
+ from __future__ import annotations
2
+
3
+ import os
2
4
  from abc import abstractmethod
3
5
  from collections.abc import Sequence
4
6
  from enum import StrEnum
5
7
  from pathlib import Path
6
- from typing import SupportsInt
8
+ from typing import Literal, SupportsInt, cast
7
9
 
8
10
  from dodal.devices.aperturescatterguard import ApertureValue
9
11
  from dodal.devices.detector import (
10
12
  DetectorParams,
11
13
  TriggerMode,
12
14
  )
13
- from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
15
+ from pydantic import (
16
+ BaseModel,
17
+ ConfigDict,
18
+ Field,
19
+ field_validator,
20
+ model_validator,
21
+ )
14
22
  from pydantic_extra_types.semantic_version import SemanticVersion
15
23
  from scanspec.core import AxesPoints
16
24
  from semver import Version
17
25
 
18
- from mx_bluesky.common.parameters.constants import TEST_MODE, DetectorParamConstants
26
+ from mx_bluesky.common.parameters.constants import (
27
+ TEST_MODE,
28
+ DetectorParamConstants,
29
+ GridscanParamConstants,
30
+ )
19
31
 
20
32
  PARAMETER_VERSION = Version.parse("5.2.0")
21
33
 
@@ -100,11 +112,6 @@ class MxBlueskyParameters(BaseModel):
100
112
  ), f"Parameter version too new! This version of hyperion uses {PARAMETER_VERSION}"
101
113
  return version
102
114
 
103
- @classmethod
104
- def from_json(cls, input: str | None):
105
- assert input is not None
106
- return cls(**json.loads(input))
107
-
108
115
 
109
116
  class WithSnapshot(BaseModel):
110
117
  snapshot_directory: Path
@@ -146,12 +153,12 @@ class DiffractionExperiment(
146
153
 
147
154
  @model_validator(mode="before")
148
155
  @classmethod
149
- def validate_snapshot_directory(cls, values):
150
- snapshot_dir = values.get(
151
- "snapshot_directory", Path(values["storage_directory"], "snapshots")
152
- )
153
- values["snapshot_directory"] = (
154
- snapshot_dir if isinstance(snapshot_dir, Path) else Path(snapshot_dir)
156
+ def validate_directories(cls, values):
157
+ os.makedirs(values["storage_directory"], exist_ok=True)
158
+
159
+ values["snapshot_directory"] = values.get(
160
+ "snapshot_directory",
161
+ Path(values["storage_directory"], "snapshots").as_posix(),
155
162
  )
156
163
  return values
157
164
 
@@ -176,6 +183,14 @@ class WithScan(BaseModel):
176
183
  def num_images(self) -> int: ...
177
184
 
178
185
 
186
+ class WithPandaGridScan(BaseModel):
187
+ """For experiments which use a PandA for constant-motion grid scans"""
188
+
189
+ panda_runup_distance_mm: float = Field(
190
+ default=GridscanParamConstants.PANDA_RUN_UP_DISTANCE_MM
191
+ )
192
+
193
+
179
194
  class SplitScan(BaseModel):
180
195
  @property
181
196
  @abstractmethod
@@ -193,6 +208,28 @@ class WithSample(BaseModel):
193
208
  class DiffractionExperimentWithSample(DiffractionExperiment, WithSample): ...
194
209
 
195
210
 
211
+ class MultiXtalSelection(BaseModel):
212
+ name: str
213
+
214
+
215
+ class TopNByMaxCountSelection(MultiXtalSelection):
216
+ name: Literal["TopNByMaxCount"] = "TopNByMaxCount" # pyright: ignore [reportIncompatibleVariableOverride]
217
+ n: int
218
+
219
+
220
+ class WithCentreSelection(BaseModel):
221
+ select_centres: TopNByMaxCountSelection = Field(
222
+ discriminator="name", default=TopNByMaxCountSelection(n=1)
223
+ )
224
+
225
+ @property
226
+ def selection_params(self) -> MultiXtalSelection:
227
+ """A helper property because pydantic does not allow polymorphism with base classes
228
+ # only type unions"""
229
+ cast1 = cast(MultiXtalSelection, self.select_centres)
230
+ return cast1
231
+
232
+
196
233
  class OptionalXyzStarts(BaseModel):
197
234
  x_start_um: float | None = None
198
235
  y_start_um: float | None = None
@@ -1,6 +1,7 @@
1
1
  from enum import Enum
2
2
 
3
3
  from dodal.devices.aperturescatterguard import ApertureValue
4
+ from dodal.devices.detector import EIGER2_X_16M_SIZE
4
5
  from dodal.devices.zocalo.zocalo_constants import ZOCALO_ENV as ZOCALO_ENV_FROM_DODAL
5
6
  from dodal.utils import get_beamline_name
6
7
  from pydantic.dataclasses import dataclass
@@ -19,6 +20,7 @@ class DocDescriptorNames:
19
20
  HARDWARE_READ_PRE = "read_hardware_for_callbacks_pre_collection"
20
21
  HARDWARE_READ_DURING = "read_hardware_for_callbacks_during_collection"
21
22
  ZOCALO_HW_READ = "zocalo_read_hardware_plan"
23
+ FLYSCAN_RESULTS = "flyscan_results_obtained"
22
24
 
23
25
 
24
26
  @dataclass(frozen=True)
@@ -47,6 +49,7 @@ class PlanNameConstants:
47
49
  ROTATION_MULTI = "multi_rotation_wrapper"
48
50
  ROTATION_OUTER = "rotation_scan_with_cleanup"
49
51
  ROTATION_MAIN = "rotation_scan_main"
52
+ FLYSCAN_RESULTS = "xray_centre_results"
50
53
 
51
54
 
52
55
  @dataclass(frozen=True)
@@ -64,6 +67,7 @@ class HardwareConstants:
64
67
  OAV_REFRESH_DELAY = 0.3
65
68
  PANDA_FGS_RUN_UP_DEFAULT = 0.17
66
69
  CRYOJET_MARGIN_MM = 0.2
70
+ THAWING_TIME = 20
67
71
 
68
72
 
69
73
  @dataclass(frozen=True)
@@ -87,8 +91,9 @@ class DetectorParamConstants:
87
91
  BEAM_XY_LUT_PATH = (
88
92
  "tests/test_data/test_det_dist_converter.txt"
89
93
  if TEST_MODE
90
- else "/dls_sw/{BEAMLINE}/software/daq_configuration/lookup/DetDistToBeamXYConverter.txt"
94
+ else f"/dls_sw/{BEAMLINE}/software/daq_configuration/lookup/DetDistToBeamXYConverter.txt"
91
95
  )
96
+ DETECTOR = EIGER2_X_16M_SIZE
92
97
 
93
98
 
94
99
  @dataclass(frozen=True)
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from dodal.devices.aperturescatterguard import ApertureValue
6
+ from dodal.devices.detector import (
7
+ DetectorParams,
8
+ )
9
+ from pydantic import Field
10
+
11
+ from mx_bluesky.common.parameters.components import (
12
+ DiffractionExperimentWithSample,
13
+ IspybExperimentType,
14
+ OptionalGonioAngleStarts,
15
+ WithScan,
16
+ XyzStarts,
17
+ )
18
+ from mx_bluesky.common.parameters.constants import (
19
+ DetectorParamConstants,
20
+ GridscanParamConstants,
21
+ HardwareConstants,
22
+ )
23
+ from mx_bluesky.common.parameters.robot_load import RobotLoadAndEnergyChange
24
+
25
+
26
+ class GridCommon(
27
+ DiffractionExperimentWithSample,
28
+ OptionalGonioAngleStarts,
29
+ ):
30
+ grid_width_um: float = Field(default=GridscanParamConstants.WIDTH_UM)
31
+ exposure_time_s: float = Field(default=GridscanParamConstants.EXPOSURE_TIME_S)
32
+ use_roi_mode: bool = Field(default=GridscanParamConstants.USE_ROI)
33
+
34
+ ispyb_experiment_type: IspybExperimentType = Field(
35
+ default=IspybExperimentType.GRIDSCAN_3D
36
+ )
37
+ selected_aperture: ApertureValue | None = Field(default=ApertureValue.SMALL)
38
+
39
+ @property
40
+ def detector_params(self):
41
+ self.det_dist_to_beam_converter_path = (
42
+ self.det_dist_to_beam_converter_path
43
+ or DetectorParamConstants.BEAM_XY_LUT_PATH
44
+ )
45
+ optional_args = {}
46
+ if self.run_number:
47
+ optional_args["run_number"] = self.run_number
48
+ assert (
49
+ self.detector_distance_mm is not None
50
+ ), "Detector distance must be filled before generating DetectorParams"
51
+ os.makedirs(self.storage_directory, exist_ok=True)
52
+ return DetectorParams(
53
+ detector_size_constants=DetectorParamConstants.DETECTOR,
54
+ expected_energy_ev=self.demand_energy_ev,
55
+ exposure_time=self.exposure_time_s,
56
+ directory=self.storage_directory,
57
+ prefix=self.file_name,
58
+ detector_distance=self.detector_distance_mm,
59
+ omega_start=self.omega_start_deg or 0,
60
+ omega_increment=0,
61
+ num_images_per_trigger=1,
62
+ num_triggers=self.num_images,
63
+ use_roi_mode=self.use_roi_mode,
64
+ det_dist_to_beam_converter_path=self.det_dist_to_beam_converter_path,
65
+ trigger_mode=self.trigger_mode,
66
+ **optional_args,
67
+ )
68
+
69
+
70
+ class RobotLoadThenCentre(GridCommon):
71
+ thawing_time: float = Field(default=HardwareConstants.THAWING_TIME)
72
+
73
+ def robot_load_params(self):
74
+ my_params = self.model_dump()
75
+ return RobotLoadAndEnergyChange(**my_params)
76
+
77
+ def pin_centre_then_xray_centre_params(self):
78
+ my_params = self.model_dump()
79
+ del my_params["thawing_time"]
80
+ return PinTipCentreThenXrayCentre(**my_params)
81
+
82
+
83
+ class GridScanWithEdgeDetect(GridCommon):
84
+ box_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)
85
+
86
+
87
+ class PinTipCentreThenXrayCentre(GridCommon):
88
+ tip_offset_um: float = 0
89
+
90
+
91
+ class SpecifiedGrid(XyzStarts, WithScan):
92
+ """A specified grid is one which has defined values for the start position,
93
+ grid and box sizes, etc., as opposed to parameters for a plan which will create
94
+ those parameters at some point (e.g. through optical pin detection)."""
@@ -7,10 +7,10 @@ from mx_bluesky.common.parameters.components import (
7
7
  WithSnapshot,
8
8
  WithVisit,
9
9
  )
10
- from mx_bluesky.hyperion.parameters.constants import CONST
10
+ from mx_bluesky.common.parameters.constants import HardwareConstants
11
11
 
12
12
 
13
13
  class RobotLoadAndEnergyChange(
14
14
  MxBlueskyParameters, WithSample, WithSnapshot, WithOptionalEnergyChange, WithVisit
15
15
  ):
16
- thawing_time: float = Field(default=CONST.I03.THAWING_TIME)
16
+ thawing_time: float = Field(default=HardwareConstants.THAWING_TIME)
@@ -3,7 +3,7 @@ from time import time
3
3
 
4
4
  import bluesky.plan_stubs as bps
5
5
  import bluesky.preprocessors as bpp
6
- from blueapi.core import MsgGenerator
6
+ from bluesky.utils import MsgGenerator
7
7
  from dodal.devices.eiger import EigerDetector
8
8
  from dodal.devices.fast_grid_scan import FastGridScanCommon
9
9
  from dodal.devices.synchrotron import Synchrotron
@@ -11,7 +11,7 @@ from dodal.devices.zocalo.zocalo_results import (
11
11
  ZOCALO_STAGE_GROUP,
12
12
  )
13
13
  from dodal.log import LOGGER
14
- from dodal.plans.check_topup import check_topup_and_wait_if_necessary
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
17
  from mx_bluesky.common.device_setup_plans.read_hardware_for_setup import (
@@ -12,6 +12,8 @@ from dodal.log import (
12
12
  )
13
13
  from dodal.log import LOGGER as dodal_logger
14
14
 
15
+ LOGGER = logging.getLogger("mx-bluesky")
16
+
15
17
  __logger_handlers: DodalLogHandlers | None = None
16
18
 
17
19
 
@@ -7,9 +7,10 @@ from queue import Queue
7
7
  from traceback import format_exception
8
8
  from typing import Any
9
9
 
10
- from blueapi.core import BlueskyContext, MsgGenerator
10
+ from blueapi.core import BlueskyContext
11
11
  from bluesky.callbacks.zmq import Publisher
12
12
  from bluesky.run_engine import RunEngine
13
+ from bluesky.utils import MsgGenerator
13
14
  from flask import Flask, request
14
15
  from flask_restful import Api, Resource
15
16
  from pydantic.dataclasses import dataclass
@@ -4,7 +4,7 @@ from importlib import resources
4
4
  from pathlib import Path
5
5
 
6
6
  import bluesky.plan_stubs as bps
7
- from blueapi.core import MsgGenerator
7
+ from bluesky.utils import MsgGenerator
8
8
  from dodal.common.beamlines.beamline_utils import get_path_provider
9
9
  from dodal.devices.fast_grid_scan import PandAGridScanParams
10
10
  from ophyd_async.core import load_device
@@ -63,12 +63,12 @@ def _get_seq_table(
63
63
  An instance of SeqTable describing the panda sequencer table
64
64
  """
65
65
 
66
- start_of_grid_x_counts = int(parameters.x_start * MM_TO_ENCODER_COUNTS)
66
+ start_of_grid_x_counts = int(parameters.x_start_mm * MM_TO_ENCODER_COUNTS)
67
67
 
68
68
  # x_start is the first trigger point, so we need to travel to x_steps-1 for the final trigger point
69
69
  end_of_grid_x_counts = int(
70
70
  start_of_grid_x_counts
71
- + (parameters.x_step_size * (parameters.x_steps - 1) * MM_TO_ENCODER_COUNTS)
71
+ + (parameters.x_step_size_mm * (parameters.x_steps - 1) * MM_TO_ENCODER_COUNTS)
72
72
  )
73
73
 
74
74
  exposure_distance_x_counts = int(exposure_distance_mm * MM_TO_ENCODER_COUNTS)
@@ -140,7 +140,7 @@ def setup_panda_for_flyscan(
140
140
  """
141
141
  assert parameters.x_steps > 0
142
142
  assert time_between_x_steps_ms * 1000 >= exposure_time_s
143
- assert sample_velocity_mm_per_s * exposure_time_s < parameters.x_step_size
143
+ assert sample_velocity_mm_per_s * exposure_time_s < parameters.x_step_size_mm
144
144
 
145
145
  yield from bps.stage(panda, group="panda-config")
146
146
 
@@ -3,7 +3,7 @@ from functools import wraps
3
3
 
4
4
  import bluesky.plan_stubs as bps
5
5
  import bluesky.preprocessors as bpp
6
- from blueapi.core import MsgGenerator
6
+ from bluesky.utils import MsgGenerator
7
7
  from dodal.devices.zebra import (
8
8
  AUTO_SHUTTER_GATE,
9
9
  AUTO_SHUTTER_INPUT_1,
@@ -9,6 +9,9 @@ from mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan import (
9
9
  from mx_bluesky.hyperion.experiment_plans.grid_detect_then_xray_centre_plan import (
10
10
  grid_detect_then_xray_centre,
11
11
  )
12
+ from mx_bluesky.hyperion.experiment_plans.load_centre_collect_full_plan import (
13
+ load_centre_collect_full,
14
+ )
12
15
  from mx_bluesky.hyperion.experiment_plans.pin_centre_then_xray_centre_plan import (
13
16
  pin_tip_centre_then_xray_centre,
14
17
  )
@@ -27,4 +30,5 @@ __all__ = [
27
30
  "pin_tip_centre_then_xray_centre",
28
31
  "multi_rotation_scan",
29
32
  "robot_load_then_centre",
33
+ "load_centre_collect_full",
30
34
  ]
@@ -0,0 +1,83 @@
1
+ import bluesky.plan_stubs as bps
2
+ import bluesky.preprocessors as bpp
3
+ import numpy
4
+ from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue
5
+ from dodal.devices.smargon import Smargon, StubPosition
6
+
7
+ from mx_bluesky.common.utils.tracing import TRACER
8
+ from mx_bluesky.hyperion.device_setup_plans.manipulate_sample import move_x_y_z
9
+ from mx_bluesky.hyperion.experiment_plans.common.xrc_result import XRayCentreResult
10
+ from mx_bluesky.hyperion.log import LOGGER
11
+ from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan
12
+
13
+
14
+ def change_aperture_then_move_to_xtal(
15
+ best_hit: XRayCentreResult,
16
+ smargon: Smargon,
17
+ aperture_scatterguard: ApertureScatterguard,
18
+ parameters: HyperionThreeDGridScan | None = None,
19
+ ):
20
+ """For the given x-ray centring result,
21
+ * Change the aperture so that the beam size is comparable to the crystal size
22
+ * Centre on the centre-of-mass
23
+ * 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, bounding_box_size
31
+ )
32
+ else:
33
+ LOGGER.warning("No bounding box size received")
34
+
35
+ # once we have the results, go to the appropriate position
36
+ LOGGER.info("Moving to centre of mass.")
37
+ with TRACER.start_span("move_to_result"):
38
+ x, y, z = best_hit.centre_of_mass_mm
39
+ yield from move_x_y_z(smargon, x, y, z, wait=True)
40
+
41
+ # TODO support for setting stub offsets in multipin
42
+ # https://github.com/DiamondLightSource/mx-bluesky/issues/552
43
+ if parameters and parameters.FGS_params.set_stub_offsets:
44
+ LOGGER.info("Recentring smargon co-ordinate system to this point.")
45
+ yield from bps.mv(
46
+ # See: https://github.com/bluesky/bluesky/issues/1809
47
+ smargon.stub_offsets, # type: ignore
48
+ StubPosition.CURRENT_AS_CENTER, # type: ignore
49
+ )
50
+
51
+
52
+ def _set_aperture_for_bbox_mm(
53
+ aperture_device: ApertureScatterguard, bbox_size_mm: list[float] | numpy.ndarray
54
+ ):
55
+ # TODO confirm correction factor see https://github.com/DiamondLightSource/mx-bluesky/issues/618
56
+ ASSUMED_BOX_SIZE_MM = 0.020
57
+ bbox_size_boxes = [round(mm / ASSUMED_BOX_SIZE_MM) for mm in bbox_size_mm]
58
+ yield from set_aperture_for_bbox_size(aperture_device, bbox_size_boxes)
59
+
60
+
61
+ def set_aperture_for_bbox_size(
62
+ aperture_device: ApertureScatterguard,
63
+ bbox_size: list[int] | numpy.ndarray,
64
+ ):
65
+ # bbox_size is [x,y,z], for i03 we only care about x
66
+ new_selected_aperture = (
67
+ ApertureValue.MEDIUM if bbox_size[0] < 2 else ApertureValue.LARGE
68
+ )
69
+ LOGGER.info(
70
+ f"Setting aperture to {new_selected_aperture} based on bounding box size {bbox_size}."
71
+ )
72
+
73
+ @bpp.set_run_key_decorator("change_aperture")
74
+ @bpp.run_decorator(
75
+ md={
76
+ "subplan_name": "change_aperture",
77
+ "aperture_size": new_selected_aperture.value,
78
+ }
79
+ )
80
+ def set_aperture():
81
+ yield from bps.abs_set(aperture_device, new_selected_aperture)
82
+
83
+ yield from set_aperture()
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ from collections.abc import Callable, Sequence
5
+ from functools import partial
6
+
7
+ import numpy as np
8
+
9
+ from mx_bluesky.common.parameters.components import (
10
+ MultiXtalSelection,
11
+ TopNByMaxCountSelection,
12
+ )
13
+
14
+
15
+ @dataclasses.dataclass
16
+ class XRayCentreResult:
17
+ """Represents information about a hit from an X-ray centring."""
18
+
19
+ centre_of_mass_mm: np.ndarray
20
+ bounding_box_mm: tuple[np.ndarray, np.ndarray]
21
+ max_count: int
22
+ total_count: int
23
+
24
+ def __eq__(self, o):
25
+ return (
26
+ isinstance(o, XRayCentreResult)
27
+ and o.max_count == self.max_count
28
+ and o.total_count == self.total_count
29
+ and all(o.centre_of_mass_mm == self.centre_of_mass_mm)
30
+ and all(o.bounding_box_mm[0] == self.bounding_box_mm[0])
31
+ and all(o.bounding_box_mm[1] == self.bounding_box_mm[1])
32
+ )
33
+
34
+
35
+ def top_n_by_max_count(
36
+ unfiltered: Sequence[XRayCentreResult], n: int
37
+ ) -> Sequence[XRayCentreResult]:
38
+ sorted_hits = sorted(unfiltered, key=lambda result: result.max_count, reverse=True)
39
+ return sorted_hits[:n]
40
+
41
+
42
+ def resolve_selection_fn(
43
+ params: MultiXtalSelection,
44
+ ) -> Callable[[Sequence[XRayCentreResult]], Sequence[XRayCentreResult]]:
45
+ if isinstance(params, TopNByMaxCountSelection):
46
+ return partial(top_n_by_max_count, n=params.n)
47
+ raise ValueError(f"Invalid selection function {params.name}")
@@ -5,6 +5,11 @@ from typing import TypedDict
5
5
 
6
6
  import mx_bluesky.hyperion.experiment_plans.flyscan_xray_centre_plan as flyscan_xray_centre_plan
7
7
  import mx_bluesky.hyperion.experiment_plans.rotation_scan_plan as rotation_scan_plan
8
+ from mx_bluesky.common.parameters.gridscan import (
9
+ GridScanWithEdgeDetect,
10
+ PinTipCentreThenXrayCentre,
11
+ RobotLoadThenCentre,
12
+ )
8
13
  from mx_bluesky.hyperion.experiment_plans import (
9
14
  grid_detect_then_xray_centre_plan,
10
15
  load_centre_collect_full_plan,
@@ -18,12 +23,7 @@ from mx_bluesky.hyperion.external_interaction.callbacks.common.callback_util imp
18
23
  create_robot_load_and_centre_callbacks,
19
24
  create_rotation_callbacks,
20
25
  )
21
- from mx_bluesky.hyperion.parameters.gridscan import (
22
- GridScanWithEdgeDetect,
23
- PinTipCentreThenXrayCentre,
24
- RobotLoadThenCentre,
25
- ThreeDGridScan,
26
- )
26
+ from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan
27
27
  from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
28
28
  from mx_bluesky.hyperion.parameters.rotation import MultiRotationScan, RotationScan
29
29
 
@@ -39,7 +39,7 @@ def do_nothing():
39
39
  class ExperimentRegistryEntry(TypedDict):
40
40
  setup: Callable
41
41
  param_type: type[
42
- ThreeDGridScan
42
+ HyperionThreeDGridScan
43
43
  | GridScanWithEdgeDetect
44
44
  | RotationScan
45
45
  | MultiRotationScan
@@ -53,7 +53,7 @@ class ExperimentRegistryEntry(TypedDict):
53
53
  PLAN_REGISTRY: dict[str, ExperimentRegistryEntry] = {
54
54
  "flyscan_xray_centre": {
55
55
  "setup": flyscan_xray_centre_plan.create_devices,
56
- "param_type": ThreeDGridScan,
56
+ "param_type": HyperionThreeDGridScan,
57
57
  "callbacks_factory": create_gridscan_callbacks,
58
58
  },
59
59
  "grid_detect_then_xray_centre": {
@@ -81,7 +81,7 @@ PLAN_REGISTRY: dict[str, ExperimentRegistryEntry] = {
81
81
  "param_type": MultiRotationScan,
82
82
  "callbacks_factory": create_rotation_callbacks,
83
83
  },
84
- "load_centre_collect_full_plan": {
84
+ "load_centre_collect_full": {
85
85
  "setup": load_centre_collect_full_plan.create_devices,
86
86
  "param_type": LoadCentreCollect,
87
87
  "callbacks_factory": create_load_centre_collect_callbacks,