mx-bluesky 1.4.1a0__py3-none-any.whl → 1.4.2__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 (49) 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/i24/serial/__init__.py +0 -6
  4. mx_bluesky/beamlines/i24/serial/dcid.py +125 -151
  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 +66 -36
  7. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +1 -1
  8. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/MappingLite-oxford_py3v1.edl +2 -46
  9. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +74 -120
  10. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +58 -66
  11. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +1 -19
  12. mx_bluesky/beamlines/i24/serial/parameters/__init__.py +9 -1
  13. mx_bluesky/beamlines/i24/serial/parameters/constants.py +6 -0
  14. mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py +75 -16
  15. mx_bluesky/beamlines/i24/serial/parameters/utils.py +19 -0
  16. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +2 -0
  17. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +32 -8
  18. mx_bluesky/beamlines/i24/serial/write_nexus.py +66 -67
  19. mx_bluesky/common/parameters/components.py +3 -3
  20. mx_bluesky/common/parameters/constants.py +5 -0
  21. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +21 -31
  22. mx_bluesky/hyperion/device_setup_plans/manipulate_sample.py +6 -6
  23. mx_bluesky/hyperion/device_setup_plans/smargon.py +3 -3
  24. mx_bluesky/hyperion/exceptions.py +13 -1
  25. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +16 -10
  26. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +0 -8
  27. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +58 -34
  28. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +8 -2
  29. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +3 -3
  30. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +30 -26
  31. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +26 -7
  32. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +0 -7
  33. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +8 -7
  34. mx_bluesky/hyperion/external_interaction/callbacks/__init__.py +0 -4
  35. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +4 -0
  36. mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py +5 -0
  37. mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +18 -10
  38. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/__init__.py +0 -0
  39. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/sample_handling_callback.py +84 -0
  40. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +10 -7
  41. mx_bluesky/hyperion/external_interaction/exceptions.py +0 -9
  42. mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +65 -15
  43. mx_bluesky/hyperion/utils/validation.py +1 -1
  44. {mx_bluesky-1.4.1a0.dist-info → mx_bluesky-1.4.2.dist-info}/METADATA +2 -2
  45. {mx_bluesky-1.4.1a0.dist-info → mx_bluesky-1.4.2.dist-info}/RECORD +49 -46
  46. {mx_bluesky-1.4.1a0.dist-info → mx_bluesky-1.4.2.dist-info}/LICENSE +0 -0
  47. {mx_bluesky-1.4.1a0.dist-info → mx_bluesky-1.4.2.dist-info}/WHEEL +0 -0
  48. {mx_bluesky-1.4.1a0.dist-info → mx_bluesky-1.4.2.dist-info}/entry_points.txt +0 -0
  49. {mx_bluesky-1.4.1a0.dist-info → mx_bluesky-1.4.2.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,6 @@ import pathlib
3
3
  import pprint
4
4
  import time
5
5
  from datetime import datetime
6
- from typing import Literal
7
6
 
8
7
  import requests
9
8
 
@@ -13,43 +12,49 @@ 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
15
+ from mx_bluesky.beamlines.i24.serial.setup_beamline import Eiger, caget
17
16
 
18
17
 
19
18
  def call_nexgen(
20
19
  chip_prog_dict: dict | None,
21
- start_time: datetime,
22
20
  parameters: ExtruderParameters | FixedTargetParameters,
23
- wavelength: float,
24
- 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,
25
24
  ):
26
- det_type = parameters.detector_name
27
- print(f"det_type: {det_type}")
25
+ """Call the nexus writer by sending a request to nexgen-server.
28
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).
39
+
40
+ """
29
41
  current_chip_map = None
30
- if expt_type == "fixed-target" and isinstance(parameters, FixedTargetParameters):
31
- if not (
32
- parameters.map_type == MappingType.NoMap
33
- or parameters.chip.chip_type == ChipType.Custom
34
- ):
35
- # NOTE Nexgen server is still on nexgen v0.7.2 (fully working for ssx)
36
- # Will need to be updated, for correctness sake map needs to be None.
37
- current_chip_map = "/dls_sw/i24/scripts/fastchips/litemaps/currentchip.map"
38
- pump_status = bool(parameters.pump_repeat)
39
- total_numb_imgs = parameters.total_num_images
40
- elif expt_type == "extruder" and isinstance(parameters, ExtruderParameters):
41
- # chip_prog_dict should be None for extruder (passed as input for now)
42
- total_numb_imgs = parameters.num_images
43
- pump_status = parameters.pump_status
44
- else:
45
- 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
+ # For nexgen >= 0.9.10
49
+ current_chip_map = parameters.chip_map
50
+ pump_status = bool(parameters.pump_repeat)
51
+ total_numb_imgs = parameters.total_num_images
52
+ case ExtruderParameters():
53
+ total_numb_imgs = parameters.num_images
54
+ pump_status = parameters.pump_status
46
55
 
47
- filename_prefix = cagetstring(Eiger.pv.filenameRBV)
48
- meta_h5 = (
49
- pathlib.Path(parameters.visit)
50
- / parameters.directory
51
- / f"{filename_prefix}_meta.h5"
52
- )
56
+ filename_prefix = parameters.filename
57
+ meta_h5 = parameters.visit / parameters.directory / f"{filename_prefix}_meta.h5"
53
58
  t0 = time.time()
54
59
  max_wait = 60 # seconds
55
60
  SSX_LOGGER.info(f"Watching for {meta_h5}")
@@ -62,44 +67,38 @@ def call_nexgen(
62
67
  time.sleep(1)
63
68
  if not meta_h5.exists():
64
69
  SSX_LOGGER.warning(f"Giving up waiting for {meta_h5} after {max_wait} seconds")
65
- return False
70
+ return
66
71
 
67
- transmission = (float(caget(Eiger.pv.transmission)),)
68
-
69
- if det_type == Eiger.name:
70
- bit_depth = int(caget(Eiger.pv.bit_depth))
71
- SSX_LOGGER.debug(
72
- f"Call to nexgen server with the following chip definition: \n{chip_prog_dict}"
73
- )
72
+ bit_depth = int(caget(Eiger.pv.bit_depth))
73
+ SSX_LOGGER.debug(
74
+ f"Call to nexgen server with the following chip definition: \n{chip_prog_dict}"
75
+ )
74
76
 
75
- access_token = pathlib.Path("/scratch/ssx_nexgen.key").read_text().strip()
76
- url = "https://ssx-nexgen.diamond.ac.uk/ssx_eiger/write"
77
- headers = {"Authorization": f"Bearer {access_token}"}
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}"}
78
80
 
79
- payload = {
80
- "beamline": "i24",
81
- "beam_center": [caget(Eiger.pv.beamx), caget(Eiger.pv.beamy)],
82
- "chipmap": current_chip_map,
83
- "chip_info": chip_prog_dict,
84
- "det_dist": parameters.detector_distance_mm,
85
- "exp_time": parameters.exposure_time_s,
86
- "expt_type": expt_type,
87
- "filename": filename_prefix,
88
- "num_imgs": total_numb_imgs,
89
- "pump_status": pump_status,
90
- "pump_exp": parameters.laser_dwell_s,
91
- "pump_delay": parameters.laser_delay_s,
92
- "transmission": transmission[0],
93
- "visitpath": os.fspath(meta_h5.parent),
94
- "wavelength": wavelength,
95
- "bit_depth": bit_depth,
96
- }
97
- SSX_LOGGER.info(f"Sending POST request to {url} with payload:")
98
- SSX_LOGGER.info(pprint.pformat(payload))
99
- response = requests.post(url, headers=headers, json=payload)
100
- SSX_LOGGER.info(
101
- f"Response: {response.text} (status code: {response.status_code})"
102
- )
103
- # the following will raise an error if the request was unsuccessful
104
- return response.status_code == requests.codes.ok
105
- return False
81
+ payload = {
82
+ "beamline": "i24",
83
+ "beam_center": beam_center_in_pix,
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": parameters.nexgen_experiment_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": parameters.transmission,
95
+ "visitpath": os.fspath(meta_h5.parent),
96
+ "wavelength": wavelength_in_a,
97
+ "bit_depth": bit_depth,
98
+ "start_time": start_time,
99
+ }
100
+ SSX_LOGGER.info(f"Sending POST request to {url} with payload:")
101
+ SSX_LOGGER.info(pprint.pformat(payload))
102
+ response = requests.post(url, headers=headers, json=payload)
103
+ response.raise_for_status()
104
+ SSX_LOGGER.info(f"Response: {response.text} (status code: {response.status_code})")
@@ -103,12 +103,12 @@ class MxBlueskyParameters(BaseModel):
103
103
 
104
104
  @field_validator("parameter_model_version")
105
105
  @classmethod
106
- def _validate_version(cls, version: Version):
106
+ def _validate_version(cls, version: SemanticVersion):
107
107
  assert (
108
- version >= Version(major=PARAMETER_VERSION.major)
108
+ version >= SemanticVersion(major=PARAMETER_VERSION.major)
109
109
  ), f"Parameter version too old! This version of hyperion uses {PARAMETER_VERSION}"
110
110
  assert (
111
- version <= Version(major=PARAMETER_VERSION.major + 1)
111
+ version <= SemanticVersion(major=PARAMETER_VERSION.major + 1)
112
112
  ), f"Parameter version too new! This version of hyperion uses {PARAMETER_VERSION}"
113
113
  return version
114
114
 
@@ -19,6 +19,7 @@ class DocDescriptorNames:
19
19
  OAV_GRID_SNAPSHOT_TRIGGERED = "snapshot_to_ispyb"
20
20
  HARDWARE_READ_PRE = "read_hardware_for_callbacks_pre_collection"
21
21
  HARDWARE_READ_DURING = "read_hardware_for_callbacks_during_collection"
22
+ SAMPLE_HANDLING_EXCEPTION = "sample_handling_exception"
22
23
  ZOCALO_HW_READ = "zocalo_read_hardware_plan"
23
24
  FLYSCAN_RESULTS = "flyscan_results_obtained"
24
25
 
@@ -36,6 +37,7 @@ class OavConstants:
36
37
 
37
38
  @dataclass(frozen=True)
38
39
  class PlanNameConstants:
40
+ LOAD_CENTRE_COLLECT = "load_centre_collect"
39
41
  # Robot load subplan
40
42
  ROBOT_LOAD = "robot_load"
41
43
  # Gridscan
@@ -45,6 +47,9 @@ class PlanNameConstants:
45
47
  GRIDSCAN_AND_MOVE = "run_gridscan_and_move"
46
48
  GRIDSCAN_MAIN = "run_gridscan"
47
49
  DO_FGS = "do_fgs"
50
+ # IspyB callback activation
51
+ ISPYB_ACTIVATION = "ispyb_activation"
52
+ ROBOT_LOAD_AND_SNAPSHOTS = "robot_load_and_snapshots"
48
53
  # Rotation scan
49
54
  ROTATION_MULTI = "multi_rotation_wrapper"
50
55
  ROTATION_OUTER = "rotation_scan_with_cleanup"
@@ -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()
@@ -78,11 +78,11 @@ def move_x_y_z(
78
78
  axes are optional."""
79
79
 
80
80
  LOGGER.info(f"Moving smargon to x, y, z: {(x_mm, y_mm, z_mm)}")
81
- if x_mm:
81
+ if x_mm is not None:
82
82
  yield from bps.abs_set(smargon.x, x_mm, group=group)
83
- if y_mm:
83
+ if y_mm is not None:
84
84
  yield from bps.abs_set(smargon.y, y_mm, group=group)
85
- if z_mm:
85
+ if z_mm is not None:
86
86
  yield from bps.abs_set(smargon.z, z_mm, group=group)
87
87
  if wait:
88
88
  yield from bps.wait(group)
@@ -100,11 +100,11 @@ def move_phi_chi_omega(
100
100
  axes are optional."""
101
101
 
102
102
  LOGGER.info(f"Moving smargon to phi, chi, omega: {(phi, chi, omega)}")
103
- if phi:
103
+ if phi is not None:
104
104
  yield from bps.abs_set(smargon.phi, phi, group=group)
105
- if chi:
105
+ if chi is not None:
106
106
  yield from bps.abs_set(smargon.chi, chi, group=group)
107
- if omega:
107
+ if omega is not None:
108
108
  yield from bps.abs_set(smargon.omega, omega, group=group)
109
109
  if wait:
110
110
  yield from bps.wait(group)
@@ -2,17 +2,17 @@ import numpy as np
2
2
  from bluesky import plan_stubs as bps
3
3
  from dodal.devices.smargon import Smargon
4
4
 
5
- from mx_bluesky.hyperion.exceptions import WarningException
5
+ from mx_bluesky.hyperion.exceptions import SampleException
6
6
 
7
7
 
8
8
  def move_smargon_warn_on_out_of_range(
9
9
  smargon: Smargon, position: np.ndarray | list[float] | tuple[float, float, float]
10
10
  ):
11
- """Throws a WarningException if the specified position is out of range for the
11
+ """Throws a SampleException if the specified position is out of range for the
12
12
  smargon. Otherwise moves to that position."""
13
13
  limits = yield from smargon.get_xyz_limits()
14
14
  if not limits.position_valid(position):
15
- raise WarningException(
15
+ raise SampleException(
16
16
  "Pin tip centring failed - pin too long/short/bent and out of range"
17
17
  )
18
18
  yield from bps.mv(
@@ -13,9 +13,21 @@ class WarningException(Exception):
13
13
  pass
14
14
 
15
15
 
16
+ class SampleException(WarningException):
17
+ """An exception which identifies an issue relating to the sample."""
18
+
19
+ pass
20
+
21
+
16
22
  T = TypeVar("T")
17
23
 
18
24
 
25
+ class CrystalNotFoundException(SampleException):
26
+ """Raised if grid detection completed normally but no crystal was found."""
27
+
28
+ pass
29
+
30
+
19
31
  def catch_exception_and_warn(
20
32
  exception_to_catch: type[Exception],
21
33
  func: Callable[..., Generator[Msg, None, T]],
@@ -36,7 +48,7 @@ def catch_exception_and_warn(
36
48
 
37
49
  def warn_if_exception_matches(exception: Exception):
38
50
  if isinstance(exception, exception_to_catch):
39
- raise WarningException(str(exception))
51
+ raise SampleException(str(exception)) from exception
40
52
  yield from null()
41
53
 
42
54
  return (
@@ -66,7 +66,7 @@ from mx_bluesky.hyperion.device_setup_plans.setup_zebra import (
66
66
  from mx_bluesky.hyperion.device_setup_plans.xbpm_feedback import (
67
67
  transmission_and_xbpm_feedback_for_collection_decorator,
68
68
  )
69
- from mx_bluesky.hyperion.exceptions import WarningException
69
+ from mx_bluesky.hyperion.exceptions import CrystalNotFoundException, SampleException
70
70
  from mx_bluesky.hyperion.experiment_plans.change_aperture_then_move_plan import (
71
71
  change_aperture_then_move_to_xtal,
72
72
  )
@@ -79,14 +79,10 @@ from mx_bluesky.hyperion.parameters.constants import CONST
79
79
  from mx_bluesky.hyperion.parameters.gridscan import HyperionThreeDGridScan
80
80
  from mx_bluesky.hyperion.utils.context import device_composite_from_context
81
81
 
82
-
83
- class SmargonSpeedException(Exception):
84
- pass
82
+ ZOCALO_MIN_TOTAL_COUNT_THRESHOLD = 3
85
83
 
86
84
 
87
- class CrystalNotFoundException(WarningException):
88
- """Raised if grid detection completed normally but no crystal was found."""
89
-
85
+ class SmargonSpeedException(Exception):
90
86
  pass
91
87
 
92
88
 
@@ -253,10 +249,20 @@ def run_gridscan_and_fetch_results(
253
249
  LOGGER.info("Zocalo triggered and read, interpreting results.")
254
250
  xrc_results = yield from get_full_processing_results(fgs_composite.zocalo)
255
251
  LOGGER.info(f"Got xray centres, top 5: {xrc_results[:5]}")
256
- if xrc_results:
252
+ filtered_results = [
253
+ result
254
+ for result in xrc_results
255
+ if result["total_count"] >= ZOCALO_MIN_TOTAL_COUNT_THRESHOLD
256
+ ]
257
+ discarded_count = len(xrc_results) - len(filtered_results)
258
+ if discarded_count > 0:
259
+ LOGGER.info(
260
+ f"Removed {discarded_count} results because below threshold"
261
+ )
262
+ if filtered_results:
257
263
  flyscan_results = [
258
264
  _xrc_result_in_boxes_to_result_in_mm(xr, parameters)
259
- for xr in xrc_results
265
+ for xr in filtered_results
260
266
  ]
261
267
  else:
262
268
  LOGGER.warning("No X-ray centre received")
@@ -378,7 +384,7 @@ def wait_for_gridscan_valid(fgs_motors: FastGridScanCommon, timeout=0.5):
378
384
  LOGGER.info("Gridscan scan valid and position counter reset")
379
385
  return
380
386
  yield from bps.sleep(SLEEP_PER_CHECK)
381
- raise WarningException("Scan invalid - pin too long/short/bent and out of range")
387
+ raise SampleException("Scan invalid - pin too long/short/bent and out of range")
382
388
 
383
389
 
384
390
  @dataclasses.dataclass
@@ -144,14 +144,6 @@ def detect_grid_and_do_gridscan(
144
144
  parameters.box_size_um,
145
145
  )
146
146
 
147
- if parameters.selected_aperture:
148
- # Start moving the aperture/scatterguard into position without moving it in
149
- yield from bps.abs_set(
150
- composite.aperture_scatterguard.aperture_outside_beam,
151
- parameters.selected_aperture,
152
- group=CONST.WAIT.GRID_READY_FOR_DC,
153
- )
154
-
155
147
  yield from run_grid_detection_plan(
156
148
  oav_params,
157
149
  snapshot_template,
@@ -1,8 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  from collections.abc import Sequence
2
4
 
3
5
  import pydantic
4
- from blueapi.core import BlueskyContext, MsgGenerator
5
- from bluesky.preprocessors import subs_wrapper
6
+ from blueapi.core import BlueskyContext
7
+ from bluesky.preprocessors import run_decorator, set_run_key_decorator, subs_wrapper
8
+ from bluesky.utils import MsgGenerator
6
9
  from dodal.devices.oav.oav_parameters import OAVParameters
7
10
  from dodal.devices.smargon import Smargon
8
11
 
@@ -19,7 +22,11 @@ from mx_bluesky.hyperion.experiment_plans.rotation_scan_plan import (
19
22
  RotationScanComposite,
20
23
  multi_rotation_scan,
21
24
  )
25
+ from mx_bluesky.hyperion.external_interaction.callbacks.sample_handling.sample_handling_callback import (
26
+ sample_handling_callback_decorator,
27
+ )
22
28
  from mx_bluesky.hyperion.log import LOGGER
29
+ from mx_bluesky.hyperion.parameters.constants import CONST
23
30
  from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
24
31
  from mx_bluesky.hyperion.utils.context import device_composite_from_context
25
32
 
@@ -53,36 +60,53 @@ def load_centre_collect_full(
53
60
  if not oav_params:
54
61
  oav_params = OAVParameters(context="xrayCentring")
55
62
 
56
- flyscan_event_handler = XRayCentreEventHandler()
57
- yield from subs_wrapper(
58
- robot_load_then_xray_centre(composite, parameters.robot_load_then_centre),
59
- flyscan_event_handler,
60
- )
61
-
62
- assert (
63
- flyscan_event_handler.xray_centre_results
64
- ), "Flyscan result event not received or no crystal found and exception not raised"
65
-
66
- selection_func = flyscan_result.resolve_selection_fn(parameters.selection_params)
67
- hits: Sequence[flyscan_result.XRayCentreResult] = selection_func(
68
- flyscan_event_handler.xray_centre_results
63
+ @set_run_key_decorator(CONST.PLAN.LOAD_CENTRE_COLLECT)
64
+ @run_decorator(
65
+ md={
66
+ "metadata": {"sample_id": parameters.sample_id},
67
+ "activate_callbacks": ["SampleHandlingCallback"],
68
+ }
69
69
  )
70
- LOGGER.info(
71
- f"Selected hits {hits} using {selection_func}, args={parameters.selection_params}"
72
- )
73
-
74
- multi_rotation = parameters.multi_rotation_scan
75
- rotation_template = multi_rotation.rotation_scans.copy()
76
-
77
- multi_rotation.rotation_scans.clear()
78
-
79
- for hit in hits:
80
- for rot in rotation_template:
81
- combination = rot.model_copy()
82
- combination.x_start_um, combination.y_start_um, combination.z_start_um = (
83
- axis * 1000 for axis in hit.centre_of_mass_mm
84
- )
85
- multi_rotation.rotation_scans.append(combination)
86
- multi_rotation = MultiRotationScan.model_validate(multi_rotation)
87
-
88
- yield from multi_rotation_scan(composite, multi_rotation, oav_params)
70
+ @sample_handling_callback_decorator()
71
+ def plan_with_callback_subs():
72
+ flyscan_event_handler = XRayCentreEventHandler()
73
+ yield from subs_wrapper(
74
+ robot_load_then_xray_centre(composite, parameters.robot_load_then_centre),
75
+ flyscan_event_handler,
76
+ )
77
+
78
+ assert flyscan_event_handler.xray_centre_results, "Flyscan result event not received or no crystal found and exception not raised"
79
+
80
+ selection_func = flyscan_result.resolve_selection_fn(
81
+ parameters.selection_params
82
+ )
83
+ hits: Sequence[flyscan_result.XRayCentreResult] = selection_func(
84
+ flyscan_event_handler.xray_centre_results
85
+ )
86
+ LOGGER.info(
87
+ f"Selected hits {hits} using {selection_func}, args={parameters.selection_params}"
88
+ )
89
+
90
+ multi_rotation = parameters.multi_rotation_scan
91
+ rotation_template = multi_rotation.rotation_scans.copy()
92
+
93
+ multi_rotation.rotation_scans.clear()
94
+
95
+ for hit in hits:
96
+ for rot in rotation_template:
97
+ combination = rot.model_copy()
98
+ (
99
+ combination.x_start_um,
100
+ combination.y_start_um,
101
+ combination.z_start_um,
102
+ ) = (axis * 1000 for axis in hit.centre_of_mass_mm)
103
+ multi_rotation.rotation_scans.append(combination)
104
+ multi_rotation = MultiRotationScan.model_validate(multi_rotation)
105
+
106
+ assert (
107
+ multi_rotation.demand_energy_ev
108
+ == parameters.robot_load_then_centre.demand_energy_ev
109
+ ), "Setting a different energy for gridscan and rotation is not supported"
110
+ yield from multi_rotation_scan(composite, multi_rotation, oav_params)
111
+
112
+ yield from plan_with_callback_subs()
@@ -3,7 +3,7 @@ from typing import Protocol
3
3
 
4
4
  from bluesky import plan_stubs as bps
5
5
  from bluesky.utils import MsgGenerator
6
- from dodal.devices.aperturescatterguard import ApertureScatterguard
6
+ from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue
7
7
  from dodal.devices.backlight import Backlight, BacklightPosition
8
8
  from dodal.devices.oav.oav_detector import OAV
9
9
  from dodal.devices.oav.oav_parameters import OAVParameters
@@ -33,16 +33,22 @@ def setup_beamline_for_OAV(
33
33
  max_vel = yield from bps.rd(smargon.omega.max_velocity)
34
34
  yield from bps.abs_set(smargon.omega.velocity, max_vel, group=group)
35
35
  yield from bps.abs_set(backlight, BacklightPosition.IN, group=group)
36
- yield from bps.trigger(aperture_scatterguard.move_out, group=group)
36
+ yield from bps.abs_set(
37
+ aperture_scatterguard,
38
+ ApertureValue.ROBOT_LOAD,
39
+ group=group,
40
+ )
37
41
 
38
42
 
39
43
  def oav_snapshot_plan(
40
44
  composite: OavSnapshotComposite,
41
45
  parameters: WithSnapshot,
42
46
  oav_parameters: OAVParameters,
47
+ wait: bool = True,
43
48
  ) -> MsgGenerator:
44
49
  if not parameters.take_snapshots:
45
50
  return
51
+ yield from bps.wait(group=CONST.WAIT.READY_FOR_OAV)
46
52
  yield from _setup_oav(composite, parameters, oav_parameters)
47
53
  for omega in parameters.snapshot_omegas_deg or []:
48
54
  yield from _take_oav_snapshot(composite, omega)
@@ -19,7 +19,7 @@ from mx_bluesky.hyperion.device_setup_plans.setup_oav import pre_centring_setup_
19
19
  from mx_bluesky.hyperion.device_setup_plans.smargon import (
20
20
  move_smargon_warn_on_out_of_range,
21
21
  )
22
- from mx_bluesky.hyperion.exceptions import WarningException
22
+ from mx_bluesky.hyperion.exceptions import SampleException
23
23
  from mx_bluesky.hyperion.log import LOGGER
24
24
  from mx_bluesky.hyperion.parameters.constants import CONST
25
25
  from mx_bluesky.hyperion.utils.context import device_composite_from_context
@@ -68,7 +68,7 @@ def move_pin_into_view(
68
68
  max_steps (int, optional): The number of steps to search with. Defaults to 2.
69
69
 
70
70
  Raises:
71
- WarningException: Error if the pin tip is never found
71
+ SampleException: Error if the pin tip is never found
72
72
 
73
73
  Returns:
74
74
  Tuple[int, int]: The location of the pin tip in pixels
@@ -105,7 +105,7 @@ def move_pin_into_view(
105
105
  tip_xy_px = yield from trigger_and_return_pin_tip(pin_tip_device)
106
106
 
107
107
  if not pin_tip_valid(tip_xy_px):
108
- raise WarningException(
108
+ raise SampleException(
109
109
  "Pin tip centring failed - pin too long/short/bent and out of range"
110
110
  )
111
111
  else: