mx-bluesky 1.5.15__py3-none-any.whl → 1.5.16__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 (72) hide show
  1. mx_bluesky/Getting started.ipynb +1 -0
  2. mx_bluesky/_version.py +2 -2
  3. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +18 -0
  4. mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +3 -2
  5. mx_bluesky/beamlines/i04/thawing_plan.py +1 -0
  6. mx_bluesky/beamlines/i24/jungfrau_commissioning/__init__.py +13 -0
  7. mx_bluesky/beamlines/i24/jungfrau_commissioning/callbacks/__init__.py +0 -0
  8. mx_bluesky/beamlines/i24/jungfrau_commissioning/callbacks/metadata_writer.py +86 -0
  9. mx_bluesky/beamlines/i24/jungfrau_commissioning/composites.py +35 -0
  10. mx_bluesky/beamlines/i24/jungfrau_commissioning/experiment_plans/do_darks.py +18 -19
  11. mx_bluesky/beamlines/i24/jungfrau_commissioning/experiment_plans/rotation_scan_plan.py +292 -0
  12. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/do_external_acquisition.py +3 -8
  13. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/do_internal_acquisition.py +4 -5
  14. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/plan_utils.py +14 -18
  15. mx_bluesky/beamlines/i24/parameters/__init__.py +0 -0
  16. mx_bluesky/beamlines/i24/parameters/constants.py +9 -0
  17. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_extruder_collect_py3v2.py +1 -1
  18. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +2 -2
  19. mx_bluesky/common/device_setup_plans/robot_load_unload.py +2 -24
  20. mx_bluesky/common/device_setup_plans/setup_zebra_and_shutter.py +5 -2
  21. mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +1 -1
  22. mx_bluesky/common/experiment_plans/inner_plans/read_hardware.py +1 -0
  23. mx_bluesky/common/experiment_plans/rotation/__init__.py +0 -0
  24. mx_bluesky/common/experiment_plans/rotation/rotation_utils.py +127 -0
  25. mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +13 -2
  26. mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +0 -2
  27. mx_bluesky/common/external_interaction/ispyb/data_model.py +1 -1
  28. mx_bluesky/common/external_interaction/ispyb/exp_eye_store.py +1 -1
  29. mx_bluesky/common/parameters/components.py +17 -7
  30. mx_bluesky/common/parameters/constants.py +6 -0
  31. mx_bluesky/{hyperion → common}/parameters/rotation.py +10 -8
  32. mx_bluesky/common/preprocessors/preprocessors.py +98 -36
  33. mx_bluesky/hyperion/__main__.py +55 -22
  34. mx_bluesky/hyperion/baton_handler.py +24 -64
  35. mx_bluesky/hyperion/blueapi_config.yaml +17 -0
  36. mx_bluesky/hyperion/blueapi_dev_config.yaml +16 -0
  37. mx_bluesky/hyperion/blueapi_plans/__init__.py +96 -0
  38. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +8 -6
  39. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +1 -1
  40. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +3 -1
  41. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +1 -0
  42. mx_bluesky/hyperion/experiment_plans/hyperion_grid_detect_then_xray_centre_plan.py +2 -2
  43. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +3 -1
  44. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +17 -6
  45. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +0 -3
  46. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +12 -126
  47. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +2 -2
  48. mx_bluesky/hyperion/external_interaction/agamemnon.py +3 -8
  49. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +121 -47
  50. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +3 -1
  51. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +3 -1
  52. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +6 -3
  53. mx_bluesky/hyperion/external_interaction/callbacks/stomp/__init__.py +0 -0
  54. mx_bluesky/hyperion/external_interaction/callbacks/stomp/dispatcher.py +33 -0
  55. mx_bluesky/hyperion/in_process_runner.py +132 -0
  56. mx_bluesky/hyperion/parameters/cli.py +43 -4
  57. mx_bluesky/hyperion/parameters/components.py +13 -0
  58. mx_bluesky/hyperion/parameters/constants.py +2 -9
  59. mx_bluesky/hyperion/parameters/load_centre_collect.py +3 -1
  60. mx_bluesky/hyperion/plan_runner.py +45 -66
  61. mx_bluesky/hyperion/plan_runner_api.py +3 -4
  62. mx_bluesky/hyperion/supervisor/__init__.py +3 -0
  63. mx_bluesky/hyperion/supervisor/_supervisor.py +116 -0
  64. mx_bluesky/hyperion/supervisor/client_config.yaml +6 -0
  65. mx_bluesky/hyperion/supervisor/supervisor_config.yaml +10 -0
  66. mx_bluesky/hyperion/supervisor/supervisor_dev_config.yaml +9 -0
  67. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/METADATA +3 -31
  68. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/RECORD +72 -52
  69. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/WHEEL +0 -0
  70. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/entry_points.txt +0 -0
  71. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/licenses/LICENSE +0 -0
  72. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/top_level.txt +0 -0
@@ -5,18 +5,19 @@ from ophyd_async.core import (
5
5
  WatchableAsyncStatus,
6
6
  )
7
7
  from ophyd_async.fastcs.jungfrau import (
8
+ GainMode,
8
9
  create_jungfrau_internal_triggering_info,
9
10
  )
10
11
  from pydantic import PositiveInt
11
12
 
12
13
  from mx_bluesky.beamlines.i24.jungfrau_commissioning.plan_stubs.plan_utils import (
13
14
  fly_jungfrau,
14
- override_file_path,
15
15
  )
16
16
 
17
17
 
18
18
  def do_internal_acquisition(
19
19
  exp_time_s: float,
20
+ gain_mode: GainMode,
20
21
  total_frames: PositiveInt = 1,
21
22
  jungfrau: CommissioningJungfrau = inject("jungfrau"),
22
23
  path_of_output_file: str | None = None,
@@ -31,6 +32,7 @@ def do_internal_acquisition(
31
32
 
32
33
  Args:
33
34
  exp_time_s: Length of detector exposure for each frame.
35
+ gain_mode: Which gain mode to put the Jungfrau into before starting the acquisition.
34
36
  total_frames: Number of frames taken after being internally triggered.
35
37
  period_between_frames_s: Time between each detector frame, including deadtime. Not needed if frames_per_triggers is 1.
36
38
  jungfrau: Jungfrau device
@@ -39,9 +41,6 @@ def do_internal_acquisition(
39
41
  wait: Optionally block until data collection is complete.
40
42
  """
41
43
 
42
- if path_of_output_file:
43
- override_file_path(jungfrau, path_of_output_file)
44
-
45
44
  trigger_info = create_jungfrau_internal_triggering_info(total_frames, exp_time_s)
46
- status = yield from fly_jungfrau(jungfrau, trigger_info, wait=wait)
45
+ status = yield from fly_jungfrau(jungfrau, trigger_info, gain_mode, wait=wait)
47
46
  return status
@@ -1,4 +1,4 @@
1
- from pathlib import PurePath
1
+ from collections.abc import Callable
2
2
  from typing import cast
3
3
 
4
4
  import bluesky.plan_stubs as bps
@@ -6,11 +6,10 @@ from bluesky.utils import MsgGenerator
6
6
  from dodal.common.watcher_utils import log_on_percentage_complete
7
7
  from dodal.devices.i24.commissioning_jungfrau import CommissioningJungfrau
8
8
  from ophyd_async.core import (
9
- AutoIncrementingPathProvider,
10
- StaticFilenameProvider,
11
9
  TriggerInfo,
12
10
  WatchableAsyncStatus,
13
11
  )
12
+ from ophyd_async.fastcs.jungfrau import GainMode
14
13
 
15
14
  from mx_bluesky.common.utils.log import LOGGER
16
15
 
@@ -20,8 +19,11 @@ JF_COMPLETE_GROUP = "JF complete"
20
19
  def fly_jungfrau(
21
20
  jungfrau: CommissioningJungfrau,
22
21
  trigger_info: TriggerInfo,
22
+ gain_mode: GainMode,
23
23
  wait: bool = False,
24
24
  log_on_percentage_prefix="Jungfrau data collection triggers received",
25
+ read_hardware_after_prepare_plan: Callable[..., MsgGenerator]
26
+ | None = None, # Param needs refactor: https://github.com/DiamondLightSource/mx-bluesky/issues/819
25
27
  ) -> MsgGenerator[WatchableAsyncStatus]:
26
28
  """Stage, prepare, and kickoff Jungfrau with a configured TriggerInfo. Optionally wait
27
29
  for completion.
@@ -32,13 +34,21 @@ def fly_jungfrau(
32
34
  Args:
33
35
  jungfrau: Jungfrau device.
34
36
  trigger_info: TriggerInfo which should be acquired using jungfrau util functions.
37
+ gain_mode: Which gain mode to put the Jungfrau into before starting the acquisition.
35
38
  wait: Optionally block until data collection is complete.
36
39
  log_on_percentage_prefix: String that will be appended to the "percentage completion" logging message.
40
+ read_hardware_after_prepare_plan: Optionally add a plan which will be ran in between preparing the jungfrau and starting
41
+ acquisition. This is useful for reading devices after they have been prepared, especially since the file writing path
42
+ is calculated during prepare.
37
43
  """
38
44
 
45
+ LOGGER.info(f"Setting Jungfrau to gain mode {gain_mode}")
46
+ yield from bps.mv(jungfrau.drv.gain_mode, gain_mode)
39
47
  LOGGER.info("Preparing detector...")
40
48
  yield from bps.prepare(jungfrau, trigger_info, wait=True)
41
- LOGGER.info("Detector prepared. Starting acquisition")
49
+ LOGGER.info("Detector prepared")
50
+ if read_hardware_after_prepare_plan:
51
+ yield from read_hardware_after_prepare_plan()
42
52
  yield from bps.kickoff(jungfrau, wait=True)
43
53
  LOGGER.info("Waiting for acquisition to complete...")
44
54
  status = yield from bps.complete(jungfrau, group=JF_COMPLETE_GROUP)
@@ -50,17 +60,3 @@ def fly_jungfrau(
50
60
  if wait:
51
61
  yield from bps.wait(JF_COMPLETE_GROUP)
52
62
  return status
53
-
54
-
55
- def override_file_path(jungfrau: CommissioningJungfrau, path_of_output_file: str):
56
- """While we should generally use device instantiation to set the path,
57
- during commissioning, it is useful to be able to explicitly set the filename
58
- and path.
59
-
60
- This function must be called before the Jungfrau is prepared.
61
- """
62
- _file_path = PurePath(path_of_output_file)
63
- _new_filename_provider = StaticFilenameProvider(_file_path.name)
64
- jungfrau._writer._path_info = AutoIncrementingPathProvider( # noqa: SLF001
65
- _new_filename_provider, _file_path.parent
66
- )
File without changes
@@ -0,0 +1,9 @@
1
+ from pydantic.dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class PlanNameConstants:
6
+ ROTATION_DEVICE_READ = "ROTATION DEVICE READ"
7
+ SINGLE_ROTATION_SCAN = "OUTER SINGLE ROTATION SCAN"
8
+ MULTI_ROTATION_SCAN = "OUTER MULTI ROTATION SCAN"
9
+ ROTATION_MAIN = "ROTATION MAIN"
@@ -113,7 +113,7 @@ def laser_check(
113
113
  """
114
114
  SSX_LOGGER.debug(f"Laser check: {mode}")
115
115
 
116
- laser_ttl = zebra.mapping.outputs.TTL_PILATUS # Update with dodal changes
116
+ laser_ttl = zebra.mapping.outputs.TTL_JUNGFRAU
117
117
 
118
118
  if mode == "laseron":
119
119
  yield from bps.abs_set(
@@ -222,6 +222,6 @@ def start_viewer(oav: OAV, pmac: PMAC, run_engine: RunEngine, oav1: str = OAV1_C
222
222
  if __name__ == "__main__":
223
223
  run_engine = RunEngine(call_returns_result=True)
224
224
  # Get devices out of dodal
225
- oav: OAV = i24.oav(connect_immediately=True)
226
- pmac: PMAC = i24.pmac(connect_immediately=True)
225
+ oav: OAV = i24.oav.build(connect_immediately=True)
226
+ pmac: PMAC = i24.pmac.build(connect_immediately=True)
227
227
  start_viewer(oav, pmac, run_engine)
@@ -5,7 +5,7 @@ import bluesky.preprocessors as bpp
5
5
  from bluesky.utils import MsgGenerator
6
6
  from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue
7
7
  from dodal.devices.motors import XYZStage
8
- from dodal.devices.robot import BartRobot
8
+ from dodal.devices.robot import SAMPLE_LOCATION_EMPTY, BartRobot
9
9
  from dodal.devices.smargon import CombinedMove, Smargon, StubPosition
10
10
  from dodal.plan_stubs.motor_utils import MoveTooLargeError, home_and_reset_wrapper
11
11
 
@@ -14,27 +14,6 @@ from mx_bluesky.common.parameters.constants import (
14
14
  HardwareConstants,
15
15
  PlanNameConstants,
16
16
  )
17
- from mx_bluesky.common.utils.log import LOGGER
18
-
19
- SLEEP_PER_CHECK = 0.1
20
-
21
-
22
- def wait_for_smargon_not_disabled(smargon: Smargon, timeout=60):
23
- """Waits for the smargon disabled flag to go low. The robot hardware is responsible
24
- for setting this to low when it is safe to move. It does this through a physical
25
- connection between the robot and the smargon.
26
- """
27
- LOGGER.info("Waiting for smargon enabled")
28
- times_to_check = int(timeout / SLEEP_PER_CHECK)
29
- for _ in range(times_to_check):
30
- smargon_disabled = yield from bps.rd(smargon.disabled)
31
- if not smargon_disabled:
32
- LOGGER.info("Smargon now enabled")
33
- return
34
- yield from bps.sleep(SLEEP_PER_CHECK)
35
- raise TimeoutError(
36
- "Timed out waiting for smargon to become enabled after robot load"
37
- )
38
17
 
39
18
 
40
19
  def _raise_exception_if_moved_out_of_cryojet(exception):
@@ -117,8 +96,7 @@ def robot_unload(
117
96
  yield from bps.save()
118
97
 
119
98
  def _unload():
120
- yield from bps.abs_set(robot, None, wait=True)
121
- yield from wait_for_smargon_not_disabled(smargon)
99
+ yield from bps.abs_set(robot, SAMPLE_LOCATION_EMPTY, wait=True)
122
100
 
123
101
  gonio_finished = yield from do_plan_while_lower_gonio_at_home(
124
102
  _unload(), lower_gonio
@@ -14,7 +14,10 @@ from dodal.devices.zebra.zebra_controlled_shutter import (
14
14
  ZebraShutterControl,
15
15
  )
16
16
 
17
- from mx_bluesky.common.parameters.constants import ZEBRA_STATUS_TIMEOUT
17
+ from mx_bluesky.common.parameters.constants import (
18
+ ZEBRA_STATUS_TIMEOUT,
19
+ PlanGroupCheckpointConstants,
20
+ )
18
21
  from mx_bluesky.common.utils.log import LOGGER
19
22
 
20
23
  """Plans in this file will work as intended if the zebra has the following configuration:
@@ -154,7 +157,7 @@ def setup_zebra_for_rotation(
154
157
  shutter_opening_deg: float = 2.5,
155
158
  shutter_opening_s: float = 0.04,
156
159
  direction: RotationDirection = RotationDirection.POSITIVE,
157
- group: str = "setup_zebra_for_rotation",
160
+ group: str = PlanGroupCheckpointConstants.SETUP_ZEBRA_FOR_ROTATION,
158
161
  wait: bool = True,
159
162
  ttl_input_for_detector_to_use: int | None = None,
160
163
  ):
@@ -154,7 +154,7 @@ def common_flyscan_xray_centre(
154
154
 
155
155
  This plan will also push data to ispyb when used with the ispyb_activation_decorator.
156
156
 
157
- There are a few other useful decorators to use with this plan, see: verify_undulator_gap_before_run_decorator, transmission_and_xbpm_feedback_for_collection_decorator
157
+ There are a few other useful decorators to use with this plan, see: verify_undulator_gap_before_run_decorator, common/preprocessors/preprocessors.py
158
158
  """
159
159
 
160
160
  def _overall_tidy():
@@ -76,6 +76,7 @@ def standard_read_hardware_during_collection(
76
76
  detector.bit_depth,
77
77
  beamsize,
78
78
  detector.cam.roi_mode,
79
+ detector.ispyb_detector_id,
79
80
  ]
80
81
  yield from read_hardware_plan(
81
82
  signals_to_read_during_collection, DocDescriptorNames.HARDWARE_READ_DURING
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+
5
+ from dodal.devices.zebra.zebra import RotationDirection
6
+ from dodal.utils import get_beamline_name
7
+
8
+ from mx_bluesky.common.parameters.constants import RotationParamConstants
9
+ from mx_bluesky.common.parameters.rotation import SingleRotationScan
10
+ from mx_bluesky.common.utils.log import LOGGER
11
+
12
+ DEFAULT_DIRECTION = RotationDirection.NEGATIVE
13
+ DEFAULT_MAX_VELOCITY = 120
14
+ # Use a slightly larger time to acceleration than EPICS as it's better to be cautious
15
+ ACCELERATION_MARGIN = 1.5
16
+
17
+
18
+ @dataclasses.dataclass
19
+ class RotationMotionProfile:
20
+ start_scan_deg: float
21
+ start_motion_deg: float
22
+ scan_width_deg: float
23
+ shutter_time_s: float
24
+ direction: RotationDirection
25
+ speed_for_rotation_deg_s: float
26
+ acceleration_offset_deg: float
27
+ shutter_opening_deg: float
28
+ total_exposure_s: float
29
+ distance_to_move_deg: float
30
+ max_velocity_deg_s: float
31
+
32
+
33
+ def calculate_motion_profile(
34
+ params: SingleRotationScan,
35
+ motor_time_to_speed_s: float,
36
+ max_velocity_deg_s: float,
37
+ ) -> RotationMotionProfile:
38
+ """Calculates the various numbers needed for motions in the rotation scan.
39
+ Rotates through "scan width" plus twice an "offset" to take into account
40
+ acceleration at the start and deceleration at the end, plus the number of extra
41
+ degrees of rotation needed to make sure the fast shutter has fully opened before the
42
+ detector trigger is sent.
43
+ See https://github.com/DiamondLightSource/hyperion/wiki/rotation-scan-geometry
44
+ for a simple pictorial explanation."""
45
+
46
+ assert params.rotation_increment_deg > 0
47
+
48
+ direction = params.rotation_direction
49
+ start_scan_deg = params.omega_start_deg
50
+
51
+ if RotationParamConstants.OMEGA_FLIP:
52
+ # If omega_flip is True then the motor omega axis is inverted with respect to the
53
+ # coordinate system.
54
+ start_scan_deg = -start_scan_deg
55
+ direction = (
56
+ direction.POSITIVE
57
+ if direction == direction.NEGATIVE
58
+ else direction.NEGATIVE
59
+ )
60
+
61
+ num_images = params.num_images
62
+ shutter_time_s = params.shutter_opening_time_s
63
+ image_width_deg = params.rotation_increment_deg
64
+ exposure_time_s = params.exposure_time_s
65
+ motor_time_to_speed_s *= ACCELERATION_MARGIN
66
+
67
+ LOGGER.info("Calculating rotation scan motion profile:")
68
+ LOGGER.info(
69
+ f"{num_images=}, {shutter_time_s=}, {image_width_deg=}, {exposure_time_s=}, {direction=}"
70
+ )
71
+
72
+ scan_width_deg = num_images * params.rotation_increment_deg
73
+ LOGGER.info(f"{scan_width_deg=} = {num_images=} * {params.rotation_increment_deg=}")
74
+
75
+ speed_for_rotation_deg_s = image_width_deg / exposure_time_s
76
+ LOGGER.info("speed_for_rotation_deg_s = image_width_deg / exposure_time_s")
77
+ LOGGER.info(
78
+ f"{speed_for_rotation_deg_s=} = {image_width_deg=} / {exposure_time_s=}"
79
+ )
80
+
81
+ acceleration_offset_deg = motor_time_to_speed_s * speed_for_rotation_deg_s
82
+ LOGGER.info(
83
+ f"{acceleration_offset_deg=} = {motor_time_to_speed_s=} * {speed_for_rotation_deg_s=}"
84
+ )
85
+
86
+ start_motion_deg = start_scan_deg - (acceleration_offset_deg * direction.multiplier)
87
+ LOGGER.info(
88
+ f"{start_motion_deg=} = {start_scan_deg=} - ({acceleration_offset_deg=} * {direction.multiplier=})"
89
+ )
90
+
91
+ shutter_opening_deg = speed_for_rotation_deg_s * shutter_time_s
92
+ LOGGER.info(
93
+ f"{shutter_opening_deg=} = {speed_for_rotation_deg_s=} * {shutter_time_s=}"
94
+ )
95
+
96
+ shutter_opening_deg = speed_for_rotation_deg_s * shutter_time_s
97
+ LOGGER.info(
98
+ f"{shutter_opening_deg=} = {speed_for_rotation_deg_s=} * {shutter_time_s=}"
99
+ )
100
+
101
+ total_exposure_s = num_images * exposure_time_s
102
+ LOGGER.info(f"{total_exposure_s=} = {num_images=} * {exposure_time_s=}")
103
+
104
+ distance_to_move_deg = (
105
+ scan_width_deg + shutter_opening_deg + acceleration_offset_deg * 2
106
+ ) * direction.multiplier
107
+ LOGGER.info(
108
+ f"{distance_to_move_deg=} = ({scan_width_deg=} + {shutter_opening_deg=} + {acceleration_offset_deg=} * 2) * {direction=})"
109
+ )
110
+
111
+ # See https://github.com/DiamondLightSource/mx-bluesky/issues/1224
112
+ if get_beamline_name("i03") == "i24":
113
+ acceleration_offset_deg = 10
114
+
115
+ return RotationMotionProfile(
116
+ start_scan_deg=start_scan_deg,
117
+ start_motion_deg=start_motion_deg,
118
+ scan_width_deg=scan_width_deg,
119
+ shutter_time_s=shutter_time_s,
120
+ direction=direction,
121
+ speed_for_rotation_deg_s=speed_for_rotation_deg_s,
122
+ acceleration_offset_deg=acceleration_offset_deg,
123
+ shutter_opening_deg=shutter_opening_deg,
124
+ total_exposure_s=total_exposure_s,
125
+ distance_to_move_deg=distance_to_move_deg,
126
+ max_velocity_deg_s=max_velocity_deg_s,
127
+ )
@@ -23,7 +23,7 @@ from mx_bluesky.common.external_interaction.ispyb.ispyb_store import (
23
23
  )
24
24
  from mx_bluesky.common.external_interaction.ispyb.ispyb_utils import get_ispyb_config
25
25
  from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample
26
- from mx_bluesky.common.parameters.constants import DocDescriptorNames
26
+ from mx_bluesky.common.parameters.constants import USE_NUMTRACKER, DocDescriptorNames
27
27
  from mx_bluesky.common.utils.log import (
28
28
  ISPYB_ZOCALO_CALLBACK_LOGGER,
29
29
  format_doc_for_log,
@@ -86,6 +86,17 @@ class BaseISPyBCallback(PlanReactiveCallback):
86
86
 
87
87
  def activity_gated_start(self, doc: RunStart):
88
88
  self._oav_snapshot_event_idx = 0
89
+
90
+ if self.params and self.params.visit == USE_NUMTRACKER:
91
+ try:
92
+ visit = doc.get("instrument_session")
93
+ assert isinstance(visit, str)
94
+ self.params.visit = visit
95
+ except Exception as e:
96
+ raise ValueError(
97
+ f"Error trying to retrieve instrument session from document {doc}"
98
+ ) from e
99
+
89
100
  return self.tag_doc(doc)
90
101
 
91
102
  def activity_gated_descriptor(self, doc: EventDescriptor):
@@ -124,7 +135,6 @@ class BaseISPyBCallback(PlanReactiveCallback):
124
135
  )
125
136
  synchrotron_mode = doc["data"]["synchrotron-synchrotron_mode"]
126
137
  assert isinstance(synchrotron_mode, SynchrotronMode)
127
-
128
138
  hwscan_data_collection_info = DataCollectionInfo(
129
139
  undulator_gap1=doc["data"]["undulator-current_gap"],
130
140
  synchrotron_mode=synchrotron_mode.value,
@@ -159,6 +169,7 @@ class BaseISPyBCallback(PlanReactiveCallback):
159
169
  beamsize_at_sampley=beamsize_y_mm,
160
170
  flux=doc["data"]["flux-flux_reading"],
161
171
  detector_mode="ROI" if doc["data"]["eiger_cam_roi_mode"] else "FULL",
172
+ ispyb_detector_id=doc["data"]["eiger-ispyb_detector_id"],
162
173
  )
163
174
  if transmission := doc["data"]["attenuator-actual_transmission"]:
164
175
  # Ispyb wants the transmission in a percentage, we use fractions
@@ -9,7 +9,6 @@ from mx_bluesky.common.external_interaction.ispyb.ispyb_utils import (
9
9
  )
10
10
  from mx_bluesky.common.parameters.components import DiffractionExperimentWithSample
11
11
 
12
- I03_EIGER_DETECTOR = 78
13
12
  EIGER_FILE_SUFFIX = "h5"
14
13
 
15
14
 
@@ -31,7 +30,6 @@ def populate_remaining_data_collection_info(
31
30
  data_collection_info.sample_id = params.sample_id
32
31
  data_collection_info.visit_string = params.visit
33
32
  data_collection_info.parent_id = data_collection_group_id
34
- data_collection_info.detector_id = I03_EIGER_DETECTOR
35
33
  data_collection_info.comments = comment
36
34
  data_collection_info.detector_distance = params.detector_params.detector_distance
37
35
  data_collection_info.exp_time = params.detector_params.exposure_time_s
@@ -40,7 +40,7 @@ class DataCollectionInfo:
40
40
  kappa_start: float | None = None
41
41
 
42
42
  visit_string: str | None = None
43
- detector_id: int | None = None
43
+ ispyb_detector_id: int | None = None
44
44
  axis_start: float | None = None
45
45
  slitgap_vertical: float | None = None
46
46
  slitgap_horizontal: float | None = None
@@ -282,7 +282,7 @@ def _data_collection_info_to_json(data: DataCollectionInfo) -> dict:
282
282
  "axisEnd": data.axis_end,
283
283
  "chiStart": data.chi_start,
284
284
  "kappaStart": data.kappa_start,
285
- "detectorId": data.detector_id,
285
+ "detectorId": data.ispyb_detector_id,
286
286
  "axisStart": data.axis_start,
287
287
  "slitGapVertical": data.slitgap_vertical,
288
288
  "slitGapHorizontal": data.slitgap_horizontal,
@@ -25,6 +25,7 @@ from semver import Version
25
25
 
26
26
  from mx_bluesky.common.parameters.constants import (
27
27
  TEST_MODE,
28
+ USE_NUMTRACKER,
28
29
  DetectorParamConstants,
29
30
  GridscanParamConstants,
30
31
  )
@@ -32,6 +33,10 @@ from mx_bluesky.common.parameters.constants import (
32
33
  PARAMETER_VERSION = Version.parse("5.3.0")
33
34
 
34
35
 
36
+ def get_param_version() -> SemanticVersion:
37
+ return SemanticVersion.validate_from_str(str(PARAMETER_VERSION))
38
+
39
+
35
40
  class RotationAxis(StrEnum):
36
41
  OMEGA = "omega"
37
42
  PHI = "phi"
@@ -152,8 +157,8 @@ class WithVisit(BaseModel):
152
157
  det_dist_to_beam_converter_path: str = Field(
153
158
  default=DetectorParamConstants.BEAM_XY_LUT_PATH
154
159
  )
155
- insertion_prefix: str = "SR03S" if TEST_MODE else "SR03I"
156
160
  detector_distance_mm: float | None = Field(default=None, gt=0)
161
+ insertion_prefix: str = "SR03S" if TEST_MODE else "SR03I"
157
162
 
158
163
 
159
164
  class DiffractionExperiment(
@@ -176,12 +181,17 @@ class DiffractionExperiment(
176
181
  @model_validator(mode="before")
177
182
  @classmethod
178
183
  def validate_directories(cls, values):
179
- os.makedirs(values["storage_directory"], exist_ok=True)
180
-
181
- values["snapshot_directory"] = values.get(
182
- "snapshot_directory",
183
- Path(values["storage_directory"], "snapshots").as_posix(),
184
- )
184
+ # Plans using numtracker currently won't work with snapshot directories:
185
+ # see https://github.com/DiamondLightSource/mx-bluesky/issues/1527
186
+ if values["storage_directory"] != USE_NUMTRACKER:
187
+ os.makedirs(values["storage_directory"], exist_ok=True)
188
+
189
+ values["snapshot_directory"] = values.get(
190
+ "snapshot_directory",
191
+ Path(values["storage_directory"], "snapshots").as_posix(),
192
+ )
193
+ else:
194
+ values["snapshot_directory"] = Path("/tmp")
185
195
  return values
186
196
 
187
197
  @property
@@ -9,6 +9,9 @@ from pydantic.dataclasses import dataclass
9
9
 
10
10
  from mx_bluesky.definitions import ROOT_DIR
11
11
 
12
+ # Use as visit if numtracker is being used
13
+ USE_NUMTRACKER = "from numtracker"
14
+
12
15
  BEAMLINE = get_beamline_name("test")
13
16
  TEST_MODE = BEAMLINE == "test"
14
17
  ZEBRA_STATUS_TIMEOUT = 30
@@ -111,6 +114,8 @@ class GridscanParamConstants:
111
114
  @dataclass(frozen=True)
112
115
  class RotationParamConstants:
113
116
  DEFAULT_APERTURE_POSITION = ApertureValue.LARGE
117
+ DEFAULT_SHUTTER_TIME_S = 0.06
118
+ OMEGA_FLIP = True # See https://github.com/DiamondLightSource/mx-bluesky/issues/1223 to make beamline-specific
114
119
 
115
120
 
116
121
  @dataclass(frozen=True)
@@ -138,6 +143,7 @@ class PlanGroupCheckpointConstants:
138
143
  MOVE_GONIO_TO_START = "move_gonio_to_start"
139
144
  READY_FOR_OAV = "ready_for_oav"
140
145
  PREPARE_APERTURE = "prepare_aperture"
146
+ SETUP_ZEBRA_FOR_ROTATION = "setup_zebra_for_rotation"
141
147
 
142
148
 
143
149
  # Eventually replace below with https://github.com/DiamondLightSource/mx-bluesky/issues/798
@@ -28,9 +28,9 @@ from mx_bluesky.common.parameters.components import (
28
28
  WithSample,
29
29
  WithScan,
30
30
  )
31
- from mx_bluesky.hyperion.parameters.constants import (
32
- CONST,
33
- I03Constants,
31
+ from mx_bluesky.common.parameters.constants import (
32
+ DetectorParamConstants,
33
+ RotationParamConstants,
34
34
  )
35
35
 
36
36
 
@@ -56,7 +56,9 @@ class RotationScanPerSweep(OptionalGonioAngleStarts, OptionalXyzStarts, WithSamp
56
56
 
57
57
 
58
58
  class RotationExperiment(DiffractionExperiment):
59
- shutter_opening_time_s: float = Field(default=CONST.I03.SHUTTER_TIME_S)
59
+ shutter_opening_time_s: float = Field(
60
+ default=RotationParamConstants.DEFAULT_SHUTTER_TIME_S
61
+ )
60
62
  rotation_increment_deg: float = Field(default=0.1, gt=0)
61
63
  ispyb_experiment_type: IspybExperimentType = Field(
62
64
  default=IspybExperimentType.ROTATION
@@ -67,7 +69,7 @@ class RotationExperiment(DiffractionExperiment):
67
69
  ) -> DetectorParams:
68
70
  self.det_dist_to_beam_converter_path = (
69
71
  self.det_dist_to_beam_converter_path
70
- or CONST.PARAM.DETECTOR.BEAM_XY_LUT_PATH
72
+ or DetectorParamConstants.BEAM_XY_LUT_PATH
71
73
  )
72
74
  optional_args = {}
73
75
  if self.run_number:
@@ -75,7 +77,7 @@ class RotationExperiment(DiffractionExperiment):
75
77
  assert self.detector_distance_mm is not None
76
78
  os.makedirs(self.storage_directory, exist_ok=True)
77
79
  return DetectorParams(
78
- detector_size_constants=I03Constants.DETECTOR,
80
+ detector_size_constants=DetectorParamConstants.DETECTOR,
79
81
  expected_energy_ev=self.demand_energy_ev,
80
82
  exposure_time_s=self.exposure_time_s,
81
83
  directory=self.storage_directory,
@@ -97,7 +99,7 @@ class RotationExperiment(DiffractionExperiment):
97
99
  @classmethod
98
100
  def _set_default_aperture_position(cls, aperture_position: ApertureValue | None):
99
101
  if not aperture_position:
100
- default_aperture = CONST.PARAM.ROTATION.DEFAULT_APERTURE_POSITION
102
+ default_aperture = RotationParamConstants.DEFAULT_APERTURE_POSITION
101
103
  LOGGER.warning(
102
104
  f"No aperture position selected. Defaulting to {default_aperture}"
103
105
  )
@@ -107,7 +109,7 @@ class RotationExperiment(DiffractionExperiment):
107
109
 
108
110
 
109
111
  class SingleRotationScan(
110
- WithScan, RotationScanPerSweep, RotationExperiment, DiffractionExperimentWithSample
112
+ WithScan, RotationExperiment, RotationScanPerSweep, DiffractionExperimentWithSample
111
113
  ):
112
114
  @property
113
115
  def detector_params(self):