mx-bluesky 1.4.0__py3-none-any.whl → 1.4.1__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 (78) hide show
  1. mx_bluesky/_version.py +2 -2
  2. mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +178 -0
  3. mx_bluesky/beamlines/i04/thawing_plan.py +1 -1
  4. mx_bluesky/beamlines/i24/serial/dcid.py +143 -171
  5. mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +1 -1
  6. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +54 -21
  7. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +2 -5
  8. mx_bluesky/beamlines/i24/serial/fixed_target/ft_utils.py +0 -1
  9. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +67 -50
  10. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +26 -79
  11. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +0 -199
  12. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +4 -6
  13. mx_bluesky/beamlines/i24/serial/log.py +1 -1
  14. mx_bluesky/beamlines/i24/serial/parameters/__init__.py +4 -0
  15. mx_bluesky/beamlines/i24/serial/parameters/constants.py +6 -1
  16. mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py +42 -15
  17. mx_bluesky/beamlines/i24/serial/run_fixed_target.sh +4 -3
  18. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +2 -0
  19. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +103 -81
  20. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +1 -2
  21. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_zebra_plans.py +24 -26
  22. mx_bluesky/beamlines/i24/serial/write_nexus.py +74 -72
  23. mx_bluesky/common/external_interaction/config_server.py +46 -0
  24. mx_bluesky/common/parameters/components.py +52 -15
  25. mx_bluesky/common/parameters/constants.py +11 -1
  26. mx_bluesky/common/parameters/gridscan.py +94 -0
  27. mx_bluesky/{hyperion → common}/parameters/robot_load.py +2 -2
  28. mx_bluesky/common/plans/do_fgs.py +2 -2
  29. mx_bluesky/common/utils/log.py +2 -0
  30. mx_bluesky/hyperion/__main__.py +2 -1
  31. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +21 -31
  32. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +4 -4
  33. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +1 -1
  34. mx_bluesky/hyperion/device_setup_plans/smargon.py +3 -3
  35. mx_bluesky/hyperion/exceptions.py +13 -1
  36. mx_bluesky/hyperion/experiment_plans/__init__.py +4 -0
  37. mx_bluesky/hyperion/experiment_plans/change_aperture_then_move_plan.py +83 -0
  38. mx_bluesky/hyperion/experiment_plans/common/xrc_result.py +47 -0
  39. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +9 -9
  40. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +133 -97
  41. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +42 -18
  42. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +75 -9
  43. mx_bluesky/hyperion/experiment_plans/oav_grid_detection_plan.py +2 -2
  44. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +1 -1
  45. mx_bluesky/hyperion/experiment_plans/optimise_attenuation_plan.py +2 -2
  46. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +36 -17
  47. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +5 -5
  48. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +28 -28
  49. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +64 -16
  50. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +11 -3
  51. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +10 -10
  52. mx_bluesky/hyperion/external_interaction/callbacks/__init__.py +0 -4
  53. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +4 -0
  54. mx_bluesky/hyperion/external_interaction/callbacks/common/abstract_event.py +66 -0
  55. mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py +5 -0
  56. mx_bluesky/hyperion/external_interaction/callbacks/grid_detection_callback.py +15 -15
  57. mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +18 -10
  58. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +3 -1
  59. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +5 -3
  60. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/__init__.py +0 -0
  61. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/sample_handling_callback.py +84 -0
  62. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +15 -9
  63. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py +5 -4
  64. mx_bluesky/hyperion/external_interaction/config_server.py +8 -37
  65. mx_bluesky/hyperion/external_interaction/exceptions.py +0 -9
  66. mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +65 -15
  67. mx_bluesky/hyperion/parameters/components.py +4 -9
  68. mx_bluesky/hyperion/parameters/constants.py +0 -1
  69. mx_bluesky/hyperion/parameters/gridscan.py +33 -76
  70. mx_bluesky/hyperion/parameters/load_centre_collect.py +14 -9
  71. mx_bluesky/hyperion/parameters/rotation.py +15 -6
  72. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1.dist-info}/METADATA +35 -34
  73. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1.dist-info}/RECORD +77 -70
  74. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1.dist-info}/WHEEL +1 -1
  75. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Mapping_py3v1.py +0 -150
  76. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1.dist-info}/LICENSE +0 -0
  77. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1.dist-info}/entry_points.txt +0 -0
  78. {mx_bluesky-1.4.0.dist-info → mx_bluesky-1.4.1.dist-info}/top_level.txt +0 -0
@@ -12,7 +12,7 @@ from mx_bluesky.hyperion.external_interaction.nexus.nexus_utils import (
12
12
  from mx_bluesky.hyperion.external_interaction.nexus.write_nexus import NexusWriter
13
13
  from mx_bluesky.hyperion.log import NEXUS_LOGGER
14
14
  from mx_bluesky.hyperion.parameters.constants import CONST
15
- from mx_bluesky.hyperion.parameters.gridscan import ThreeDGridScan
15
+ from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from event_model.documents import Event, EventDescriptor, RunStart
@@ -45,11 +45,12 @@ class GridscanNexusFileCallback(PlanReactiveCallback):
45
45
 
46
46
  def activity_gated_start(self, doc: RunStart):
47
47
  if doc.get("subplan_name") == CONST.PLAN.GRIDSCAN_OUTER:
48
- json_params = doc.get("hyperion_parameters")
48
+ hyperion_params = doc.get("hyperion_parameters")
49
+ assert isinstance(hyperion_params, str)
49
50
  NEXUS_LOGGER.info(
50
- f"Nexus writer received start document with experiment parameters {json_params}"
51
+ f"Nexus writer received start document with experiment parameters {hyperion_params}"
51
52
  )
52
- parameters = ThreeDGridScan.from_json(json_params)
53
+ parameters = HyperionThreeDGridScan.model_validate_json(hyperion_params)
53
54
  d_size = parameters.detector_params.detector_size_constants.det_size_pixels
54
55
  grid_n_img_1 = parameters.scan_indices[1]
55
56
  grid_n_img_2 = parameters.num_images - grid_n_img_1
@@ -1,47 +1,18 @@
1
+ from functools import cache
2
+
1
3
  from daq_config_server.client import ConfigServer
2
- from pydantic import BaseModel, Field, model_validator
3
4
 
5
+ from mx_bluesky.common.external_interaction.config_server import FeatureFlags
4
6
  from mx_bluesky.hyperion.log import LOGGER
5
7
  from mx_bluesky.hyperion.parameters.constants import CONST
6
8
 
7
- _CONFIG_SERVER: ConfigServer | None = None
8
-
9
9
 
10
- def config_server() -> ConfigServer:
11
- global _CONFIG_SERVER
12
- if _CONFIG_SERVER is None:
13
- _CONFIG_SERVER = ConfigServer(CONST.CONFIG_SERVER_URL, LOGGER)
14
- return _CONFIG_SERVER
10
+ class HyperionFeatureFlags(FeatureFlags):
11
+ @staticmethod
12
+ @cache
13
+ def get_config_server() -> ConfigServer:
14
+ return ConfigServer(CONST.CONFIG_SERVER_URL, LOGGER)
15
15
 
16
-
17
- class FeatureFlags(BaseModel):
18
- # The default value will be used as the fallback when doing a best-effort fetch
19
- # from the service
20
16
  use_panda_for_gridscan: bool = CONST.I03.USE_PANDA_FOR_GRIDSCAN
21
17
  compare_cpu_and_gpu_zocalo: bool = CONST.I03.COMPARE_CPU_AND_GPU_ZOCALO
22
18
  set_stub_offsets: bool = CONST.I03.SET_STUB_OFFSETS
23
-
24
- # Feature values supplied at construction will override values from the config server
25
- overriden_features: dict = Field(default_factory=dict, exclude=True)
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
- @classmethod
35
- def _get_flags(cls):
36
- flags = config_server().best_effort_get_all_feature_flags()
37
- return {f: flags[f] for f in flags if f in cls.model_fields.keys()}
38
-
39
- def update_self_from_server(self):
40
- """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."""
41
- for flag, value in self._get_flags().items():
42
- updated_value = (
43
- value
44
- if flag not in self.overriden_features.keys()
45
- else self.overriden_features[flag]
46
- )
47
- setattr(self, flag, updated_value)
@@ -1,13 +1,4 @@
1
- from mx_bluesky.hyperion.exceptions import WarningException
2
-
3
-
4
1
  class ISPyBDepositionNotMade(Exception):
5
2
  """Raised when the ISPyB or Zocalo callbacks can't access ISPyB deposition numbers."""
6
3
 
7
4
  pass
8
-
9
-
10
- class NoCentreFoundException(WarningException):
11
- """Error for if zocalo is unable to find the centre during a gridscan."""
12
-
13
- pass
@@ -1,4 +1,6 @@
1
1
  import configparser
2
+ from dataclasses import dataclass
3
+ from enum import StrEnum
2
4
 
3
5
  from requests import patch, post
4
6
  from requests.auth import AuthBase
@@ -29,20 +31,44 @@ def _get_base_url_and_token() -> tuple[str, str]:
29
31
  return expeye_config["url"], expeye_config["token"]
30
32
 
31
33
 
34
+ def _send_and_get_response(auth, url, data, send_func) -> dict:
35
+ response = send_func(url, auth=auth, json=data)
36
+ if not response.ok:
37
+ raise ISPyBDepositionNotMade(f"Could not write {data} to {url}: {response}")
38
+ return response.json()
39
+
40
+
41
+ @dataclass
42
+ class BLSample:
43
+ container_id: int
44
+ bl_sample_id: int
45
+ bl_sample_status: str | None
46
+
47
+
48
+ class BLSampleStatus(StrEnum):
49
+ # The sample has been loaded
50
+ LOADED = "LOADED"
51
+ # Problem with the sample e.g. pin too long/short
52
+ ERROR_SAMPLE = "ERROR - sample"
53
+ # Any other general error
54
+ ERROR_BEAMLINE = "ERROR - beamline"
55
+
56
+
57
+ assert all(
58
+ len(value) <= 20 for value in BLSampleStatus
59
+ ), "Column size limit of 20 for BLSampleStatus"
60
+
61
+
32
62
  class ExpeyeInteraction:
63
+ """Exposes functionality from the Expeye core API"""
64
+
33
65
  CREATE_ROBOT_ACTION = "/proposals/{proposal}/sessions/{visit_number}/robot-actions"
34
66
  UPDATE_ROBOT_ACTION = "/robot-actions/{action_id}"
35
67
 
36
68
  def __init__(self) -> None:
37
69
  url, token = _get_base_url_and_token()
38
- self.base_url = url + "/core"
39
- self.auth = BearerAuth(token)
40
-
41
- def _send_and_get_response(self, url, data, send_func) -> dict:
42
- response = send_func(url, auth=self.auth, json=data)
43
- if not response.ok:
44
- raise ISPyBDepositionNotMade(f"Could not write {data} to {url}: {response}")
45
- return response.json()
70
+ self._base_url = url
71
+ self._auth = BearerAuth(token)
46
72
 
47
73
  def start_load(
48
74
  self,
@@ -66,7 +92,7 @@ class ExpeyeInteraction:
66
92
  Returns:
67
93
  RobotActionID: The id of the robot load action that is created
68
94
  """
69
- url = self.base_url + self.CREATE_ROBOT_ACTION.format(
95
+ url = self._base_url + self.CREATE_ROBOT_ACTION.format(
70
96
  proposal=proposal_reference, visit_number=visit_number
71
97
  )
72
98
 
@@ -77,7 +103,7 @@ class ExpeyeInteraction:
77
103
  "containerLocation": container_location,
78
104
  "dewarLocation": dewar_location,
79
105
  }
80
- response = self._send_and_get_response(url, data, post)
106
+ response = _send_and_get_response(self._auth, url, data, post)
81
107
  return response["robotActionId"]
82
108
 
83
109
  def update_barcode_and_snapshots(
@@ -95,14 +121,14 @@ class ExpeyeInteraction:
95
121
  snapshot_before_path (str): Path to the snapshot before robot load
96
122
  snapshot_after_path (str): Path to the snapshot after robot load
97
123
  """
98
- url = self.base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id)
124
+ url = self._base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id)
99
125
 
100
126
  data = {
101
127
  "sampleBarcode": barcode,
102
128
  "xtalSnapshotBefore": snapshot_before_path,
103
129
  "xtalSnapshotAfter": snapshot_after_path,
104
130
  }
105
- self._send_and_get_response(url, data, patch)
131
+ _send_and_get_response(self._auth, url, data, patch)
106
132
 
107
133
  def end_load(self, action_id: RobotActionID, status: str, reason: str):
108
134
  """Finish an existing robot action, providing final information about how it went
@@ -113,13 +139,37 @@ class ExpeyeInteraction:
113
139
  otherwise error
114
140
  reason (str): If the status is in error than the reason for that error
115
141
  """
116
- url = self.base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id)
142
+ url = self._base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id)
117
143
 
118
144
  run_status = "SUCCESS" if status == "success" else "ERROR"
119
145
 
120
146
  data = {
121
147
  "endTimestamp": get_current_time_string(),
122
148
  "status": run_status,
123
- "message": reason,
149
+ "message": reason[:255] if reason else "",
124
150
  }
125
- self._send_and_get_response(url, data, patch)
151
+ _send_and_get_response(self._auth, url, data, patch)
152
+
153
+ def update_sample_status(
154
+ self, bl_sample_id: int, bl_sample_status: BLSampleStatus
155
+ ) -> BLSample:
156
+ """Update the blSampleStatus of a sample.
157
+ Args:
158
+ bl_sample_id: The sample ID
159
+ bl_sample_status: The sample status
160
+ status_message: An optional message
161
+ Returns:
162
+ The updated sample
163
+ """
164
+ data = {"blSampleStatus": (str(bl_sample_status))}
165
+ response = _send_and_get_response(
166
+ self._auth, self._base_url + f"/samples/{bl_sample_id}", data, patch
167
+ )
168
+ return self._sample_from_json(response)
169
+
170
+ def _sample_from_json(self, response) -> BLSample:
171
+ return BLSample(
172
+ bl_sample_id=response["blSampleId"],
173
+ bl_sample_status=response["blSampleStatus"],
174
+ container_id=response["containerId"],
175
+ )
@@ -1,12 +1,7 @@
1
- from __future__ import annotations
1
+ from pydantic import BaseModel, Field
2
2
 
3
- from pydantic import (
4
- BaseModel,
5
- Field,
6
- )
3
+ from mx_bluesky.hyperion.external_interaction.config_server import HyperionFeatureFlags
7
4
 
8
- from mx_bluesky.hyperion.external_interaction.config_server import FeatureFlags
9
5
 
10
-
11
- class WithFeatures(BaseModel):
12
- features: FeatureFlags = Field(default=FeatureFlags())
6
+ class WithHyperionFeatures(BaseModel):
7
+ features: HyperionFeatureFlags = Field(default=HyperionFeatureFlags())
@@ -27,7 +27,6 @@ class I03Constants:
27
27
  OAV_CENTRING_FILE = OavConstants.OAV_CONFIG_JSON
28
28
  SHUTTER_TIME_S = 0.06
29
29
  USE_PANDA_FOR_GRIDSCAN = False
30
- THAWING_TIME = 20
31
30
  SET_STUB_OFFSETS = False
32
31
 
33
32
  # Turns on GPU processing for zocalo and logs a comparison between GPU and CPU-
@@ -1,8 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
4
-
5
- from dodal.devices.aperturescatterguard import ApertureValue
6
3
  from dodal.devices.detector import (
7
4
  DetectorParams,
8
5
  )
@@ -15,38 +12,21 @@ from scanspec.core import Path as ScanPath
15
12
  from scanspec.specs import Line, Static
16
13
 
17
14
  from mx_bluesky.common.parameters.components import (
18
- DiffractionExperimentWithSample,
19
- IspybExperimentType,
20
- OptionalGonioAngleStarts,
21
15
  SplitScan,
22
16
  WithOptionalEnergyChange,
23
- WithScan,
24
- XyzStarts,
17
+ WithPandaGridScan,
18
+ )
19
+ from mx_bluesky.common.parameters.gridscan import (
20
+ GridCommon,
21
+ SpecifiedGrid,
25
22
  )
26
- from mx_bluesky.common.parameters.constants import GridscanParamConstants
27
- from mx_bluesky.hyperion.parameters.components import WithFeatures
23
+ from mx_bluesky.hyperion.parameters.components import WithHyperionFeatures
28
24
  from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants
29
- from mx_bluesky.hyperion.parameters.robot_load import RobotLoadAndEnergyChange
30
25
 
31
26
 
32
- # This will be restructed once Once https://github.com/DiamondLightSource/mx-bluesky/issues/323#issue-2500957290 is further along
33
- # to handle slightly different parameters between different beamline implementations
34
- class GridCommon(
35
- DiffractionExperimentWithSample,
36
- OptionalGonioAngleStarts,
37
- WithFeatures,
38
- ):
39
- grid_width_um: float = Field(default=CONST.PARAM.GRIDSCAN.WIDTH_UM)
40
- exposure_time_s: float = Field(default=CONST.PARAM.GRIDSCAN.EXPOSURE_TIME_S)
41
- use_roi_mode: bool = Field(default=CONST.PARAM.GRIDSCAN.USE_ROI)
42
- panda_runup_distance_mm: float = Field(
43
- default=GridscanParamConstants.PANDA_RUN_UP_DISTANCE_MM
44
- )
45
-
46
- ispyb_experiment_type: IspybExperimentType = Field(
47
- default=IspybExperimentType.GRIDSCAN_3D
48
- )
49
- selected_aperture: ApertureValue | None = Field(default=ApertureValue.SMALL)
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"""
50
30
 
51
31
  @property
52
32
  def detector_params(self):
@@ -60,7 +40,6 @@ class GridCommon(
60
40
  assert (
61
41
  self.detector_distance_mm is not None
62
42
  ), "Detector distance must be filled before generating DetectorParams"
63
- os.makedirs(self.storage_directory, exist_ok=True)
64
43
  return DetectorParams(
65
44
  detector_size_constants=I03Constants.DETECTOR,
66
45
  expected_energy_ev=self.demand_energy_ev,
@@ -80,36 +59,13 @@ class GridCommon(
80
59
  )
81
60
 
82
61
 
83
- class GridScanWithEdgeDetect(GridCommon):
84
- box_size_um: float = Field(default=CONST.PARAM.GRIDSCAN.BOX_WIDTH_UM)
85
-
86
-
87
- class PinTipCentreThenXrayCentre(GridCommon):
88
- tip_offset_um: float = 0
89
-
90
-
91
- class RobotLoadThenCentre(GridCommon):
92
- thawing_time: float = Field(default=CONST.I03.THAWING_TIME)
93
-
94
- def robot_load_params(self):
95
- my_params = self.model_dump()
96
- return RobotLoadAndEnergyChange(**my_params)
97
-
98
- def pin_centre_then_xray_centre_params(self):
99
- my_params = self.model_dump()
100
- del my_params["thawing_time"]
101
- return PinTipCentreThenXrayCentre(**my_params)
102
-
103
-
104
- class SpecifiedGridScan(GridCommon, XyzStarts, WithScan):
105
- """A specified grid scan is one which has defined values for the start position,
106
- grid and box sizes, etc., as opposed to parameters for a plan which will create
107
- those parameters at some point (e.g. through optical pin detection)."""
108
-
109
- ...
110
-
111
-
112
- class ThreeDGridScan(SpecifiedGridScan, SplitScan, WithOptionalEnergyChange):
62
+ class HyperionThreeDGridScan(
63
+ HyperionGridCommon,
64
+ SpecifiedGrid,
65
+ SplitScan,
66
+ WithOptionalEnergyChange,
67
+ WithPandaGridScan,
68
+ ):
113
69
  """Parameters representing a so-called 3D grid scan, which consists of doing a
114
70
  gridscan in X and Y, followed by one in X and Z."""
115
71
 
@@ -131,14 +87,14 @@ class ThreeDGridScan(SpecifiedGridScan, SplitScan, WithOptionalEnergyChange):
131
87
  x_steps=self.x_steps,
132
88
  y_steps=self.y_steps,
133
89
  z_steps=self.z_steps,
134
- x_step_size=self.x_step_size_um,
135
- y_step_size=self.y_step_size_um,
136
- z_step_size=self.z_step_size_um,
137
- x_start=self.x_start_um,
138
- y1_start=self.y_start_um,
139
- z1_start=self.z_start_um,
140
- y2_start=self.y2_start_um,
141
- z2_start=self.z2_start_um,
90
+ x_step_size_mm=self.x_step_size_um / 1000,
91
+ y_step_size_mm=self.y_step_size_um / 1000,
92
+ z_step_size_mm=self.z_step_size_um / 1000,
93
+ x_start_mm=self.x_start_um / 1000,
94
+ y1_start_mm=self.y_start_um / 1000,
95
+ z1_start_mm=self.z_start_um / 1000,
96
+ y2_start_mm=self.y2_start_um / 1000,
97
+ z2_start_mm=self.z2_start_um / 1000,
142
98
  set_stub_offsets=self.features.set_stub_offsets,
143
99
  dwell_time_ms=self.exposure_time_s * 1000,
144
100
  transmission_fraction=self.transmission_frac,
@@ -147,6 +103,7 @@ class ThreeDGridScan(SpecifiedGridScan, SplitScan, WithOptionalEnergyChange):
147
103
  @property
148
104
  def panda_FGS_params(self) -> PandAGridScanParams:
149
105
  if self.y_steps % 2 and self.z_steps > 0:
106
+ # See https://github.com/DiamondLightSource/hyperion/issues/1118 for explanation
150
107
  raise OddYStepsException(
151
108
  "The number of Y steps must be even for a PandA gridscan"
152
109
  )
@@ -154,14 +111,14 @@ class ThreeDGridScan(SpecifiedGridScan, SplitScan, WithOptionalEnergyChange):
154
111
  x_steps=self.x_steps,
155
112
  y_steps=self.y_steps,
156
113
  z_steps=self.z_steps,
157
- x_step_size=self.x_step_size_um,
158
- y_step_size=self.y_step_size_um,
159
- z_step_size=self.z_step_size_um,
160
- x_start=self.x_start_um,
161
- y1_start=self.y_start_um,
162
- z1_start=self.z_start_um,
163
- y2_start=self.y2_start_um,
164
- z2_start=self.z2_start_um,
114
+ x_step_size_mm=self.x_step_size_um / 1000,
115
+ y_step_size_mm=self.y_step_size_um / 1000,
116
+ z_step_size_mm=self.z_step_size_um / 1000,
117
+ x_start_mm=self.x_start_um / 1000,
118
+ y1_start_mm=self.y_start_um / 1000,
119
+ z1_start_mm=self.z_start_um / 1000,
120
+ y2_start_mm=self.y2_start_um / 1000,
121
+ z2_start_mm=self.z2_start_um / 1000,
165
122
  set_stub_offsets=self.features.set_stub_offsets,
166
123
  run_up_distance_mm=self.panda_runup_distance_mm,
167
124
  transmission_fraction=self.transmission_frac,
@@ -4,10 +4,11 @@ from pydantic import BaseModel, model_validator
4
4
 
5
5
  from mx_bluesky.common.parameters.components import (
6
6
  MxBlueskyParameters,
7
+ WithCentreSelection,
7
8
  WithSample,
8
9
  WithVisit,
9
10
  )
10
- from mx_bluesky.hyperion.parameters.gridscan import (
11
+ from mx_bluesky.common.parameters.gridscan import (
11
12
  RobotLoadThenCentre,
12
13
  )
13
14
  from mx_bluesky.hyperion.parameters.rotation import MultiRotationScan
@@ -15,13 +16,15 @@ from mx_bluesky.hyperion.parameters.rotation import MultiRotationScan
15
16
  T = TypeVar("T", bound=BaseModel)
16
17
 
17
18
 
18
- def construct_from_values(parent_context: dict, key: str, t: type[T]) -> T:
19
- values = dict(parent_context)
20
- values |= values[key]
19
+ def construct_from_values(parent_context: dict, child_dict: dict, t: type[T]) -> T:
20
+ values = {k: v for k, v in parent_context.items() if not isinstance(v, dict)}
21
+ values |= child_dict
21
22
  return t(**values)
22
23
 
23
24
 
24
- class LoadCentreCollect(MxBlueskyParameters, WithVisit, WithSample):
25
+ class LoadCentreCollect(
26
+ MxBlueskyParameters, WithVisit, WithSample, WithCentreSelection
27
+ ):
25
28
  """Experiment parameters to perform the combined robot load,
26
29
  pin-tip centre and rotation scan operations."""
27
30
 
@@ -41,10 +44,12 @@ class LoadCentreCollect(MxBlueskyParameters, WithVisit, WithSample):
41
44
  disallowed_keys == set()
42
45
  ), f"Unexpected fields found in LoadCentreCollect {disallowed_keys}"
43
46
 
44
- values["robot_load_then_centre"] = construct_from_values(
45
- values, "robot_load_then_centre", RobotLoadThenCentre
47
+ new_robot_load_then_centre_params = construct_from_values(
48
+ values, values["robot_load_then_centre"], RobotLoadThenCentre
46
49
  )
47
- values["multi_rotation_scan"] = construct_from_values(
48
- values, "multi_rotation_scan", MultiRotationScan
50
+ new_multi_rotation_scan_params = construct_from_values(
51
+ values, values["multi_rotation_scan"], MultiRotationScan
49
52
  )
53
+ values["multi_rotation_scan"] = new_multi_rotation_scan_params
54
+ values["robot_load_then_centre"] = new_robot_load_then_centre_params
50
55
  return values
@@ -47,7 +47,9 @@ class RotationExperiment(DiffractionExperimentWithSample):
47
47
  default=IspybExperimentType.ROTATION
48
48
  )
49
49
 
50
- def _detector_params(self, omega_start_deg: float):
50
+ def _detector_params_impl(
51
+ self, omega_start_deg: float, num_images_per_trigger: int, num_triggers: int
52
+ ) -> DetectorParams:
51
53
  self.det_dist_to_beam_converter_path = (
52
54
  self.det_dist_to_beam_converter_path
53
55
  or CONST.PARAM.DETECTOR.BEAM_XY_LUT_PATH
@@ -66,13 +68,16 @@ class RotationExperiment(DiffractionExperimentWithSample):
66
68
  detector_distance=self.detector_distance_mm,
67
69
  omega_start=omega_start_deg,
68
70
  omega_increment=self.rotation_increment_deg,
69
- num_images_per_trigger=self.num_images,
70
- num_triggers=1,
71
+ num_images_per_trigger=num_images_per_trigger,
72
+ num_triggers=num_triggers,
71
73
  use_roi_mode=False,
72
74
  det_dist_to_beam_converter_path=self.det_dist_to_beam_converter_path,
73
75
  **optional_args,
74
76
  )
75
77
 
78
+ def _detector_params(self, omega_start_deg: float) -> DetectorParams:
79
+ return self._detector_params_impl(omega_start_deg, self.num_images, 1)
80
+
76
81
  @field_validator("selected_aperture")
77
82
  @classmethod
78
83
  def _set_default_aperture_position(cls, aperture_position: ApertureValue | None):
@@ -115,7 +120,7 @@ class MultiRotationScan(RotationExperiment, SplitScan):
115
120
 
116
121
  def _single_rotation_scan(self, scan: RotationScanPerSweep) -> RotationScan:
117
122
  # self has everything from RotationExperiment
118
- allowed_keys = RotationScan.model_fields.keys()
123
+ allowed_keys = RotationScan.model_fields.keys() # type: ignore # mypy doesn't recognise this as a property...
119
124
  params_dump = self.model_dump()
120
125
  # provided `scan` has everything from RotationScanPerSweep
121
126
  scan_dump = scan.model_dump()
@@ -156,8 +161,12 @@ class MultiRotationScan(RotationExperiment, SplitScan):
156
161
  return list(accumulate([0, *self._num_images_per_scan()]))
157
162
 
158
163
  @property
159
- def detector_params(self):
160
- return self._detector_params(self.rotation_scans[0].omega_start_deg)
164
+ def detector_params(self) -> DetectorParams:
165
+ return self._detector_params_impl(
166
+ self.rotation_scans[0].omega_start_deg,
167
+ self._num_images_per_scan()[0],
168
+ len(self._num_images_per_scan()),
169
+ )
161
170
 
162
171
  @property
163
172
  def ispyb_params(self): # pyright: ignore
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mx-bluesky
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: Bluesky tools for MX Beamlines at DLS
5
5
  Author-email: Dominic Oram <dominic.oram@diamond.ac.uk>
6
- License: Apache License
6
+ License: Apache License
7
7
  Version 2.0, January 2004
8
8
  http://www.apache.org/licenses/
9
9
 
@@ -213,7 +213,7 @@ Classifier: Programming Language :: Python :: 3.12
213
213
  Requires-Python: >=3.11
214
214
  Description-Content-Type: text/x-rst
215
215
  License-File: LICENSE
216
- Requires-Dist: annotated-types
216
+ Requires-Dist: annotated_types
217
217
  Requires-Dist: caproto
218
218
  Requires-Dist: fastapi[all]
219
219
  Requires-Dist: flask-restful
@@ -233,38 +233,39 @@ Requires-Dist: requests
233
233
  Requires-Dist: scanspec
234
234
  Requires-Dist: scipy
235
235
  Requires-Dist: semver
236
- Requires-Dist: blueapi >=0.5.0
237
- Requires-Dist: daq-config-server >=0.1.1
238
- Requires-Dist: ophyd ==1.9.0
239
- Requires-Dist: ophyd-async >=0.3a5
240
- Requires-Dist: bluesky >=1.13.0a4
241
- Requires-Dist: dls-dodal ==1.35.0
236
+ Requires-Dist: matplotlib
237
+ Requires-Dist: blueapi>=0.5.0
238
+ Requires-Dist: daq-config-server>=0.1.1
239
+ Requires-Dist: ophyd==1.9.0
240
+ Requires-Dist: ophyd-async>=0.8a5
241
+ Requires-Dist: bluesky>=1.13.0a4
242
+ Requires-Dist: dls-dodal==1.36.2
242
243
  Provides-Extra: dev
243
- Requires-Dist: black ; extra == 'dev'
244
- Requires-Dist: build ; extra == 'dev'
245
- Requires-Dist: diff-cover ; extra == 'dev'
246
- Requires-Dist: GitPython ; extra == 'dev'
247
- Requires-Dist: import-linter ; extra == 'dev'
248
- Requires-Dist: ipython ; extra == 'dev'
249
- Requires-Dist: mypy ; extra == 'dev'
250
- Requires-Dist: myst-parser ; extra == 'dev'
251
- Requires-Dist: pipdeptree ; extra == 'dev'
252
- Requires-Dist: pre-commit ; extra == 'dev'
253
- Requires-Dist: pydata-sphinx-theme >=0.12 ; extra == 'dev'
254
- Requires-Dist: pyright ; extra == 'dev'
255
- Requires-Dist: pytest-asyncio ; extra == 'dev'
256
- Requires-Dist: pytest-cov ; extra == 'dev'
257
- Requires-Dist: pytest-random-order ; extra == 'dev'
258
- Requires-Dist: pytest ; extra == 'dev'
259
- Requires-Dist: ruff ; extra == 'dev'
260
- Requires-Dist: sphinx-autobuild ; extra == 'dev'
261
- Requires-Dist: sphinx-copybutton ; extra == 'dev'
262
- Requires-Dist: sphinxcontrib-plantuml ; extra == 'dev'
263
- Requires-Dist: sphinx-design ; extra == 'dev'
264
- Requires-Dist: tox-direct ; extra == 'dev'
265
- Requires-Dist: tox ; extra == 'dev'
266
- Requires-Dist: types-mock ; extra == 'dev'
267
- Requires-Dist: types-requests ; extra == 'dev'
244
+ Requires-Dist: black; extra == "dev"
245
+ Requires-Dist: build; extra == "dev"
246
+ Requires-Dist: diff-cover; extra == "dev"
247
+ Requires-Dist: GitPython; extra == "dev"
248
+ Requires-Dist: import-linter; extra == "dev"
249
+ Requires-Dist: ipython; extra == "dev"
250
+ Requires-Dist: mypy; extra == "dev"
251
+ Requires-Dist: myst-parser; extra == "dev"
252
+ Requires-Dist: pipdeptree; extra == "dev"
253
+ Requires-Dist: pre-commit; extra == "dev"
254
+ Requires-Dist: pydata-sphinx-theme>=0.12; extra == "dev"
255
+ Requires-Dist: pyright; extra == "dev"
256
+ Requires-Dist: pytest-asyncio; extra == "dev"
257
+ Requires-Dist: pytest-cov; extra == "dev"
258
+ Requires-Dist: pytest-random-order; extra == "dev"
259
+ Requires-Dist: pytest; extra == "dev"
260
+ Requires-Dist: ruff; extra == "dev"
261
+ Requires-Dist: sphinx-autobuild; extra == "dev"
262
+ Requires-Dist: sphinx-copybutton; extra == "dev"
263
+ Requires-Dist: sphinxcontrib-plantuml; extra == "dev"
264
+ Requires-Dist: sphinx-design; extra == "dev"
265
+ Requires-Dist: tox-direct; extra == "dev"
266
+ Requires-Dist: tox; extra == "dev"
267
+ Requires-Dist: types-mock; extra == "dev"
268
+ Requires-Dist: types-requests; extra == "dev"
268
269
 
269
270
  mx-bluesky
270
271
  ===========================