mx-bluesky 1.4.2__py3-none-any.whl → 1.4.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 (92) hide show
  1. mx_bluesky/_version.py +2 -2
  2. mx_bluesky/beamlines/i24/serial/dcid.py +3 -3
  3. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +24 -9
  4. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +13 -4
  5. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +1 -1
  6. mx_bluesky/beamlines/i24/serial/parameters/__init__.py +2 -1
  7. mx_bluesky/beamlines/i24/serial/parameters/constants.py +13 -5
  8. mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py +20 -4
  9. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +40 -11
  10. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +81 -40
  11. mx_bluesky/{hyperion/external_interaction/callbacks → common/external_interaction/callbacks/common}/aperture_change_callback.py +1 -1
  12. mx_bluesky/{hyperion/external_interaction/callbacks → common/external_interaction/callbacks/common}/grid_detection_callback.py +19 -1
  13. mx_bluesky/{hyperion/external_interaction/callbacks → common/external_interaction/callbacks/common}/ispyb_callback_base.py +40 -34
  14. mx_bluesky/{hyperion → common}/external_interaction/callbacks/common/ispyb_mapping.py +4 -4
  15. mx_bluesky/{hyperion/external_interaction/callbacks → common/external_interaction/callbacks/common}/logging_callback.py +1 -1
  16. mx_bluesky/{hyperion/external_interaction/callbacks → common/external_interaction/callbacks/common}/zocalo_callback.py +14 -9
  17. mx_bluesky/{hyperion → common}/external_interaction/callbacks/xray_centre/ispyb_callback.py +39 -34
  18. mx_bluesky/{hyperion → common}/external_interaction/callbacks/xray_centre/ispyb_mapping.py +2 -2
  19. mx_bluesky/{hyperion → common}/external_interaction/callbacks/xray_centre/nexus_callback.py +20 -15
  20. mx_bluesky/common/external_interaction/config_server.py +11 -0
  21. mx_bluesky/common/external_interaction/ispyb/__init__.py +0 -0
  22. mx_bluesky/{hyperion → common}/external_interaction/ispyb/data_model.py +2 -0
  23. mx_bluesky/{hyperion → common}/external_interaction/ispyb/exp_eye_store.py +5 -5
  24. mx_bluesky/{hyperion → common}/external_interaction/ispyb/ispyb_store.py +20 -18
  25. mx_bluesky/{hyperion → common}/external_interaction/ispyb/ispyb_utils.py +2 -2
  26. mx_bluesky/common/external_interaction/nexus/__init__.py +0 -0
  27. mx_bluesky/{hyperion → common}/external_interaction/nexus/nexus_utils.py +21 -6
  28. mx_bluesky/{hyperion → common}/external_interaction/nexus/write_nexus.py +5 -5
  29. mx_bluesky/common/external_interaction/test_config_server.py +38 -0
  30. mx_bluesky/common/parameters/components.py +9 -7
  31. mx_bluesky/common/parameters/constants.py +1 -0
  32. mx_bluesky/common/parameters/gridscan.py +102 -53
  33. mx_bluesky/common/plans/do_fgs.py +4 -4
  34. mx_bluesky/{hyperion → common/utils}/exceptions.py +15 -1
  35. mx_bluesky/common/utils/log.py +17 -7
  36. mx_bluesky/hyperion/__main__.py +15 -14
  37. mx_bluesky/hyperion/device_setup_plans/check_beamstop.py +27 -0
  38. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +13 -6
  39. mx_bluesky/hyperion/device_setup_plans/manipulate_sample.py +1 -1
  40. mx_bluesky/hyperion/device_setup_plans/position_detector.py +1 -1
  41. mx_bluesky/hyperion/device_setup_plans/read_hardware_for_setup.py +3 -3
  42. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +21 -4
  43. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +62 -36
  44. mx_bluesky/hyperion/device_setup_plans/smargon.py +1 -1
  45. mx_bluesky/hyperion/device_setup_plans/utils.py +4 -0
  46. mx_bluesky/hyperion/device_setup_plans/xbpm_feedback.py +8 -8
  47. mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py +28 -17
  48. mx_bluesky/hyperion/experiment_plans/common/xrc_result.py +10 -1
  49. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +9 -9
  50. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +39 -49
  51. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +22 -23
  52. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +4 -11
  53. mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +3 -3
  54. mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +6 -14
  55. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +12 -11
  56. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +2 -2
  57. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +9 -4
  58. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +10 -11
  59. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +33 -14
  60. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +2 -2
  61. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +28 -21
  62. mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py +43 -32
  63. mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +22 -15
  64. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +25 -24
  65. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +1 -1
  66. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +13 -9
  67. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/sample_handling_callback.py +12 -46
  68. mx_bluesky/hyperion/external_interaction/config_server.py +15 -1
  69. mx_bluesky/hyperion/parameters/components.py +3 -2
  70. mx_bluesky/hyperion/parameters/constants.py +1 -0
  71. mx_bluesky/hyperion/parameters/gridscan.py +56 -89
  72. mx_bluesky/hyperion/parameters/load_centre_collect.py +51 -6
  73. mx_bluesky/hyperion/parameters/robot_load.py +40 -0
  74. mx_bluesky/hyperion/parameters/rotation.py +28 -3
  75. mx_bluesky/hyperion/utils/context.py +1 -1
  76. mx_bluesky/hyperion/utils/validation.py +4 -2
  77. {mx_bluesky-1.4.2.dist-info → mx_bluesky-1.4.3.dist-info}/METADATA +6 -6
  78. {mx_bluesky-1.4.2.dist-info → mx_bluesky-1.4.3.dist-info}/RECORD +89 -87
  79. {mx_bluesky-1.4.2.dist-info → mx_bluesky-1.4.3.dist-info}/WHEEL +1 -1
  80. mx_bluesky/common/parameters/robot_load.py +0 -16
  81. mx_bluesky/hyperion/external_interaction/exceptions.py +0 -4
  82. mx_bluesky/hyperion/log.py +0 -15
  83. /mx_bluesky/{hyperion/external_interaction/callbacks/xray_centre → common/external_interaction}/__init__.py +0 -0
  84. /mx_bluesky/{hyperion/external_interaction/ispyb → common/external_interaction/callbacks/common}/__init__.py +0 -0
  85. /mx_bluesky/{hyperion → common}/external_interaction/callbacks/common/abstract_event.py +0 -0
  86. /mx_bluesky/{hyperion/external_interaction/callbacks → common/external_interaction/callbacks/common}/log_uid_tag_callback.py +0 -0
  87. /mx_bluesky/{hyperion/external_interaction/callbacks → common/external_interaction/callbacks/common}/plan_reactive_callback.py +0 -0
  88. /mx_bluesky/{hyperion/external_interaction/nexus → common/external_interaction/callbacks/xray_centre}/__init__.py +0 -0
  89. /mx_bluesky/{hyperion → common}/utils/utils.py +0 -0
  90. {mx_bluesky-1.4.2.dist-info → mx_bluesky-1.4.3.dist-info}/LICENSE +0 -0
  91. {mx_bluesky-1.4.2.dist-info → mx_bluesky-1.4.3.dist-info}/entry_points.txt +0 -0
  92. {mx_bluesky-1.4.2.dist-info → mx_bluesky-1.4.3.dist-info}/top_level.txt +0 -0
@@ -2,20 +2,22 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
- from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import (
5
+ from mx_bluesky.common.external_interaction.callbacks.common.logging_callback import (
6
+ format_doc_for_log,
7
+ )
8
+ from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import (
6
9
  PlanReactiveCallback,
7
10
  )
8
- from mx_bluesky.hyperion.external_interaction.nexus.nexus_utils import (
11
+ from mx_bluesky.common.external_interaction.nexus.nexus_utils import (
12
+ AxisDirection,
9
13
  create_beam_and_attenuator_parameters,
10
14
  vds_type_based_on_bit_depth,
11
15
  )
12
- from mx_bluesky.hyperion.external_interaction.nexus.write_nexus import NexusWriter
13
- from mx_bluesky.hyperion.log import NEXUS_LOGGER
16
+ from mx_bluesky.common.external_interaction.nexus.write_nexus import NexusWriter
17
+ from mx_bluesky.common.utils.log import NEXUS_LOGGER
14
18
  from mx_bluesky.hyperion.parameters.constants import CONST
15
19
  from mx_bluesky.hyperion.parameters.rotation import RotationScan
16
20
 
17
- from ..logging_callback import format_doc_for_log
18
-
19
21
  if TYPE_CHECKING:
20
22
  from event_model.documents import Event, EventDescriptor, RunStart
21
23
 
@@ -64,7 +66,7 @@ class RotationNexusFileCallback(PlanReactiveCallback):
64
66
  self.writer.attenuator,
65
67
  ) = create_beam_and_attenuator_parameters(
66
68
  data["dcm-energy_in_kev"],
67
- data["flux_flux_reading"],
69
+ data["flux-flux_reading"],
68
70
  data["attenuator-actual_transmission"],
69
71
  )
70
72
  vds_data_type = vds_type_based_on_bit_depth(doc["data"]["eiger_bit_depth"])
@@ -78,7 +80,7 @@ class RotationNexusFileCallback(PlanReactiveCallback):
78
80
  self.meta_data_run_number = doc.get("meta_data_run_number")
79
81
  if doc.get("subplan_name") == CONST.PLAN.ROTATION_OUTER:
80
82
  self.run_uid = doc.get("uid")
81
- hyperion_params = doc.get("hyperion_parameters")
83
+ hyperion_params = doc.get("mx_bluesky_parameters")
82
84
  assert isinstance(hyperion_params, str)
83
85
  NEXUS_LOGGER.info(
84
86
  f"Nexus writer received start document with experiment parameters {hyperion_params}"
@@ -100,5 +102,7 @@ class RotationNexusFileCallback(PlanReactiveCallback):
100
102
  vds_start_index=parameters.nexus_vds_start_img,
101
103
  full_num_of_images=self.full_num_of_images,
102
104
  meta_data_run_number=self.meta_data_run_number,
103
- rotation_direction=parameters.rotation_direction,
105
+ axis_direction=AxisDirection.NEGATIVE
106
+ if parameters.features.omega_flip
107
+ else AxisDirection.POSITIVE,
104
108
  )
@@ -1,45 +1,14 @@
1
- import dataclasses
2
- from collections.abc import Generator
3
- from functools import partial
4
- from typing import Any
1
+ from event_model import RunStart, RunStop
5
2
 
6
- import bluesky.plan_stubs as bps
7
- from bluesky.preprocessors import contingency_wrapper
8
- from bluesky.utils import Msg, make_decorator
9
- from event_model import Event, EventDescriptor, RunStart
10
-
11
- from mx_bluesky.hyperion.exceptions import CrystalNotFoundException, SampleException
12
- from mx_bluesky.hyperion.external_interaction.callbacks.common.abstract_event import (
13
- AbstractEvent,
14
- )
15
- from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import (
3
+ from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import (
16
4
  PlanReactiveCallback,
17
5
  )
18
- from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import (
6
+ from mx_bluesky.common.external_interaction.ispyb.exp_eye_store import (
19
7
  BLSampleStatus,
20
8
  ExpeyeInteraction,
21
9
  )
22
- from mx_bluesky.hyperion.log import ISPYB_LOGGER
23
- from mx_bluesky.hyperion.parameters.constants import CONST
24
-
25
- # TODO remove this event-raising shenanigans once
26
- # https://github.com/bluesky/bluesky/issues/1829 is addressed
27
-
28
-
29
- @dataclasses.dataclass(frozen=True)
30
- class _ExceptionEvent(AbstractEvent):
31
- exception_type: str
32
-
33
-
34
- def _exception_interceptor(exception: Exception) -> Generator[Msg, Any, Any]:
35
- yield from bps.create(CONST.DESCRIPTORS.SAMPLE_HANDLING_EXCEPTION)
36
- yield from bps.read(_ExceptionEvent(type(exception).__name__))
37
- yield from bps.save()
38
-
39
-
40
- sample_handling_callback_decorator = make_decorator(
41
- partial(contingency_wrapper, except_plan=_exception_interceptor)
42
- )
10
+ from mx_bluesky.common.utils.exceptions import CrystalNotFoundException, SampleException
11
+ from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER
43
12
 
44
13
 
45
14
  class SampleHandlingCallback(PlanReactiveCallback):
@@ -47,7 +16,7 @@ class SampleHandlingCallback(PlanReactiveCallback):
47
16
  field according to the type of exception raised."""
48
17
 
49
18
  def __init__(self):
50
- super().__init__(log=ISPYB_LOGGER)
19
+ super().__init__(log=ISPYB_ZOCALO_CALLBACK_LOGGER)
51
20
  self._sample_id: int | None = None
52
21
  self._descriptor: str | None = None
53
22
 
@@ -57,16 +26,13 @@ class SampleHandlingCallback(PlanReactiveCallback):
57
26
  self.log.info(f"Recording sample ID at run start {sample_id}")
58
27
  self._sample_id = sample_id
59
28
 
60
- def activity_gated_descriptor(self, doc: EventDescriptor) -> EventDescriptor | None:
61
- if doc.get("name") == CONST.DESCRIPTORS.SAMPLE_HANDLING_EXCEPTION:
62
- self._descriptor = doc["uid"]
63
- return super().activity_gated_descriptor(doc)
64
-
65
- def activity_gated_event(self, doc: Event) -> Event | None:
66
- if doc["descriptor"] == self._descriptor:
67
- exception_type = doc["data"]["exception_type"]
29
+ def activity_gated_stop(self, doc: RunStop) -> RunStop:
30
+ if doc["exit_status"] != "success":
31
+ exception_type, message = SampleException.type_and_message_from_reason(
32
+ doc.get("reason", "")
33
+ )
68
34
  self.log.info(
69
- f"Sample handling callback intercepted exception of type {exception_type}"
35
+ f"Sample handling callback intercepted exception of type {exception_type}: {message}"
70
36
  )
71
37
  self._record_exception(exception_type)
72
38
  return doc
@@ -3,11 +3,24 @@ from functools import cache
3
3
  from daq_config_server.client import ConfigServer
4
4
 
5
5
  from mx_bluesky.common.external_interaction.config_server import FeatureFlags
6
- from mx_bluesky.hyperion.log import LOGGER
6
+ from mx_bluesky.common.utils.log import LOGGER
7
7
  from mx_bluesky.hyperion.parameters.constants import CONST
8
8
 
9
9
 
10
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
+ compare_cpu_and_gpu_zocalo: If True then GPU result processing is enabled alongside CPU, if False then
17
+ CPU only is used.
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.
22
+ """
23
+
11
24
  @staticmethod
12
25
  @cache
13
26
  def get_config_server() -> ConfigServer:
@@ -16,3 +29,4 @@ class HyperionFeatureFlags(FeatureFlags):
16
29
  use_panda_for_gridscan: bool = CONST.I03.USE_PANDA_FOR_GRIDSCAN
17
30
  compare_cpu_and_gpu_zocalo: bool = CONST.I03.COMPARE_CPU_AND_GPU_ZOCALO
18
31
  set_stub_offsets: bool = CONST.I03.SET_STUB_OFFSETS
32
+ omega_flip: bool = CONST.I03.OMEGA_FLIP
@@ -1,7 +1,8 @@
1
- from pydantic import BaseModel, Field
1
+ from pydantic import Field
2
2
 
3
+ from mx_bluesky.common.parameters.components import WithPandaGridScan
3
4
  from mx_bluesky.hyperion.external_interaction.config_server import HyperionFeatureFlags
4
5
 
5
6
 
6
- class WithHyperionFeatures(BaseModel):
7
+ class WithHyperionUDCFeatures(WithPandaGridScan):
7
8
  features: HyperionFeatureFlags = Field(default=HyperionFeatureFlags())
@@ -28,6 +28,7 @@ class I03Constants:
28
28
  SHUTTER_TIME_S = 0.06
29
29
  USE_PANDA_FOR_GRIDSCAN = False
30
30
  SET_STUB_OFFSETS = False
31
+ OMEGA_FLIP = True
31
32
 
32
33
  # Turns on GPU processing for zocalo and logs a comparison between GPU and CPU-
33
34
  # processed results. GPU results never used in analysis for now
@@ -7,27 +7,22 @@ from dodal.devices.fast_grid_scan import (
7
7
  PandAGridScanParams,
8
8
  ZebraGridScanParams,
9
9
  )
10
- from pydantic import Field, PrivateAttr
11
- from scanspec.core import Path as ScanPath
12
- from scanspec.specs import Line, Static
13
-
14
- from mx_bluesky.common.parameters.components import (
15
- SplitScan,
16
- WithOptionalEnergyChange,
17
- WithPandaGridScan,
18
- )
10
+ from pydantic import Field
11
+
12
+ from mx_bluesky.common.parameters.constants import GridscanParamConstants
19
13
  from mx_bluesky.common.parameters.gridscan import (
20
14
  GridCommon,
21
- SpecifiedGrid,
15
+ SpecifiedThreeDGridScan,
22
16
  )
23
- from mx_bluesky.hyperion.parameters.components import WithHyperionFeatures
17
+ from mx_bluesky.hyperion.parameters.components import WithHyperionUDCFeatures
24
18
  from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants
25
19
 
26
20
 
27
- class HyperionGridCommon(GridCommon, WithHyperionFeatures):
28
- # This class only exists so that we can properly select enable_dev_shm. Remove in
29
- # https://github.com/DiamondLightSource/hyperion/issues/1395"""
21
+ class GridCommonWithHyperionDetectorParams(GridCommon, WithHyperionUDCFeatures):
22
+ """Used by models which require detector parameters but have no specifications of the grid"""
30
23
 
24
+ # These detector params only exist so that we can properly select enable_dev_shm. Remove in
25
+ # https://github.com/DiamondLightSource/hyperion/issues/1395"""
31
26
  @property
32
27
  def detector_params(self):
33
28
  self.det_dist_to_beam_converter_path = (
@@ -37,9 +32,9 @@ class HyperionGridCommon(GridCommon, WithHyperionFeatures):
37
32
  optional_args = {}
38
33
  if self.run_number:
39
34
  optional_args["run_number"] = self.run_number
40
- assert (
41
- self.detector_distance_mm is not None
42
- ), "Detector distance must be filled before generating DetectorParams"
35
+ assert self.detector_distance_mm is not None, (
36
+ "Detector distance must be filled before generating DetectorParams"
37
+ )
43
38
  return DetectorParams(
44
39
  detector_size_constants=I03Constants.DETECTOR,
45
40
  expected_energy_ev=self.demand_energy_ev,
@@ -59,28 +54,42 @@ class HyperionGridCommon(GridCommon, WithHyperionFeatures):
59
54
  )
60
55
 
61
56
 
62
- class HyperionThreeDGridScan(
63
- HyperionGridCommon,
64
- SpecifiedGrid,
65
- SplitScan,
66
- WithOptionalEnergyChange,
67
- WithPandaGridScan,
68
- ):
69
- """Parameters representing a so-called 3D grid scan, which consists of doing a
70
- gridscan in X and Y, followed by one in X and Z."""
71
-
72
- grid1_omega_deg: float = Field(default=CONST.PARAM.GRIDSCAN.OMEGA_1) # type: ignore
73
- grid2_omega_deg: float = Field(default=CONST.PARAM.GRIDSCAN.OMEGA_2)
74
- x_step_size_um: float = Field(default=CONST.PARAM.GRIDSCAN.BOX_WIDTH_UM)
75
- y_step_size_um: float = Field(default=CONST.PARAM.GRIDSCAN.BOX_WIDTH_UM)
76
- z_step_size_um: float = Field(default=CONST.PARAM.GRIDSCAN.BOX_WIDTH_UM)
77
- y2_start_um: float
78
- z2_start_um: float
79
- x_steps: int = Field(gt=0)
80
- y_steps: int = Field(gt=0)
81
- z_steps: int = Field(gt=0)
82
- _set_stub_offsets: bool = PrivateAttr(default_factory=lambda: False)
57
+ class HyperionSpecifiedThreeDGridScan(SpecifiedThreeDGridScan, WithHyperionUDCFeatures):
58
+ """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"""
59
+
60
+ # These detector params only exist so that we can properly select enable_dev_shm. Remove in
61
+ # https://github.com/DiamondLightSource/hyperion/issues/1395"""
62
+ @property
63
+ def detector_params(self):
64
+ self.det_dist_to_beam_converter_path = (
65
+ self.det_dist_to_beam_converter_path
66
+ or CONST.PARAM.DETECTOR.BEAM_XY_LUT_PATH
67
+ )
68
+ optional_args = {}
69
+ if self.run_number:
70
+ optional_args["run_number"] = self.run_number
71
+ assert self.detector_distance_mm is not None, (
72
+ "Detector distance must be filled before generating DetectorParams"
73
+ )
74
+ return DetectorParams(
75
+ detector_size_constants=I03Constants.DETECTOR,
76
+ expected_energy_ev=self.demand_energy_ev,
77
+ exposure_time=self.exposure_time_s,
78
+ directory=self.storage_directory,
79
+ prefix=self.file_name,
80
+ detector_distance=self.detector_distance_mm,
81
+ omega_start=self.omega_start_deg or 0,
82
+ omega_increment=0,
83
+ num_images_per_trigger=1,
84
+ num_triggers=self.num_images,
85
+ use_roi_mode=self.use_roi_mode,
86
+ det_dist_to_beam_converter_path=self.det_dist_to_beam_converter_path,
87
+ trigger_mode=self.trigger_mode,
88
+ enable_dev_shm=self.features.compare_cpu_and_gpu_zocalo,
89
+ **optional_args,
90
+ )
83
91
 
92
+ # Relative to common grid scan, stub offsets are defined by config server
84
93
  @property
85
94
  def FGS_params(self) -> ZebraGridScanParams:
86
95
  return ZebraGridScanParams(
@@ -124,59 +133,17 @@ class HyperionThreeDGridScan(
124
133
  transmission_fraction=self.transmission_frac,
125
134
  )
126
135
 
127
- def do_set_stub_offsets(self, value: bool):
128
- self._set_stub_offsets = value
129
-
130
- @property
131
- def grid_1_spec(self):
132
- x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1)
133
- y1_end = self.y_start_um + self.y_step_size_um * (self.y_steps - 1)
134
- grid_1_x = Line("sam_x", self.x_start_um, x_end, self.x_steps)
135
- grid_1_y = Line("sam_y", self.y_start_um, y1_end, self.y_steps)
136
- grid_1_z = Static("sam_z", self.z_start_um)
137
- return grid_1_y.zip(grid_1_z) * ~grid_1_x
138
-
139
- @property
140
- def grid_2_spec(self):
141
- x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1)
142
- z2_end = self.z2_start_um + self.z_step_size_um * (self.z_steps - 1)
143
- grid_2_x = Line("sam_x", self.x_start_um, x_end, self.x_steps)
144
- grid_2_z = Line("sam_z", self.z2_start_um, z2_end, self.z_steps)
145
- grid_2_y = Static("sam_y", self.y2_start_um)
146
- return grid_2_z.zip(grid_2_y) * ~grid_2_x
147
-
148
- @property
149
- def scan_indices(self):
150
- """The first index of each gridscan, useful for writing nexus files/VDS"""
151
- return [
152
- 0,
153
- len(ScanPath(self.grid_1_spec.calculate()).consume().midpoints["sam_x"]),
154
- ]
155
-
156
- @property
157
- def scan_spec(self):
158
- """A fully specified ScanSpec object representing both grids, with x, y, z and
159
- omega positions."""
160
- return self.grid_1_spec.concat(self.grid_2_spec)
161
136
 
162
- @property
163
- def scan_points(self):
164
- """A list of all the points in the scan_spec."""
165
- return ScanPath(self.scan_spec.calculate()).consume().midpoints
166
-
167
- @property
168
- def scan_points_first_grid(self):
169
- """A list of all the points in the first grid scan."""
170
- return ScanPath(self.grid_1_spec.calculate()).consume().midpoints
137
+ class OddYStepsException(Exception): ...
171
138
 
172
- @property
173
- def scan_points_second_grid(self):
174
- """A list of all the points in the second grid scan."""
175
- return ScanPath(self.grid_2_spec.calculate()).consume().midpoints
176
139
 
177
- @property
178
- def num_images(self) -> int:
179
- return len(self.scan_points["sam_x"])
140
+ class PinTipCentreThenXrayCentre(
141
+ GridCommonWithHyperionDetectorParams, WithHyperionUDCFeatures
142
+ ):
143
+ tip_offset_um: float = 0
180
144
 
181
145
 
182
- class OddYStepsException(Exception): ...
146
+ class GridScanWithEdgeDetect(
147
+ GridCommonWithHyperionDetectorParams, WithHyperionUDCFeatures
148
+ ):
149
+ box_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)
@@ -1,4 +1,4 @@
1
- from typing import TypeVar
1
+ from typing import Self, TypeVar
2
2
 
3
3
  from pydantic import BaseModel, model_validator
4
4
 
@@ -8,7 +8,8 @@ from mx_bluesky.common.parameters.components import (
8
8
  WithSample,
9
9
  WithVisit,
10
10
  )
11
- from mx_bluesky.common.parameters.gridscan import (
11
+ from mx_bluesky.hyperion.parameters.components import WithHyperionUDCFeatures
12
+ from mx_bluesky.hyperion.parameters.robot_load import (
12
13
  RobotLoadThenCentre,
13
14
  )
14
15
  from mx_bluesky.hyperion.parameters.rotation import MultiRotationScan
@@ -23,7 +24,11 @@ def construct_from_values(parent_context: dict, child_dict: dict, t: type[T]) ->
23
24
 
24
25
 
25
26
  class LoadCentreCollect(
26
- MxBlueskyParameters, WithVisit, WithSample, WithCentreSelection
27
+ MxBlueskyParameters,
28
+ WithVisit,
29
+ WithSample,
30
+ WithCentreSelection,
31
+ WithHyperionUDCFeatures,
27
32
  ):
28
33
  """Experiment parameters to perform the combined robot load,
29
34
  pin-tip centre and rotation scan operations."""
@@ -39,10 +44,31 @@ class LoadCentreCollect(
39
44
  | RobotLoadThenCentre.model_fields.keys()
40
45
  | MultiRotationScan.model_fields.keys()
41
46
  )
47
+
42
48
  disallowed_keys = values.keys() - allowed_keys
43
- assert (
44
- disallowed_keys == set()
45
- ), f"Unexpected fields found in LoadCentreCollect {disallowed_keys}"
49
+ assert disallowed_keys == set(), (
50
+ f"Unexpected fields found in LoadCentreCollect {disallowed_keys}"
51
+ )
52
+
53
+ keys_from_outer_load_centre_collect = (
54
+ MxBlueskyParameters.model_fields.keys()
55
+ | WithSample.model_fields.keys()
56
+ | WithVisit.model_fields.keys()
57
+ )
58
+ duplicated_robot_load_then_centre_keys = (
59
+ keys_from_outer_load_centre_collect
60
+ & values["robot_load_then_centre"].keys()
61
+ )
62
+ assert not (duplicated_robot_load_then_centre_keys), (
63
+ f"Unexpected keys in robot_load_then_centre: {', '.join(duplicated_robot_load_then_centre_keys)}"
64
+ )
65
+
66
+ duplicated_multi_rotation_scan_keys = (
67
+ keys_from_outer_load_centre_collect & values["multi_rotation_scan"].keys()
68
+ )
69
+ assert not (duplicated_multi_rotation_scan_keys), (
70
+ f"Unexpected keys in multi_rotation_scan: {', '.join(duplicated_multi_rotation_scan_keys)}"
71
+ )
46
72
 
47
73
  new_robot_load_then_centre_params = construct_from_values(
48
74
  values, values["robot_load_then_centre"], RobotLoadThenCentre
@@ -53,3 +79,22 @@ class LoadCentreCollect(
53
79
  values["multi_rotation_scan"] = new_multi_rotation_scan_params
54
80
  values["robot_load_then_centre"] = new_robot_load_then_centre_params
55
81
  return values
82
+
83
+ @model_validator(mode="after")
84
+ def _check_rotation_start_xyz_is_not_specified(self) -> Self:
85
+ for scan in self.multi_rotation_scan.single_rotation_scans:
86
+ assert (
87
+ not scan.x_start_um and not scan.y_start_um and not scan.z_start_um
88
+ ), (
89
+ "Specifying start xyz for sweeps is not supported in combination with centring."
90
+ )
91
+ return self
92
+
93
+ @model_validator(mode="after")
94
+ def _check_different_gridscan_and_rotation_energy_not_specified(self) -> Self:
95
+ assert (
96
+ self.multi_rotation_scan.demand_energy_ev is None
97
+ or self.multi_rotation_scan.demand_energy_ev
98
+ == self.robot_load_then_centre.demand_energy_ev
99
+ ), "Setting a different energy for gridscan and rotation is not supported."
100
+ return self
@@ -0,0 +1,40 @@
1
+ from pydantic import Field
2
+
3
+ from mx_bluesky.common.parameters.components import (
4
+ MxBlueskyParameters,
5
+ WithOptionalEnergyChange,
6
+ WithSample,
7
+ WithSnapshot,
8
+ WithVisit,
9
+ )
10
+ from mx_bluesky.common.parameters.constants import (
11
+ HardwareConstants,
12
+ )
13
+ from mx_bluesky.hyperion.parameters.components import WithHyperionUDCFeatures
14
+ from mx_bluesky.hyperion.parameters.gridscan import (
15
+ GridCommonWithHyperionDetectorParams,
16
+ PinTipCentreThenXrayCentre,
17
+ )
18
+
19
+
20
+ class RobotLoadAndEnergyChange(
21
+ MxBlueskyParameters, WithSample, WithSnapshot, WithOptionalEnergyChange, WithVisit
22
+ ):
23
+ thawing_time: float = Field(default=HardwareConstants.THAWING_TIME)
24
+
25
+
26
+ class RobotLoadThenCentre(
27
+ GridCommonWithHyperionDetectorParams, WithHyperionUDCFeatures
28
+ ):
29
+ thawing_time: float = Field(default=HardwareConstants.THAWING_TIME)
30
+
31
+ @property
32
+ def robot_load_params(self) -> RobotLoadAndEnergyChange:
33
+ my_params = self.model_dump()
34
+ return RobotLoadAndEnergyChange(**my_params)
35
+
36
+ @property
37
+ def pin_centre_then_xray_centre_params(self) -> PinTipCentreThenXrayCentre:
38
+ my_params = self.model_dump()
39
+ del my_params["thawing_time"]
40
+ return PinTipCentreThenXrayCentre(**my_params)
@@ -3,12 +3,12 @@ from __future__ import annotations
3
3
  import os
4
4
  from collections.abc import Iterator
5
5
  from itertools import accumulate
6
- from typing import Annotated, Any
6
+ from typing import Annotated, Any, Self
7
7
 
8
8
  from annotated_types import Len
9
9
  from dodal.devices.aperturescatterguard import ApertureValue
10
10
  from dodal.devices.detector import DetectorParams
11
- from dodal.devices.zebra import (
11
+ from dodal.devices.zebra.zebra import (
12
12
  RotationDirection,
13
13
  )
14
14
  from dodal.log import LOGGER
@@ -26,6 +26,7 @@ from mx_bluesky.common.parameters.components import (
26
26
  SplitScan,
27
27
  WithScan,
28
28
  )
29
+ from mx_bluesky.hyperion.parameters.components import WithHyperionUDCFeatures
29
30
  from mx_bluesky.hyperion.parameters.constants import (
30
31
  CONST,
31
32
  I03Constants,
@@ -33,6 +34,18 @@ from mx_bluesky.hyperion.parameters.constants import (
33
34
 
34
35
 
35
36
  class RotationScanPerSweep(OptionalGonioAngleStarts, OptionalXyzStarts):
37
+ """
38
+ Describes a rotation scan about the specified axis.
39
+
40
+ Attributes:
41
+ rotation_axis: The rotation axis, by default this is the omega axis
42
+ omega_start_deg: The initial angle of the rotation in degrees (default 0)
43
+ scan_width_deg: The sweep of the rotation in degrees, this must be positive (default 360)
44
+ rotation_direction: Indicates the direction of rotation, if RotationDirection.POSITIVE
45
+ the final angle is obtained by adding scan_width_deg, otherwise by subtraction (default NEGATIVE)
46
+ nexus_vds_start_img: The frame number of the first frame captured during the rotation
47
+ """
48
+
36
49
  omega_start_deg: float = Field(default=0) # type: ignore
37
50
  rotation_axis: RotationAxis = Field(default=RotationAxis.OMEGA)
38
51
  scan_width_deg: float = Field(default=360, gt=0)
@@ -40,7 +53,7 @@ class RotationScanPerSweep(OptionalGonioAngleStarts, OptionalXyzStarts):
40
53
  nexus_vds_start_img: int = Field(default=0, ge=0)
41
54
 
42
55
 
43
- class RotationExperiment(DiffractionExperimentWithSample):
56
+ class RotationExperiment(DiffractionExperimentWithSample, WithHyperionUDCFeatures):
44
57
  shutter_opening_time_s: float = Field(default=CONST.I03.SHUTTER_TIME_S)
45
58
  rotation_increment_deg: float = Field(default=0.1, gt=0)
46
59
  ispyb_experiment_type: IspybExperimentType = Field(
@@ -98,6 +111,7 @@ class RotationScan(WithScan, RotationScanPerSweep, RotationExperiment):
98
111
 
99
112
  @property
100
113
  def scan_points(self) -> AxesPoints:
114
+ """The scan points are defined in application space"""
101
115
  scan_spec = Line(
102
116
  axis="omega",
103
117
  start=self.omega_start_deg,
@@ -141,6 +155,17 @@ class MultiRotationScan(RotationExperiment, SplitScan):
141
155
  start_img += scan.scan_width_deg / values.rotation_increment_deg
142
156
  return values
143
157
 
158
+ @model_validator(mode="after")
159
+ def _check_valid_for_single_arm_multiple_sweep(self) -> Self:
160
+ if len(self.rotation_scans) > 0:
161
+ scan_width = self.rotation_scans[0].scan_width_deg
162
+ for scan in self.rotation_scans[1:]:
163
+ assert scan.scan_width_deg == scan_width, (
164
+ "Sweeps with different numbers of frames are not supported."
165
+ )
166
+
167
+ return self
168
+
144
169
  @property
145
170
  def single_rotation_scans(self) -> Iterator[RotationScan]:
146
171
  for scan in self.rotation_scans:
@@ -6,7 +6,7 @@ from blueapi.core.bluesky_types import Device
6
6
  from dodal.utils import get_beamline_based_on_environment_variable
7
7
 
8
8
  import mx_bluesky.hyperion.experiment_plans as hyperion_plans
9
- from mx_bluesky.hyperion.log import LOGGER
9
+ from mx_bluesky.common.utils.log import LOGGER
10
10
 
11
11
  T = TypeVar("T", bound=Device)
12
12
 
@@ -62,7 +62,7 @@ def fake_rotation_scan(
62
62
  @bpp.run_decorator( # attach experiment metadata to the start document
63
63
  md={
64
64
  "subplan_name": CONST.PLAN.ROTATION_OUTER,
65
- "hyperion_parameters": parameters.model_dump_json(),
65
+ "mx_bluesky_parameters": parameters.model_dump_json(),
66
66
  "activate_callbacks": "RotationNexusFileCallback",
67
67
  }
68
68
  )
@@ -79,6 +79,7 @@ def fake_rotation_scan(
79
79
 
80
80
 
81
81
  def fake_create_rotation_devices():
82
+ beamstop = i03.beamstop(fake_with_ophyd_sim=True)
82
83
  eiger = i03.eiger(fake_with_ophyd_sim=True)
83
84
  smargon = i03.smargon(fake_with_ophyd_sim=True)
84
85
  zebra = i03.zebra(fake_with_ophyd_sim=True)
@@ -106,6 +107,7 @@ def fake_create_rotation_devices():
106
107
  return RotationScanComposite(
107
108
  attenuator=attenuator,
108
109
  backlight=backlight,
110
+ beamstop=beamstop,
109
111
  dcm=dcm,
110
112
  detector_motion=detector_motion,
111
113
  eiger=eiger,
@@ -135,7 +137,7 @@ def sim_rotation_scan_to_create_nexus(
135
137
  fake_create_rotation_devices.eiger.bit_depth.sim_put(32) # type: ignore
136
138
 
137
139
  with patch(
138
- "mx_bluesky.hyperion.external_interaction.nexus.write_nexus.get_start_and_predicted_end_time",
140
+ "mx_bluesky.common.external_interaction.nexus.write_nexus.get_start_and_predicted_end_time",
139
141
  return_value=("test_time", "test_time"),
140
142
  ):
141
143
  RE(
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: mx-bluesky
3
- Version: 1.4.2
3
+ Version: 1.4.3
4
4
  Summary: Bluesky tools for MX Beamlines at DLS
5
5
  Author-email: Dominic Oram <dominic.oram@diamond.ac.uk>
6
6
  License: Apache License
@@ -226,7 +226,7 @@ Requires-Dist: opencv-python
226
226
  Requires-Dist: opentelemetry-distro
227
227
  Requires-Dist: opentelemetry-exporter-otlp
228
228
  Requires-Dist: pydantic
229
- Requires-Dist: pydantic-extra-types
229
+ Requires-Dist: pydantic-extra-types>=2.10.1
230
230
  Requires-Dist: pyepics
231
231
  Requires-Dist: pyzmq
232
232
  Requires-Dist: requests
@@ -238,8 +238,8 @@ Requires-Dist: blueapi>=0.5.0
238
238
  Requires-Dist: daq-config-server>=0.1.1
239
239
  Requires-Dist: ophyd==1.9.0
240
240
  Requires-Dist: ophyd-async>=0.8a5
241
- Requires-Dist: bluesky>=1.13.0a4
242
- Requires-Dist: dls-dodal==1.36.3
241
+ Requires-Dist: bluesky>=1.13
242
+ Requires-Dist: dls-dodal==1.38.0
243
243
  Provides-Extra: dev
244
244
  Requires-Dist: black; extra == "dev"
245
245
  Requires-Dist: build; extra == "dev"
@@ -250,6 +250,7 @@ Requires-Dist: ipython; extra == "dev"
250
250
  Requires-Dist: mypy; extra == "dev"
251
251
  Requires-Dist: myst-parser; extra == "dev"
252
252
  Requires-Dist: pipdeptree; extra == "dev"
253
+ Requires-Dist: plantweb; extra == "dev"
253
254
  Requires-Dist: pre-commit; extra == "dev"
254
255
  Requires-Dist: pydata-sphinx-theme>=0.12; extra == "dev"
255
256
  Requires-Dist: pyright; extra == "dev"
@@ -260,7 +261,6 @@ Requires-Dist: pytest; extra == "dev"
260
261
  Requires-Dist: ruff; extra == "dev"
261
262
  Requires-Dist: sphinx-autobuild; extra == "dev"
262
263
  Requires-Dist: sphinx-copybutton; extra == "dev"
263
- Requires-Dist: sphinxcontrib-plantuml; extra == "dev"
264
264
  Requires-Dist: sphinx-design; extra == "dev"
265
265
  Requires-Dist: tox-direct; extra == "dev"
266
266
  Requires-Dist: tox; extra == "dev"