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
@@ -1,105 +1,107 @@
1
- import logging
2
1
  import os
3
2
  import pathlib
4
3
  import pprint
5
4
  import time
6
5
  from datetime import datetime
7
- from typing import Literal
8
6
 
9
7
  import requests
10
8
 
11
9
  from mx_bluesky.beamlines.i24.serial.fixed_target.ft_utils import ChipType, MappingType
10
+ from mx_bluesky.beamlines.i24.serial.log import SSX_LOGGER
12
11
  from mx_bluesky.beamlines.i24.serial.parameters import (
13
12
  ExtruderParameters,
14
13
  FixedTargetParameters,
15
14
  )
16
- from mx_bluesky.beamlines.i24.serial.setup_beamline import Eiger, caget, cagetstring
17
-
18
- logger = logging.getLogger("I24ssx.nexus_writer")
15
+ from mx_bluesky.beamlines.i24.serial.setup_beamline import Eiger, caget
19
16
 
20
17
 
21
18
  def call_nexgen(
22
19
  chip_prog_dict: dict | None,
23
- start_time: datetime,
24
20
  parameters: ExtruderParameters | FixedTargetParameters,
25
- wavelength: float,
26
- expt_type: Literal["fixed-target", "extruder"] = "fixed-target",
21
+ wavelength_in_a: float,
22
+ beam_center_in_pix: tuple[float, float],
23
+ start_time: datetime | None = None,
27
24
  ):
28
- det_type = parameters.detector_name
29
- print(f"det_type: {det_type}")
25
+ """Call the nexus writer by sending a request to nexgen-server.
26
+
27
+ Args:
28
+ chip_prog_dict (dict | None): Dictionary containing most of the information \
29
+ passed to the program runner for the collection. Only used for fixed target.
30
+ start_time
31
+ parameters (SerialAndLaserExperiment): Collection parameters.
32
+ wavelength_in_a (float): Wavelength, in A.
33
+ beam_center_in_pix (list[float]): Beam center position on detector, in pixels.
34
+ start_time (datetime, optional): Collection start time.
35
+
36
+ Raises:
37
+ ValueError: For a wrong experiment type passed (either unknwon or not matched \
38
+ to parameter model).
30
39
 
40
+ """
31
41
  current_chip_map = None
32
- if expt_type == "fixed-target" and isinstance(parameters, FixedTargetParameters):
33
- if not (
34
- parameters.map_type == MappingType.NoMap
35
- or parameters.chip.chip_type == ChipType.Custom
36
- ):
37
- # NOTE Nexgen server is still on nexgen v0.7.2 (fully working for ssx)
38
- # Will need to be updated, for correctness sake map needs to be None.
39
- current_chip_map = "/dls_sw/i24/scripts/fastchips/litemaps/currentchip.map"
40
- pump_status = bool(parameters.pump_repeat)
41
- total_numb_imgs = parameters.total_num_images
42
- elif expt_type == "extruder" and isinstance(parameters, ExtruderParameters):
43
- # chip_prog_dict should be None for extruder (passed as input for now)
44
- total_numb_imgs = parameters.num_images
45
- pump_status = parameters.pump_status
46
- else:
47
- raise ValueError(f"{expt_type=} not recognised")
42
+ match parameters:
43
+ case FixedTargetParameters():
44
+ if not (
45
+ parameters.map_type == MappingType.NoMap
46
+ or parameters.chip.chip_type == ChipType.Custom
47
+ ):
48
+ # NOTE Nexgen server is still on nexgen v0.7.2 (fully working for ssx)
49
+ # Will need to be updated, for correctness sake map needs to be None.
50
+ current_chip_map = (
51
+ "/dls_sw/i24/scripts/fastchips/litemaps/currentchip.map"
52
+ )
53
+ pump_status = bool(parameters.pump_repeat)
54
+ total_numb_imgs = parameters.total_num_images
55
+ case ExtruderParameters():
56
+ total_numb_imgs = parameters.num_images
57
+ pump_status = parameters.pump_status
48
58
 
49
- filename_prefix = cagetstring(Eiger.pv.filenameRBV)
50
- meta_h5 = (
51
- pathlib.Path(parameters.visit)
52
- / parameters.directory
53
- / f"{filename_prefix}_meta.h5"
54
- )
59
+ filename_prefix = parameters.filename
60
+ meta_h5 = parameters.visit / parameters.directory / f"{filename_prefix}_meta.h5"
55
61
  t0 = time.time()
56
62
  max_wait = 60 # seconds
57
- logger.info(f"Watching for {meta_h5}")
63
+ SSX_LOGGER.info(f"Watching for {meta_h5}")
58
64
  while time.time() - t0 < max_wait:
59
65
  if meta_h5.exists():
60
- logger.info(f"Found {meta_h5} after {time.time() - t0:.1f} seconds")
66
+ SSX_LOGGER.info(f"Found {meta_h5} after {time.time() - t0:.1f} seconds")
61
67
  time.sleep(5)
62
68
  break
63
- logger.debug(f"Waiting for {meta_h5}")
69
+ SSX_LOGGER.debug(f"Waiting for {meta_h5}")
64
70
  time.sleep(1)
65
71
  if not meta_h5.exists():
66
- logger.warning(f"Giving up waiting for {meta_h5} after {max_wait} seconds")
67
- return False
68
-
69
- transmission = (float(caget(Eiger.pv.transmission)),)
72
+ SSX_LOGGER.warning(f"Giving up waiting for {meta_h5} after {max_wait} seconds")
73
+ return
70
74
 
71
- if det_type == Eiger.name:
72
- bit_depth = int(caget(Eiger.pv.bit_depth))
73
- logger.debug(
74
- f"Call to nexgen server with the following chip definition: \n{chip_prog_dict}"
75
- )
75
+ bit_depth = int(caget(Eiger.pv.bit_depth))
76
+ SSX_LOGGER.debug(
77
+ f"Call to nexgen server with the following chip definition: \n{chip_prog_dict}"
78
+ )
76
79
 
77
- access_token = pathlib.Path("/scratch/ssx_nexgen.key").read_text().strip()
78
- url = "https://ssx-nexgen.diamond.ac.uk/ssx_eiger/write"
79
- headers = {"Authorization": f"Bearer {access_token}"}
80
+ access_token = pathlib.Path("/scratch/ssx_nexgen.key").read_text().strip()
81
+ url = "https://ssx-nexgen.diamond.ac.uk/ssx_eiger/write"
82
+ headers = {"Authorization": f"Bearer {access_token}"}
80
83
 
81
- payload = {
82
- "beamline": "i24",
83
- "beam_center": [caget(Eiger.pv.beamx), caget(Eiger.pv.beamy)],
84
- "chipmap": current_chip_map,
85
- "chip_info": chip_prog_dict,
86
- "det_dist": parameters.detector_distance_mm,
87
- "exp_time": parameters.exposure_time_s,
88
- "expt_type": expt_type,
89
- "filename": filename_prefix,
90
- "num_imgs": total_numb_imgs,
91
- "pump_status": pump_status,
92
- "pump_exp": parameters.laser_dwell_s,
93
- "pump_delay": parameters.laser_delay_s,
94
- "transmission": transmission[0],
95
- "visitpath": os.fspath(meta_h5.parent),
96
- "wavelength": wavelength,
97
- "bit_depth": bit_depth,
98
- }
99
- logger.info(f"Sending POST request to {url} with payload:")
100
- logger.info(pprint.pformat(payload))
101
- response = requests.post(url, headers=headers, json=payload)
102
- logger.info(f"Response: {response.text} (status code: {response.status_code})")
103
- # the following will raise an error if the request was unsuccessful
104
- return response.status_code == requests.codes.ok
105
- return False
84
+ payload = {
85
+ "beamline": "i24",
86
+ "beam_center": beam_center_in_pix,
87
+ "chipmap": current_chip_map,
88
+ "chip_info": chip_prog_dict,
89
+ "det_dist": parameters.detector_distance_mm,
90
+ "exp_time": parameters.exposure_time_s,
91
+ "expt_type": parameters.nexgen_experiment_type,
92
+ "filename": filename_prefix,
93
+ "num_imgs": total_numb_imgs,
94
+ "pump_status": pump_status,
95
+ "pump_exp": parameters.laser_dwell_s,
96
+ "pump_delay": parameters.laser_delay_s,
97
+ "transmission": parameters.transmission,
98
+ "visitpath": os.fspath(meta_h5.parent),
99
+ "wavelength": wavelength_in_a,
100
+ "bit_depth": bit_depth,
101
+ "start_time": start_time,
102
+ }
103
+ SSX_LOGGER.info(f"Sending POST request to {url} with payload:")
104
+ SSX_LOGGER.info(pprint.pformat(payload))
105
+ response = requests.post(url, headers=headers, json=payload)
106
+ response.raise_for_status()
107
+ SSX_LOGGER.info(f"Response: {response.text} (status code: {response.status_code})")
@@ -0,0 +1,46 @@
1
+ from abc import ABC, abstractmethod
2
+ from functools import cache
3
+
4
+ from daq_config_server.client import ConfigServer
5
+ from pydantic import BaseModel, Field, model_validator
6
+
7
+
8
+ class FeatureFlags(BaseModel, ABC):
9
+ """Abstract class to use ConfigServer to toggle features for an experiment
10
+
11
+ A module wanting to use FeatureFlags should inherit this class, add boolean features
12
+ as attributes, and implement a get_config_server method, which returns a cached creation of
13
+ ConfigServer. See HyperionFeatureFlags for an example
14
+
15
+ Values supplied upon class instantiation will always take priority over the config server. If connection to the server cannot
16
+ be made AND values were not supplied, attributes will use their default values
17
+ """
18
+
19
+ # Feature values supplied at construction will override values from the config server
20
+ overriden_features: dict = Field(default_factory=dict, exclude=True)
21
+
22
+ @staticmethod
23
+ @cache
24
+ @abstractmethod
25
+ def get_config_server() -> ConfigServer: ...
26
+
27
+ @model_validator(mode="before")
28
+ @classmethod
29
+ def mark_overridden_features(cls, values):
30
+ assert isinstance(values, dict)
31
+ values["overriden_features"] = values.copy()
32
+ return values
33
+
34
+ def _get_flags(self):
35
+ flags = type(self).get_config_server().best_effort_get_all_feature_flags()
36
+ return {f: flags[f] for f in flags if f in self.model_fields.keys()}
37
+
38
+ def update_self_from_server(self):
39
+ """Used to update the feature flags from the server during a plan. Where there are flags which were explicitly set from externally supplied parameters, these values will be used instead."""
40
+ for flag, value in self._get_flags().items():
41
+ updated_value = (
42
+ value
43
+ if flag not in self.overriden_features.keys()
44
+ else self.overriden_features[flag]
45
+ )
46
+ setattr(self, flag, updated_value)
@@ -1,21 +1,33 @@
1
- import json
1
+ from __future__ import annotations
2
+
3
+ import os
2
4
  from abc import abstractmethod
3
5
  from collections.abc import Sequence
4
6
  from enum import StrEnum
5
7
  from pathlib import Path
6
- from typing import SupportsInt
8
+ from typing import Literal, SupportsInt, cast
7
9
 
8
10
  from dodal.devices.aperturescatterguard import ApertureValue
9
11
  from dodal.devices.detector import (
10
12
  DetectorParams,
11
13
  TriggerMode,
12
14
  )
13
- from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
15
+ from pydantic import (
16
+ BaseModel,
17
+ ConfigDict,
18
+ Field,
19
+ field_validator,
20
+ model_validator,
21
+ )
14
22
  from pydantic_extra_types.semantic_version import SemanticVersion
15
23
  from scanspec.core import AxesPoints
16
24
  from semver import Version
17
25
 
18
- from mx_bluesky.common.parameters.constants import TEST_MODE, DetectorParamConstants
26
+ from mx_bluesky.common.parameters.constants import (
27
+ TEST_MODE,
28
+ DetectorParamConstants,
29
+ GridscanParamConstants,
30
+ )
19
31
 
20
32
  PARAMETER_VERSION = Version.parse("5.2.0")
21
33
 
@@ -100,11 +112,6 @@ class MxBlueskyParameters(BaseModel):
100
112
  ), f"Parameter version too new! This version of hyperion uses {PARAMETER_VERSION}"
101
113
  return version
102
114
 
103
- @classmethod
104
- def from_json(cls, input: str | None):
105
- assert input is not None
106
- return cls(**json.loads(input))
107
-
108
115
 
109
116
  class WithSnapshot(BaseModel):
110
117
  snapshot_directory: Path
@@ -146,12 +153,12 @@ class DiffractionExperiment(
146
153
 
147
154
  @model_validator(mode="before")
148
155
  @classmethod
149
- def validate_snapshot_directory(cls, values):
150
- snapshot_dir = values.get(
151
- "snapshot_directory", Path(values["storage_directory"], "snapshots")
152
- )
153
- values["snapshot_directory"] = (
154
- snapshot_dir if isinstance(snapshot_dir, Path) else Path(snapshot_dir)
156
+ def validate_directories(cls, values):
157
+ os.makedirs(values["storage_directory"], exist_ok=True)
158
+
159
+ values["snapshot_directory"] = values.get(
160
+ "snapshot_directory",
161
+ Path(values["storage_directory"], "snapshots").as_posix(),
155
162
  )
156
163
  return values
157
164
 
@@ -176,6 +183,14 @@ class WithScan(BaseModel):
176
183
  def num_images(self) -> int: ...
177
184
 
178
185
 
186
+ class WithPandaGridScan(BaseModel):
187
+ """For experiments which use a PandA for constant-motion grid scans"""
188
+
189
+ panda_runup_distance_mm: float = Field(
190
+ default=GridscanParamConstants.PANDA_RUN_UP_DISTANCE_MM
191
+ )
192
+
193
+
179
194
  class SplitScan(BaseModel):
180
195
  @property
181
196
  @abstractmethod
@@ -193,6 +208,28 @@ class WithSample(BaseModel):
193
208
  class DiffractionExperimentWithSample(DiffractionExperiment, WithSample): ...
194
209
 
195
210
 
211
+ class MultiXtalSelection(BaseModel):
212
+ name: str
213
+
214
+
215
+ class TopNByMaxCountSelection(MultiXtalSelection):
216
+ name: Literal["TopNByMaxCount"] = "TopNByMaxCount" # pyright: ignore [reportIncompatibleVariableOverride]
217
+ n: int
218
+
219
+
220
+ class WithCentreSelection(BaseModel):
221
+ select_centres: TopNByMaxCountSelection = Field(
222
+ discriminator="name", default=TopNByMaxCountSelection(n=1)
223
+ )
224
+
225
+ @property
226
+ def selection_params(self) -> MultiXtalSelection:
227
+ """A helper property because pydantic does not allow polymorphism with base classes
228
+ # only type unions"""
229
+ cast1 = cast(MultiXtalSelection, self.select_centres)
230
+ return cast1
231
+
232
+
196
233
  class OptionalXyzStarts(BaseModel):
197
234
  x_start_um: float | None = None
198
235
  y_start_um: float | None = None
@@ -1,6 +1,7 @@
1
1
  from enum import Enum
2
2
 
3
3
  from dodal.devices.aperturescatterguard import ApertureValue
4
+ from dodal.devices.detector import EIGER2_X_16M_SIZE
4
5
  from dodal.devices.zocalo.zocalo_constants import ZOCALO_ENV as ZOCALO_ENV_FROM_DODAL
5
6
  from dodal.utils import get_beamline_name
6
7
  from pydantic.dataclasses import dataclass
@@ -18,7 +19,9 @@ class DocDescriptorNames:
18
19
  OAV_GRID_SNAPSHOT_TRIGGERED = "snapshot_to_ispyb"
19
20
  HARDWARE_READ_PRE = "read_hardware_for_callbacks_pre_collection"
20
21
  HARDWARE_READ_DURING = "read_hardware_for_callbacks_during_collection"
22
+ SAMPLE_HANDLING_EXCEPTION = "sample_handling_exception"
21
23
  ZOCALO_HW_READ = "zocalo_read_hardware_plan"
24
+ FLYSCAN_RESULTS = "flyscan_results_obtained"
22
25
 
23
26
 
24
27
  @dataclass(frozen=True)
@@ -34,6 +37,7 @@ class OavConstants:
34
37
 
35
38
  @dataclass(frozen=True)
36
39
  class PlanNameConstants:
40
+ LOAD_CENTRE_COLLECT = "load_centre_collect"
37
41
  # Robot load subplan
38
42
  ROBOT_LOAD = "robot_load"
39
43
  # Gridscan
@@ -43,10 +47,14 @@ class PlanNameConstants:
43
47
  GRIDSCAN_AND_MOVE = "run_gridscan_and_move"
44
48
  GRIDSCAN_MAIN = "run_gridscan"
45
49
  DO_FGS = "do_fgs"
50
+ # IspyB callback activation
51
+ ISPYB_ACTIVATION = "ispyb_activation"
52
+ ROBOT_LOAD_AND_SNAPSHOTS = "robot_load_and_snapshots"
46
53
  # Rotation scan
47
54
  ROTATION_MULTI = "multi_rotation_wrapper"
48
55
  ROTATION_OUTER = "rotation_scan_with_cleanup"
49
56
  ROTATION_MAIN = "rotation_scan_main"
57
+ FLYSCAN_RESULTS = "xray_centre_results"
50
58
 
51
59
 
52
60
  @dataclass(frozen=True)
@@ -64,6 +72,7 @@ class HardwareConstants:
64
72
  OAV_REFRESH_DELAY = 0.3
65
73
  PANDA_FGS_RUN_UP_DEFAULT = 0.17
66
74
  CRYOJET_MARGIN_MM = 0.2
75
+ THAWING_TIME = 20
67
76
 
68
77
 
69
78
  @dataclass(frozen=True)
@@ -87,8 +96,9 @@ class DetectorParamConstants:
87
96
  BEAM_XY_LUT_PATH = (
88
97
  "tests/test_data/test_det_dist_converter.txt"
89
98
  if TEST_MODE
90
- else "/dls_sw/{BEAMLINE}/software/daq_configuration/lookup/DetDistToBeamXYConverter.txt"
99
+ else f"/dls_sw/{BEAMLINE}/software/daq_configuration/lookup/DetDistToBeamXYConverter.txt"
91
100
  )
101
+ DETECTOR = EIGER2_X_16M_SIZE
92
102
 
93
103
 
94
104
  @dataclass(frozen=True)
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from dodal.devices.aperturescatterguard import ApertureValue
6
+ from dodal.devices.detector import (
7
+ DetectorParams,
8
+ )
9
+ from pydantic import Field
10
+
11
+ from mx_bluesky.common.parameters.components import (
12
+ DiffractionExperimentWithSample,
13
+ IspybExperimentType,
14
+ OptionalGonioAngleStarts,
15
+ WithScan,
16
+ XyzStarts,
17
+ )
18
+ from mx_bluesky.common.parameters.constants import (
19
+ DetectorParamConstants,
20
+ GridscanParamConstants,
21
+ HardwareConstants,
22
+ )
23
+ from mx_bluesky.common.parameters.robot_load import RobotLoadAndEnergyChange
24
+
25
+
26
+ class GridCommon(
27
+ DiffractionExperimentWithSample,
28
+ OptionalGonioAngleStarts,
29
+ ):
30
+ grid_width_um: float = Field(default=GridscanParamConstants.WIDTH_UM)
31
+ exposure_time_s: float = Field(default=GridscanParamConstants.EXPOSURE_TIME_S)
32
+ use_roi_mode: bool = Field(default=GridscanParamConstants.USE_ROI)
33
+
34
+ ispyb_experiment_type: IspybExperimentType = Field(
35
+ default=IspybExperimentType.GRIDSCAN_3D
36
+ )
37
+ selected_aperture: ApertureValue | None = Field(default=ApertureValue.SMALL)
38
+
39
+ @property
40
+ def detector_params(self):
41
+ self.det_dist_to_beam_converter_path = (
42
+ self.det_dist_to_beam_converter_path
43
+ or DetectorParamConstants.BEAM_XY_LUT_PATH
44
+ )
45
+ optional_args = {}
46
+ if self.run_number:
47
+ optional_args["run_number"] = self.run_number
48
+ assert (
49
+ self.detector_distance_mm is not None
50
+ ), "Detector distance must be filled before generating DetectorParams"
51
+ os.makedirs(self.storage_directory, exist_ok=True)
52
+ return DetectorParams(
53
+ detector_size_constants=DetectorParamConstants.DETECTOR,
54
+ expected_energy_ev=self.demand_energy_ev,
55
+ exposure_time=self.exposure_time_s,
56
+ directory=self.storage_directory,
57
+ prefix=self.file_name,
58
+ detector_distance=self.detector_distance_mm,
59
+ omega_start=self.omega_start_deg or 0,
60
+ omega_increment=0,
61
+ num_images_per_trigger=1,
62
+ num_triggers=self.num_images,
63
+ use_roi_mode=self.use_roi_mode,
64
+ det_dist_to_beam_converter_path=self.det_dist_to_beam_converter_path,
65
+ trigger_mode=self.trigger_mode,
66
+ **optional_args,
67
+ )
68
+
69
+
70
+ class RobotLoadThenCentre(GridCommon):
71
+ thawing_time: float = Field(default=HardwareConstants.THAWING_TIME)
72
+
73
+ def robot_load_params(self):
74
+ my_params = self.model_dump()
75
+ return RobotLoadAndEnergyChange(**my_params)
76
+
77
+ def pin_centre_then_xray_centre_params(self):
78
+ my_params = self.model_dump()
79
+ del my_params["thawing_time"]
80
+ return PinTipCentreThenXrayCentre(**my_params)
81
+
82
+
83
+ class GridScanWithEdgeDetect(GridCommon):
84
+ box_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)
85
+
86
+
87
+ class PinTipCentreThenXrayCentre(GridCommon):
88
+ tip_offset_um: float = 0
89
+
90
+
91
+ class SpecifiedGrid(XyzStarts, WithScan):
92
+ """A specified grid is one which has defined values for the start position,
93
+ grid and box sizes, etc., as opposed to parameters for a plan which will create
94
+ those parameters at some point (e.g. through optical pin detection)."""
@@ -7,10 +7,10 @@ from mx_bluesky.common.parameters.components import (
7
7
  WithSnapshot,
8
8
  WithVisit,
9
9
  )
10
- from mx_bluesky.hyperion.parameters.constants import CONST
10
+ from mx_bluesky.common.parameters.constants import HardwareConstants
11
11
 
12
12
 
13
13
  class RobotLoadAndEnergyChange(
14
14
  MxBlueskyParameters, WithSample, WithSnapshot, WithOptionalEnergyChange, WithVisit
15
15
  ):
16
- thawing_time: float = Field(default=CONST.I03.THAWING_TIME)
16
+ thawing_time: float = Field(default=HardwareConstants.THAWING_TIME)
@@ -3,7 +3,7 @@ from time import time
3
3
 
4
4
  import bluesky.plan_stubs as bps
5
5
  import bluesky.preprocessors as bpp
6
- from blueapi.core import MsgGenerator
6
+ from bluesky.utils import MsgGenerator
7
7
  from dodal.devices.eiger import EigerDetector
8
8
  from dodal.devices.fast_grid_scan import FastGridScanCommon
9
9
  from dodal.devices.synchrotron import Synchrotron
@@ -11,7 +11,7 @@ from dodal.devices.zocalo.zocalo_results import (
11
11
  ZOCALO_STAGE_GROUP,
12
12
  )
13
13
  from dodal.log import LOGGER
14
- from dodal.plans.check_topup import check_topup_and_wait_if_necessary
14
+ from dodal.plan_stubs.check_topup import check_topup_and_wait_if_necessary
15
15
  from scanspec.core import AxesPoints, Axis
16
16
 
17
17
  from mx_bluesky.common.device_setup_plans.read_hardware_for_setup import (
@@ -12,6 +12,8 @@ from dodal.log import (
12
12
  )
13
13
  from dodal.log import LOGGER as dodal_logger
14
14
 
15
+ LOGGER = logging.getLogger("mx-bluesky")
16
+
15
17
  __logger_handlers: DodalLogHandlers | None = None
16
18
 
17
19
 
@@ -7,9 +7,10 @@ from queue import Queue
7
7
  from traceback import format_exception
8
8
  from typing import Any
9
9
 
10
- from blueapi.core import BlueskyContext, MsgGenerator
10
+ from blueapi.core import BlueskyContext
11
11
  from bluesky.callbacks.zmq import Publisher
12
12
  from bluesky.run_engine import RunEngine
13
+ from bluesky.utils import MsgGenerator
13
14
  from flask import Flask, request
14
15
  from flask_restful import Api, Resource
15
16
  from pydantic.dataclasses import dataclass
@@ -19,6 +19,7 @@ from mx_bluesky.hyperion.utils.utils import (
19
19
 
20
20
  MIRROR_VOLTAGE_GROUP = "MIRROR_VOLTAGE_GROUP"
21
21
  DCM_GROUP = "DCM_GROUP"
22
+ YAW_LAT_TIMEOUT_S = 30
22
23
 
23
24
 
24
25
  def _apply_and_wait_for_voltages_to_settle(
@@ -46,31 +47,42 @@ def _apply_and_wait_for_voltages_to_settle(
46
47
  for voltage_channel, required_voltage in zip(
47
48
  channels.values(), required_voltages, strict=True
48
49
  ):
49
- LOGGER.debug(
50
+ LOGGER.info(
50
51
  f"Applying and waiting for voltage {voltage_channel.name} = {required_voltage}"
51
52
  )
52
53
  yield from bps.abs_set(
53
- voltage_channel, required_voltage, group=MIRROR_VOLTAGE_GROUP
54
+ voltage_channel, required_voltage, group=MIRROR_VOLTAGE_GROUP, wait=True
54
55
  )
55
56
 
56
- yield from bps.wait(group=MIRROR_VOLTAGE_GROUP)
57
-
58
57
 
59
58
  def adjust_mirror_stripe(
60
59
  energy_kev, mirror: FocusingMirrorWithStripes, mirror_voltages: MirrorVoltages
61
60
  ):
62
61
  """Feedback should be OFF prior to entry, in order to prevent
63
62
  feedback from making unnecessary corrections while beam is being adjusted."""
64
- stripe = mirror.energy_to_stripe(energy_kev)
63
+ mirror_config = mirror.energy_to_stripe(energy_kev)
65
64
 
66
65
  LOGGER.info(
67
- f"Adjusting mirror stripe for {energy_kev}keV selecting {stripe} stripe"
66
+ f"Adjusting mirror stripe for {energy_kev}keV selecting {mirror_config['stripe']} stripe"
68
67
  )
69
- yield from bps.abs_set(mirror.stripe, stripe, wait=True)
68
+ yield from bps.abs_set(mirror.stripe, mirror_config["stripe"], wait=True)
70
69
  yield from bps.trigger(mirror.apply_stripe)
71
70
 
71
+ # yaw, lat cannot be done simultaneously
72
+ LOGGER.info(f"Adjusting {mirror.name} lat to {mirror_config['lat_mm']}")
73
+ yield from bps.abs_set(
74
+ mirror.x_mm, mirror_config["lat_mm"], wait=True, timeout=YAW_LAT_TIMEOUT_S
75
+ )
76
+
77
+ LOGGER.info(f"Adjusting {mirror.name} yaw to {mirror_config['yaw_mrad']}")
78
+ yield from bps.abs_set(
79
+ mirror.yaw_mrad, mirror_config["yaw_mrad"], wait=True, timeout=YAW_LAT_TIMEOUT_S
80
+ )
81
+
72
82
  LOGGER.info("Adjusting mirror voltages...")
73
- yield from _apply_and_wait_for_voltages_to_settle(stripe, mirror_voltages)
83
+ yield from _apply_and_wait_for_voltages_to_settle(
84
+ mirror_config["stripe"], mirror_voltages
85
+ )
74
86
 
75
87
 
76
88
  def adjust_dcm_pitch_roll_vfm_from_lut(
@@ -109,31 +121,9 @@ def adjust_dcm_pitch_roll_vfm_from_lut(
109
121
  yield from dcm_roll_adjuster(DCM_GROUP)
110
122
  LOGGER.info("Waiting for DCM roll adjust to complete...")
111
123
 
112
- # DCM Perp pitch
113
- offset_mm = undulator_dcm.dcm_fixed_offset_mm
114
- LOGGER.info(f"Adjusting DCM offset to {offset_mm} mm")
115
- yield from bps.abs_set(dcm.offset_in_mm, offset_mm, group=DCM_GROUP)
116
-
117
124
  #
118
- # Adjust mirrors
125
+ # Adjust vfm mirror stripe and mirror voltages
119
126
  #
120
127
 
121
- # No need to change HFM
122
-
123
- # Assumption is focus mode is already set to "sample"
124
- # not sure how we check this
125
-
126
128
  # VFM Stripe selection
127
129
  yield from adjust_mirror_stripe(energy_kev, vfm, mirror_voltages)
128
- yield from bps.wait(DCM_GROUP)
129
-
130
- # VFM Adjust - for I03 this table always returns the same value
131
- vfm_lut = vfm.bragg_to_lat_lookup_table_path
132
- assert vfm_lut is not None
133
- vfm_x_adjuster = lookup_table_adjuster(
134
- linear_interpolation_lut(vfm_lut),
135
- vfm.x_mm,
136
- bragg_deg,
137
- )
138
- LOGGER.info("Waiting for VFM Lat (Horizontal Translation) to complete...")
139
- yield from vfm_x_adjuster()
@@ -4,7 +4,7 @@ from importlib import resources
4
4
  from pathlib import Path
5
5
 
6
6
  import bluesky.plan_stubs as bps
7
- from blueapi.core import MsgGenerator
7
+ from bluesky.utils import MsgGenerator
8
8
  from dodal.common.beamlines.beamline_utils import get_path_provider
9
9
  from dodal.devices.fast_grid_scan import PandAGridScanParams
10
10
  from ophyd_async.core import load_device
@@ -63,12 +63,12 @@ def _get_seq_table(
63
63
  An instance of SeqTable describing the panda sequencer table
64
64
  """
65
65
 
66
- start_of_grid_x_counts = int(parameters.x_start * MM_TO_ENCODER_COUNTS)
66
+ start_of_grid_x_counts = int(parameters.x_start_mm * MM_TO_ENCODER_COUNTS)
67
67
 
68
68
  # x_start is the first trigger point, so we need to travel to x_steps-1 for the final trigger point
69
69
  end_of_grid_x_counts = int(
70
70
  start_of_grid_x_counts
71
- + (parameters.x_step_size * (parameters.x_steps - 1) * MM_TO_ENCODER_COUNTS)
71
+ + (parameters.x_step_size_mm * (parameters.x_steps - 1) * MM_TO_ENCODER_COUNTS)
72
72
  )
73
73
 
74
74
  exposure_distance_x_counts = int(exposure_distance_mm * MM_TO_ENCODER_COUNTS)
@@ -140,7 +140,7 @@ def setup_panda_for_flyscan(
140
140
  """
141
141
  assert parameters.x_steps > 0
142
142
  assert time_between_x_steps_ms * 1000 >= exposure_time_s
143
- assert sample_velocity_mm_per_s * exposure_time_s < parameters.x_step_size
143
+ assert sample_velocity_mm_per_s * exposure_time_s < parameters.x_step_size_mm
144
144
 
145
145
  yield from bps.stage(panda, group="panda-config")
146
146