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
@@ -11,18 +11,18 @@ from ispyb.sp.mxacquisition import MXAcquisition
11
11
  from ispyb.strictordereddict import StrictOrderedDict
12
12
  from pydantic import BaseModel
13
13
 
14
- from mx_bluesky.common.utils.tracing import TRACER
15
- from mx_bluesky.hyperion.external_interaction.ispyb.data_model import (
14
+ from mx_bluesky.common.external_interaction.ispyb.data_model import (
16
15
  DataCollectionGridInfo,
17
16
  DataCollectionGroupInfo,
18
17
  DataCollectionInfo,
19
18
  ScanDataInfo,
20
19
  )
21
- from mx_bluesky.hyperion.external_interaction.ispyb.ispyb_utils import (
20
+ from mx_bluesky.common.external_interaction.ispyb.ispyb_utils import (
22
21
  get_current_time_string,
23
22
  get_session_id_from_visit,
24
23
  )
25
- from mx_bluesky.hyperion.log import ISPYB_LOGGER
24
+ from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER
25
+ from mx_bluesky.common.utils.tracing import TRACER
26
26
 
27
27
  if TYPE_CHECKING:
28
28
  pass
@@ -62,12 +62,12 @@ class StoreInIspyb:
62
62
  ispyb_ids,
63
63
  scan_data_infos: Sequence[ScanDataInfo],
64
64
  ) -> IspybIds:
65
- assert (
66
- ispyb_ids.data_collection_group_id
67
- ), "Attempted to store scan data without a collection group"
68
- assert (
69
- ispyb_ids.data_collection_ids
70
- ), "Attempted to store scan data without a collection"
65
+ assert ispyb_ids.data_collection_group_id, (
66
+ "Attempted to store scan data without a collection group"
67
+ )
68
+ assert ispyb_ids.data_collection_ids, (
69
+ "Attempted to store scan data without a collection"
70
+ )
71
71
  return self._begin_or_update_deposition(ispyb_ids, None, scan_data_infos)
72
72
 
73
73
  def _begin_or_update_deposition(
@@ -87,7 +87,9 @@ class StoreInIspyb:
87
87
  )
88
88
  )
89
89
  else:
90
- assert ispyb_ids.data_collection_group_id, "Attempt to update data collection without a data collection group ID"
90
+ assert ispyb_ids.data_collection_group_id, (
91
+ "Attempt to update data collection without a data collection group ID"
92
+ )
91
93
 
92
94
  grid_ids = list(ispyb_ids.grid_ids)
93
95
  data_collection_ids_out = list(ispyb_ids.data_collection_ids)
@@ -116,15 +118,15 @@ class StoreInIspyb:
116
118
  return ispyb_ids
117
119
 
118
120
  def end_deposition(self, ispyb_ids: IspybIds, success: str, reason: str):
119
- assert (
120
- ispyb_ids.data_collection_ids
121
- ), "Can't end ISPyB deposition, data_collection IDs are missing"
122
- assert (
123
- ispyb_ids.data_collection_group_id is not None
124
- ), "Cannot end ISPyB deposition without data collection group ID"
121
+ assert ispyb_ids.data_collection_ids, (
122
+ "Can't end ISPyB deposition, data_collection IDs are missing"
123
+ )
124
+ assert ispyb_ids.data_collection_group_id is not None, (
125
+ "Cannot end ISPyB deposition without data collection group ID"
126
+ )
125
127
 
126
128
  for id_ in ispyb_ids.data_collection_ids:
127
- ISPYB_LOGGER.info(
129
+ ISPYB_ZOCALO_CALLBACK_LOGGER.info(
128
130
  f"End ispyb deposition with status '{success}' and reason '{reason}'."
129
131
  )
130
132
  if success == "fail" or success == "abort":
@@ -7,11 +7,11 @@ from ispyb import NoResult
7
7
  from ispyb.connector.mysqlsp.main import ISPyBMySQLSPConnector as Connector
8
8
  from ispyb.sp.core import Core
9
9
 
10
- from mx_bluesky.hyperion.parameters.constants import CONST
10
+ from mx_bluesky.common.parameters.constants import SimConstants
11
11
 
12
12
 
13
13
  def get_ispyb_config():
14
- return os.environ.get("ISPYB_CONFIG_PATH", CONST.SIM.ISPYB_CONFIG)
14
+ return os.environ.get("ISPYB_CONFIG_PATH", SimConstants.ISPYB_CONFIG)
15
15
 
16
16
 
17
17
  def get_session_id_from_visit(conn: Connector, visit: str):
@@ -2,16 +2,27 @@ from __future__ import annotations
2
2
 
3
3
  import time
4
4
  from datetime import UTC, datetime, timedelta
5
+ from enum import Enum
5
6
 
6
7
  import numpy as np
7
8
  from dodal.devices.detector import DetectorParams
8
- from dodal.devices.zebra import RotationDirection
9
9
  from nexgen.nxs_utils import Attenuator, Axis, Beam, Detector, EigerDetector, Goniometer
10
10
  from nexgen.nxs_utils.axes import TransformationType
11
11
  from numpy.typing import DTypeLike
12
12
 
13
- from mx_bluesky.hyperion.log import NEXUS_LOGGER
14
- from mx_bluesky.hyperion.utils.utils import convert_eV_to_angstrom
13
+ from mx_bluesky.common.utils.log import NEXUS_LOGGER
14
+ from mx_bluesky.common.utils.utils import convert_eV_to_angstrom
15
+
16
+
17
+ class AxisDirection(Enum):
18
+ """
19
+ Identifies whether the omega axis of rotation is on the positive x-axis or
20
+ negative x-axis as per the Nexus standard:
21
+ https://journals.iucr.org/m/issues/2020/05/00/ti5018/ti5018.pdf
22
+ """
23
+
24
+ POSITIVE = 1
25
+ NEGATIVE = -1
15
26
 
16
27
 
17
28
  def vds_type_based_on_bit_depth(detector_bit_depth: int) -> DTypeLike:
@@ -35,8 +46,8 @@ def create_goniometer_axes(
35
46
  x_y_z_increments: tuple[float, float, float] = (0.0, 0.0, 0.0),
36
47
  chi: float = 0.0,
37
48
  phi: float = 0.0,
38
- rotation_direction: RotationDirection = RotationDirection.NEGATIVE,
39
- ):
49
+ omega_axis_direction: AxisDirection = AxisDirection.NEGATIVE,
50
+ ) -> Goniometer:
40
51
  """Returns a Nexgen 'Goniometer' object with the dependency chain of I03's Smargon
41
52
  goniometer. If scan points is provided these values will be used in preference to
42
53
  those from the params object.
@@ -50,13 +61,17 @@ def create_goniometer_axes(
50
61
  x_y_z_increments: optionally, specify the increments between each image for
51
62
  the x, y, and z axes. Will be ignored if scan_points
52
63
  is provided.
64
+ omega_axis_direction: The direction of the omega axis, this determines the
65
+ coordinate space parity
66
+ Returns:
67
+ The created Goniometer object
53
68
  """
54
69
  gonio_axes = [
55
70
  Axis(
56
71
  "omega",
57
72
  ".",
58
73
  TransformationType.ROTATION,
59
- (1.0 * rotation_direction.multiplier, 0.0, 0.0),
74
+ (1.0 * omega_axis_direction.value, 0.0, 0.0),
60
75
  omega_start,
61
76
  ),
62
77
  Axis(
@@ -8,19 +8,19 @@ from __future__ import annotations
8
8
  import math
9
9
  from pathlib import Path
10
10
 
11
- from dodal.devices.zebra import RotationDirection
12
11
  from dodal.utils import get_beamline_name
13
12
  from nexgen.nxs_utils import Attenuator, Beam, Detector, Goniometer, Source
14
13
  from nexgen.nxs_write.nxmx_writer import NXmxFileWriter
15
14
  from numpy.typing import DTypeLike
16
15
  from scanspec.core import AxesPoints
17
16
 
18
- from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample
19
- from mx_bluesky.hyperion.external_interaction.nexus.nexus_utils import (
17
+ from mx_bluesky.common.external_interaction.nexus.nexus_utils import (
18
+ AxisDirection,
20
19
  create_detector_parameters,
21
20
  create_goniometer_axes,
22
21
  get_start_and_predicted_end_time,
23
22
  )
23
+ from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample
24
24
 
25
25
 
26
26
  class NexusWriter:
@@ -39,7 +39,7 @@ class NexusWriter:
39
39
  # detector arming event:
40
40
  full_num_of_images: int | None = None,
41
41
  meta_data_run_number: int | None = None,
42
- rotation_direction: RotationDirection = RotationDirection.NEGATIVE,
42
+ axis_direction: AxisDirection = AxisDirection.NEGATIVE,
43
43
  ) -> None:
44
44
  self.beam: Beam | None = None
45
45
  self.attenuator: Attenuator | None = None
@@ -69,7 +69,7 @@ class NexusWriter:
69
69
  self.scan_points,
70
70
  chi=chi_start_deg,
71
71
  phi=phi_start_deg,
72
- rotation_direction=rotation_direction,
72
+ omega_axis_direction=axis_direction,
73
73
  )
74
74
 
75
75
  def create_nexus_file(self, bit_depth: DTypeLike):
@@ -0,0 +1,38 @@
1
+ from functools import cache
2
+
3
+ import pytest
4
+
5
+ from mx_bluesky.common.external_interaction.config_server import FeatureFlags
6
+
7
+
8
+ class MockConfigServer:
9
+ def best_effort_get_all_feature_flags(self):
10
+ return {
11
+ "feature_a": False,
12
+ "feature_b": False,
13
+ }
14
+
15
+
16
+ class FakeFeatureFlags(FeatureFlags):
17
+ @staticmethod
18
+ @cache
19
+ def get_config_server() -> MockConfigServer: # type: ignore
20
+ return MockConfigServer()
21
+
22
+ feature_a: bool = False
23
+ feature_b: bool = False
24
+
25
+
26
+ @pytest.fixture
27
+ def fake_feature_flags():
28
+ return FakeFeatureFlags(feature_a=False, feature_b=False)
29
+
30
+
31
+ def test_valid_overridden_features(fake_feature_flags: FakeFeatureFlags):
32
+ assert fake_feature_flags.feature_a is False
33
+ assert fake_feature_flags.feature_b is False
34
+
35
+
36
+ def test_invalid_overridden_features():
37
+ with pytest.raises(ValueError, match="Invalid feature toggle"):
38
+ FakeFeatureFlags(feature_x=True) # type: ignore
@@ -29,7 +29,7 @@ from mx_bluesky.common.parameters.constants import (
29
29
  GridscanParamConstants,
30
30
  )
31
31
 
32
- PARAMETER_VERSION = Version.parse("5.2.0")
32
+ PARAMETER_VERSION = Version.parse("5.3.0")
33
33
 
34
34
 
35
35
  class RotationAxis(StrEnum):
@@ -83,6 +83,7 @@ class IspybExperimentType(StrEnum):
83
83
  STILL = "Still"
84
84
  SSX_CHIP = "SSX-Chip"
85
85
  SSX_JET = "SSX-Jet"
86
+ METAL_ID = "Metal ID"
86
87
 
87
88
  # Aliases for historic hyperion experiment type mapping
88
89
  ROTATION = "SAD"
@@ -104,12 +105,12 @@ class MxBlueskyParameters(BaseModel):
104
105
  @field_validator("parameter_model_version")
105
106
  @classmethod
106
107
  def _validate_version(cls, version: SemanticVersion):
107
- assert (
108
- version >= SemanticVersion(major=PARAMETER_VERSION.major)
109
- ), f"Parameter version too old! This version of hyperion uses {PARAMETER_VERSION}"
110
- assert (
111
- version <= SemanticVersion(major=PARAMETER_VERSION.major + 1)
112
- ), f"Parameter version too new! This version of hyperion uses {PARAMETER_VERSION}"
108
+ assert version >= SemanticVersion(major=PARAMETER_VERSION.major), (
109
+ f"Parameter version too old! This version of hyperion uses {PARAMETER_VERSION}"
110
+ )
111
+ assert version <= SemanticVersion(major=PARAMETER_VERSION.major + 1), (
112
+ f"Parameter version too new! This version of hyperion uses {PARAMETER_VERSION}"
113
+ )
113
114
  return version
114
115
 
115
116
 
@@ -150,6 +151,7 @@ class DiffractionExperiment(
150
151
  transmission_frac: float = Field(default=0.1)
151
152
  ispyb_experiment_type: IspybExperimentType
152
153
  storage_directory: str
154
+ use_roi_mode: bool = Field(default=GridscanParamConstants.USE_ROI)
153
155
 
154
156
  @model_validator(mode="before")
155
157
  @classmethod
@@ -73,6 +73,7 @@ class HardwareConstants:
73
73
  PANDA_FGS_RUN_UP_DEFAULT = 0.17
74
74
  CRYOJET_MARGIN_MM = 0.2
75
75
  THAWING_TIME = 20
76
+ TIP_OFFSET_UM = 0
76
77
 
77
78
 
78
79
  @dataclass(frozen=True)
@@ -1,94 +1,143 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
4
-
5
3
  from dodal.devices.aperturescatterguard import ApertureValue
6
- from dodal.devices.detector import (
7
- DetectorParams,
4
+ from dodal.devices.fast_grid_scan import (
5
+ ZebraGridScanParams,
8
6
  )
9
- from pydantic import Field
7
+ from pydantic import Field, PrivateAttr
8
+ from scanspec.core import Path as ScanPath
9
+ from scanspec.specs import Line, Static
10
10
 
11
11
  from mx_bluesky.common.parameters.components import (
12
12
  DiffractionExperimentWithSample,
13
13
  IspybExperimentType,
14
14
  OptionalGonioAngleStarts,
15
+ SplitScan,
16
+ WithOptionalEnergyChange,
15
17
  WithScan,
16
18
  XyzStarts,
17
19
  )
18
20
  from mx_bluesky.common.parameters.constants import (
19
- DetectorParamConstants,
20
21
  GridscanParamConstants,
21
22
  HardwareConstants,
22
23
  )
23
- from mx_bluesky.common.parameters.robot_load import RobotLoadAndEnergyChange
24
+
25
+ """Parameter models in this file are abstract. They should be inherited by a top-level model"""
24
26
 
25
27
 
26
28
  class GridCommon(
27
29
  DiffractionExperimentWithSample,
28
30
  OptionalGonioAngleStarts,
29
31
  ):
32
+ """Parameters used in every MX diffraction experiment using grids. This model should be used by plans which have no knowledge of the grid specifications - i.e before automatic grid detection has completed"""
33
+
30
34
  grid_width_um: float = Field(default=GridscanParamConstants.WIDTH_UM)
31
35
  exposure_time_s: float = Field(default=GridscanParamConstants.EXPOSURE_TIME_S)
32
- use_roi_mode: bool = Field(default=GridscanParamConstants.USE_ROI)
33
36
 
34
37
  ispyb_experiment_type: IspybExperimentType = Field(
35
38
  default=IspybExperimentType.GRIDSCAN_3D
36
39
  )
37
40
  selected_aperture: ApertureValue | None = Field(default=ApertureValue.SMALL)
38
41
 
42
+ tip_offset_um: float = Field(default=HardwareConstants.TIP_OFFSET_UM)
43
+
44
+
45
+ class SpecifiedGrid(XyzStarts, WithScan):
46
+ """A specified grid is one which has defined values for the start position,
47
+ grid and box sizes, etc., as opposed to parameters for a plan which will create
48
+ those parameters at some point (e.g. through optical pin detection)."""
49
+
50
+
51
+ class SpecifiedThreeDGridScan(
52
+ GridCommon,
53
+ SpecifiedGrid,
54
+ SplitScan,
55
+ WithOptionalEnergyChange,
56
+ ):
57
+ """Parameters representing a so-called 3D grid scan, which consists of doing a
58
+ gridscan in X and Y, followed by one in X and Z."""
59
+
60
+ grid1_omega_deg: float = Field(default=GridscanParamConstants.OMEGA_1) # type: ignore
61
+ grid2_omega_deg: float = Field(default=GridscanParamConstants.OMEGA_2)
62
+ x_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)
63
+ y_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)
64
+ z_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)
65
+ y2_start_um: float
66
+ z2_start_um: float
67
+ x_steps: int = Field(gt=0)
68
+ y_steps: int = Field(gt=0)
69
+ z_steps: int = Field(gt=0)
70
+ _set_stub_offsets: bool = PrivateAttr(default_factory=lambda: False)
71
+
39
72
  @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,
73
+ def FGS_params(self) -> ZebraGridScanParams:
74
+ return ZebraGridScanParams(
75
+ x_steps=self.x_steps,
76
+ y_steps=self.y_steps,
77
+ z_steps=self.z_steps,
78
+ x_step_size_mm=self.x_step_size_um / 1000,
79
+ y_step_size_mm=self.y_step_size_um / 1000,
80
+ z_step_size_mm=self.z_step_size_um / 1000,
81
+ x_start_mm=self.x_start_um / 1000,
82
+ y1_start_mm=self.y_start_um / 1000,
83
+ z1_start_mm=self.z_start_um / 1000,
84
+ y2_start_mm=self.y2_start_um / 1000,
85
+ z2_start_mm=self.z2_start_um / 1000,
86
+ set_stub_offsets=self._set_stub_offsets,
87
+ dwell_time_ms=self.exposure_time_s * 1000,
88
+ transmission_fraction=self.transmission_frac,
67
89
  )
68
90
 
91
+ def do_set_stub_offsets(self, value: bool):
92
+ self._set_stub_offsets = value
69
93
 
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)
94
+ @property
95
+ def grid_1_spec(self):
96
+ x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1)
97
+ y1_end = self.y_start_um + self.y_step_size_um * (self.y_steps - 1)
98
+ grid_1_x = Line("sam_x", self.x_start_um, x_end, self.x_steps)
99
+ grid_1_y = Line("sam_y", self.y_start_um, y1_end, self.y_steps)
100
+ grid_1_z = Static("sam_z", self.z_start_um)
101
+ return grid_1_y.zip(grid_1_z) * ~grid_1_x
76
102
 
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)
103
+ @property
104
+ def grid_2_spec(self):
105
+ x_end = self.x_start_um + self.x_step_size_um * (self.x_steps - 1)
106
+ z2_end = self.z2_start_um + self.z_step_size_um * (self.z_steps - 1)
107
+ grid_2_x = Line("sam_x", self.x_start_um, x_end, self.x_steps)
108
+ grid_2_z = Line("sam_z", self.z2_start_um, z2_end, self.z_steps)
109
+ grid_2_y = Static("sam_y", self.y2_start_um)
110
+ return grid_2_z.zip(grid_2_y) * ~grid_2_x
81
111
 
112
+ @property
113
+ def scan_indices(self):
114
+ """The first index of each gridscan, useful for writing nexus files/VDS"""
115
+ return [
116
+ 0,
117
+ len(ScanPath(self.grid_1_spec.calculate()).consume().midpoints["sam_x"]),
118
+ ]
82
119
 
83
- class GridScanWithEdgeDetect(GridCommon):
84
- box_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)
120
+ @property
121
+ def scan_spec(self):
122
+ """A fully specified ScanSpec object representing both grids, with x, y, z and
123
+ omega positions."""
124
+ return self.grid_1_spec.concat(self.grid_2_spec)
85
125
 
126
+ @property
127
+ def scan_points(self):
128
+ """A list of all the points in the scan_spec."""
129
+ return ScanPath(self.scan_spec.calculate()).consume().midpoints
86
130
 
87
- class PinTipCentreThenXrayCentre(GridCommon):
88
- tip_offset_um: float = 0
131
+ @property
132
+ def scan_points_first_grid(self):
133
+ """A list of all the points in the first grid scan."""
134
+ return ScanPath(self.grid_1_spec.calculate()).consume().midpoints
89
135
 
136
+ @property
137
+ def scan_points_second_grid(self):
138
+ """A list of all the points in the second grid scan."""
139
+ return ScanPath(self.grid_2_spec.calculate()).consume().midpoints
90
140
 
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)."""
141
+ @property
142
+ def num_images(self) -> int:
143
+ return len(self.scan_points["sam_x"])
@@ -59,7 +59,7 @@ def _wait_for_zocalo_to_stage_then_do_fgs(
59
59
  yield from bps.complete(grid_scan_device, wait=True)
60
60
  # Remove this logging statement once metrics have been added
61
61
  LOGGER.info(
62
- f"Grid scan motion program took {round(time()-gridscan_start_time,2)} to complete"
62
+ f"Grid scan motion program took {round(time() - gridscan_start_time, 2)} to complete"
63
63
  )
64
64
 
65
65
 
@@ -89,9 +89,9 @@ def kickoff_and_complete_gridscan(
89
89
  zocalo_environment (Optional, str) Used for zocalo connection
90
90
  """
91
91
 
92
- assert len(scan_points) == len(
93
- scan_start_indices
94
- ), "scan_points and scan_start_indices must be lists of the same length!"
92
+ assert len(scan_points) == len(scan_start_indices), (
93
+ "scan_points and scan_start_indices must be lists of the same length!"
94
+ )
95
95
 
96
96
  plan_name = PlanNameConstants.DO_FGS
97
97
 
@@ -1,3 +1,4 @@
1
+ import re
1
2
  from collections.abc import Callable, Generator
2
3
  from typing import TypeVar
3
4
 
@@ -13,10 +14,23 @@ class WarningException(Exception):
13
14
  pass
14
15
 
15
16
 
17
+ class ISPyBDepositionNotMade(Exception):
18
+ """Raised when the ISPyB or Zocalo callbacks can't access ISPyB deposition numbers."""
19
+
20
+ pass
21
+
22
+
16
23
  class SampleException(WarningException):
17
24
  """An exception which identifies an issue relating to the sample."""
18
25
 
19
- pass
26
+ def __str__(self):
27
+ class_name = type(self).__name__
28
+ return f"[{class_name}]: {super().__str__()}"
29
+
30
+ @classmethod
31
+ def type_and_message_from_reason(cls, reason: str) -> tuple[str, str]:
32
+ match = re.match(r"\[(\S*)?]: (.*)", reason)
33
+ return (match.group(1), match.group(2)) if match else (None, None)
20
34
 
21
35
 
22
36
  T = TypeVar("T")
@@ -12,7 +12,17 @@ from dodal.log import (
12
12
  )
13
13
  from dodal.log import LOGGER as dodal_logger
14
14
 
15
- LOGGER = logging.getLogger("mx-bluesky")
15
+ LOGGER = logging.getLogger("MX-Bluesky")
16
+ LOGGER.setLevel("DEBUG")
17
+ LOGGER.parent = dodal_logger
18
+
19
+ ISPYB_ZOCALO_CALLBACK_LOGGER = logging.getLogger("ISPyB and Zocalo callbacks")
20
+ ISPYB_ZOCALO_CALLBACK_LOGGER.setLevel(logging.DEBUG)
21
+
22
+ NEXUS_LOGGER = logging.getLogger("NeXus callbacks")
23
+ NEXUS_LOGGER.setLevel(logging.DEBUG)
24
+
25
+ ALL_LOGGERS = [LOGGER, ISPYB_ZOCALO_CALLBACK_LOGGER, NEXUS_LOGGER]
16
26
 
17
27
  __logger_handlers: DodalLogHandlers | None = None
18
28
 
@@ -76,18 +86,18 @@ def do_default_logging_setup(
76
86
 
77
87
 
78
88
  def _get_debug_handler() -> CircularMemoryHandler:
79
- assert (
80
- __logger_handlers is not None
81
- ), "You can only use this after running the default logging setup"
89
+ assert __logger_handlers is not None, (
90
+ "You can only use this after running the default logging setup"
91
+ )
82
92
  return __logger_handlers["debug_memory_handler"]
83
93
 
84
94
 
85
95
  def flush_debug_handler() -> str:
86
96
  """Writes the contents of the circular debug log buffer to disk and returns the written filename"""
87
97
  handler = _get_debug_handler()
88
- assert isinstance(
89
- handler.target, TimedRotatingFileHandler
90
- ), "Circular memory handler doesn't have an appropriate fileHandler target"
98
+ assert isinstance(handler.target, TimedRotatingFileHandler), (
99
+ "Circular memory handler doesn't have an appropriate fileHandler target"
100
+ )
91
101
  handler.flush()
92
102
  return handler.target.baseFilename
93
103
 
@@ -15,11 +15,24 @@ from flask import Flask, request
15
15
  from flask_restful import Api, Resource
16
16
  from pydantic.dataclasses import dataclass
17
17
 
18
+ from mx_bluesky.common.external_interaction.callbacks.common.aperture_change_callback import (
19
+ ApertureChangeCallback,
20
+ )
21
+ from mx_bluesky.common.external_interaction.callbacks.common.log_uid_tag_callback import (
22
+ LogUidTaggingCallback,
23
+ )
24
+ from mx_bluesky.common.external_interaction.callbacks.common.logging_callback import (
25
+ VerbosePlanExecutionLoggingCallback,
26
+ )
18
27
  from mx_bluesky.common.parameters.components import MxBlueskyParameters
19
28
  from mx_bluesky.common.parameters.constants import Actions, Status
20
- from mx_bluesky.common.utils.log import do_default_logging_setup, flush_debug_handler
29
+ from mx_bluesky.common.utils.exceptions import WarningException
30
+ from mx_bluesky.common.utils.log import (
31
+ LOGGER,
32
+ do_default_logging_setup,
33
+ flush_debug_handler,
34
+ )
21
35
  from mx_bluesky.common.utils.tracing import TRACER
22
- from mx_bluesky.hyperion.exceptions import WarningException
23
36
  from mx_bluesky.hyperion.experiment_plans.experiment_registry import (
24
37
  PLAN_REGISTRY,
25
38
  PlanNotFound,
@@ -27,21 +40,9 @@ from mx_bluesky.hyperion.experiment_plans.experiment_registry import (
27
40
  from mx_bluesky.hyperion.external_interaction.callbacks.__main__ import (
28
41
  setup_logging as setup_callback_logging,
29
42
  )
30
- from mx_bluesky.hyperion.external_interaction.callbacks.aperture_change_callback import (
31
- ApertureChangeCallback,
32
- )
33
43
  from mx_bluesky.hyperion.external_interaction.callbacks.common.callback_util import (
34
44
  CallbacksFactory,
35
45
  )
36
- from mx_bluesky.hyperion.external_interaction.callbacks.log_uid_tag_callback import (
37
- LogUidTaggingCallback,
38
- )
39
- from mx_bluesky.hyperion.external_interaction.callbacks.logging_callback import (
40
- VerbosePlanExecutionLoggingCallback,
41
- )
42
- from mx_bluesky.hyperion.log import (
43
- LOGGER,
44
- )
45
46
  from mx_bluesky.hyperion.parameters.cli import parse_cli_args
46
47
  from mx_bluesky.hyperion.parameters.constants import CONST
47
48
  from mx_bluesky.hyperion.utils.context import setup_context
@@ -0,0 +1,27 @@
1
+ import bluesky.plan_stubs as bps
2
+ from dodal.devices.i03.beamstop import Beamstop, BeamstopPositions
3
+
4
+ from mx_bluesky.common.utils.log import LOGGER
5
+
6
+
7
+ class BeamstopException(Exception):
8
+ pass
9
+
10
+
11
+ def check_beamstop(beamstop: Beamstop):
12
+ """
13
+ Check the current position of the beamstop to ensure it is in position for data collection
14
+ Args:
15
+ beamstop: The beamstop device
16
+
17
+ Raises:
18
+ BeamstopException: If the beamstop is in any other position than DATA_COLLECTION
19
+ """
20
+ current_pos = yield from bps.rd(beamstop.selected_pos)
21
+ if current_pos != BeamstopPositions.DATA_COLLECTION:
22
+ LOGGER.info(f"Beamstop check failed: position {current_pos}")
23
+ raise BeamstopException(
24
+ f"Beamstop is not DATA_COLLECTION, current state is {current_pos}"
25
+ )
26
+
27
+ LOGGER.info("Beamstop check ok")