mx-bluesky 1.4.7__py3-none-any.whl → 1.4.9__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 (89) hide show
  1. mx_bluesky/_version.py +2 -2
  2. mx_bluesky/beamlines/aithre_lasershaping/__init__.py +8 -0
  3. mx_bluesky/beamlines/aithre_lasershaping/beamline_safe.py +36 -0
  4. mx_bluesky/beamlines/aithre_lasershaping/goniometer_controls.py +43 -0
  5. mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +4 -4
  6. mx_bluesky/beamlines/i04/thawing_plan.py +8 -2
  7. mx_bluesky/beamlines/i23/__init__.py +3 -0
  8. mx_bluesky/beamlines/i23/serial.py +71 -0
  9. mx_bluesky/beamlines/i24/serial/__init__.py +2 -0
  10. mx_bluesky/beamlines/i24/serial/blueapi_config.yaml +2 -1
  11. mx_bluesky/beamlines/i24/serial/dcid.py +5 -5
  12. mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DetStage.edl +2 -2
  13. mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +9 -9
  14. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +25 -5
  15. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DetStage.edl +2 -2
  16. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +14 -14
  17. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/pumpprobe-py3v1.edl +5 -5
  18. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +29 -60
  19. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +7 -1
  20. mx_bluesky/beamlines/i24/serial/log.py +9 -10
  21. mx_bluesky/beamlines/i24/serial/parameters/utils.py +36 -7
  22. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +0 -1
  23. mx_bluesky/beamlines/i24/serial/setup_beamline/pv_abstract.py +4 -4
  24. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +4 -12
  25. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_detector.py +2 -1
  26. mx_bluesky/beamlines/i24/serial/web_gui_plans/general_plans.py +71 -11
  27. mx_bluesky/beamlines/i24/serial/write_nexus.py +3 -3
  28. mx_bluesky/{hyperion → common}/device_setup_plans/manipulate_sample.py +6 -14
  29. mx_bluesky/{hyperion → common}/device_setup_plans/setup_oav.py +12 -6
  30. mx_bluesky/{hyperion → common}/experiment_plans/change_aperture_then_move_plan.py +4 -5
  31. mx_bluesky/{hyperion → common}/experiment_plans/oav_grid_detection_plan.py +6 -6
  32. mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +6 -5
  33. mx_bluesky/common/external_interaction/callbacks/xray_centre/ispyb_callback.py +16 -47
  34. mx_bluesky/common/external_interaction/ispyb/ispyb_store.py +4 -1
  35. mx_bluesky/common/external_interaction/ispyb/ispyb_utils.py +4 -4
  36. mx_bluesky/common/external_interaction/nexus/nexus_utils.py +2 -2
  37. mx_bluesky/common/parameters/components.py +22 -2
  38. mx_bluesky/common/parameters/constants.py +4 -16
  39. mx_bluesky/common/parameters/gridscan.py +36 -32
  40. mx_bluesky/common/plans/common_flyscan_xray_centre_plan.py +316 -0
  41. mx_bluesky/common/plans/inner_plans/__init__ .py +0 -0
  42. mx_bluesky/common/plans/read_hardware.py +3 -3
  43. mx_bluesky/common/utils/log.py +19 -15
  44. mx_bluesky/hyperion/__main__.py +6 -24
  45. mx_bluesky/hyperion/baton_handler.py +8 -3
  46. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +4 -4
  47. mx_bluesky/hyperion/device_setup_plans/setup_zebra.py +0 -33
  48. mx_bluesky/hyperion/device_setup_plans/smargon.py +2 -7
  49. mx_bluesky/hyperion/device_setup_plans/utils.py +6 -5
  50. mx_bluesky/hyperion/experiment_plans/__init__.py +1 -7
  51. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +3 -13
  52. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +80 -87
  53. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +183 -0
  54. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +50 -15
  55. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +31 -7
  56. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +4 -4
  57. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +1 -1
  58. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +13 -14
  59. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +9 -8
  60. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +30 -71
  61. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +2 -2
  62. mx_bluesky/hyperion/external_interaction/agamemnon.py +78 -80
  63. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +8 -6
  64. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +3 -3
  65. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +6 -3
  66. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +2 -2
  67. mx_bluesky/hyperion/external_interaction/callbacks/snapshot_callback.py +183 -31
  68. mx_bluesky/hyperion/external_interaction/config_server.py +4 -1
  69. mx_bluesky/hyperion/parameters/cli.py +4 -19
  70. mx_bluesky/hyperion/parameters/constants.py +1 -5
  71. mx_bluesky/hyperion/parameters/device_composites.py +40 -5
  72. mx_bluesky/hyperion/parameters/gridscan.py +9 -58
  73. mx_bluesky/hyperion/parameters/load_centre_collect.py +4 -4
  74. mx_bluesky/hyperion/parameters/rotation.py +9 -12
  75. mx_bluesky/hyperion/utils/context.py +2 -2
  76. mx_bluesky/hyperion/utils/validation.py +15 -19
  77. {mx_bluesky-1.4.7.dist-info → mx_bluesky-1.4.9.dist-info}/METADATA +7 -6
  78. {mx_bluesky-1.4.7.dist-info → mx_bluesky-1.4.9.dist-info}/RECORD +86 -83
  79. {mx_bluesky-1.4.7.dist-info → mx_bluesky-1.4.9.dist-info}/WHEEL +1 -1
  80. mx_bluesky/common/external_interaction/test_config_server.py +0 -38
  81. mx_bluesky/hyperion/device_setup_plans/check_beamstop.py +0 -27
  82. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +0 -467
  83. /mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/{short1-laser.png → s1l.png} +0 -0
  84. /mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/{short2-laser.png → s2l.png} +0 -0
  85. /mx_bluesky/{hyperion → common}/device_setup_plans/position_detector.py +0 -0
  86. /mx_bluesky/common/plans/{do_fgs.py → inner_plans/do_fgs.py} +0 -0
  87. {mx_bluesky-1.4.7.dist-info → mx_bluesky-1.4.9.dist-info}/entry_points.txt +0 -0
  88. {mx_bluesky-1.4.7.dist-info → mx_bluesky-1.4.9.dist-info}/licenses/LICENSE +0 -0
  89. {mx_bluesky-1.4.7.dist-info → mx_bluesky-1.4.9.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,8 @@
1
1
  import logging
2
2
  from collections.abc import Callable, Sequence
3
3
  from threading import Thread
4
+ from time import sleep # noqa
4
5
 
5
- import bluesky.plan_stubs as bps
6
6
  from bluesky.callbacks import CallbackBase
7
7
  from bluesky.callbacks.zmq import Proxy, RemoteDispatcher
8
8
  from dodal.log import LOGGER as dodal_logger
@@ -26,7 +26,7 @@ from mx_bluesky.common.external_interaction.callbacks.xray_centre.nexus_callback
26
26
  from mx_bluesky.common.utils.log import (
27
27
  ISPYB_ZOCALO_CALLBACK_LOGGER,
28
28
  NEXUS_LOGGER,
29
- _get_logging_dir,
29
+ _get_logging_dirs,
30
30
  tag_filter,
31
31
  )
32
32
  from mx_bluesky.hyperion.external_interaction.callbacks.robot_load.ispyb_callback import (
@@ -93,14 +93,16 @@ def setup_logging(dev_mode: bool):
93
93
  (ISPYB_ZOCALO_CALLBACK_LOGGER, "hyperion_ispyb_callback.log"),
94
94
  (NEXUS_LOGGER, "hyperion_nexus_callback.log"),
95
95
  ]:
96
+ logging_path, debug_logging_path = _get_logging_dirs(dev_mode)
96
97
  if logger.handlers == []:
97
98
  handlers = set_up_all_logging_handlers(
98
99
  logger,
99
- _get_logging_dir(),
100
+ logging_path,
100
101
  filename,
101
102
  dev_mode,
102
- error_log_buffer_lines=ERROR_LOG_BUFFER_LINES,
103
- graylog_port=CONST.GRAYLOG_PORT,
103
+ ERROR_LOG_BUFFER_LINES,
104
+ CONST.GRAYLOG_PORT,
105
+ debug_logging_path,
104
106
  )
105
107
  handlers["graylog_handler"].addFilter(tag_filter)
106
108
  log_info(f"Loggers initialised with dev_mode={dev_mode}")
@@ -140,7 +142,7 @@ def wait_for_threads_forever(threads: Sequence[Thread]):
140
142
  try:
141
143
  log_debug("Trying to wait forever on callback and dispatcher threads")
142
144
  while all(alive):
143
- yield from bps.sleep(LIVENESS_POLL_SECONDS)
145
+ sleep(LIVENESS_POLL_SECONDS)
144
146
  alive = [t.is_alive() for t in threads]
145
147
  except KeyboardInterrupt:
146
148
  log_info("Main thread received interrupt - exiting.")
@@ -25,7 +25,7 @@ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_mapping i
25
25
  populate_data_collection_info_for_rotation,
26
26
  )
27
27
  from mx_bluesky.hyperion.parameters.constants import CONST
28
- from mx_bluesky.hyperion.parameters.rotation import RotationScan
28
+ from mx_bluesky.hyperion.parameters.rotation import SingleRotationScan
29
29
 
30
30
  if TYPE_CHECKING:
31
31
  from event_model.documents import Event, RunStart, RunStop
@@ -62,7 +62,7 @@ class RotationISPyBCallback(BaseISPyBCallback):
62
62
  )
63
63
  hyperion_params = doc.get("mx_bluesky_parameters")
64
64
  assert isinstance(hyperion_params, str)
65
- self.params = RotationScan.model_validate_json(hyperion_params)
65
+ self.params = SingleRotationScan.model_validate_json(hyperion_params)
66
66
  dcgid = (
67
67
  self.ispyb_ids.data_collection_group_id
68
68
  if (self.params.sample_id == self.last_sample_id)
@@ -86,7 +86,7 @@ class RotationISPyBCallback(BaseISPyBCallback):
86
86
  ISPYB_ZOCALO_CALLBACK_LOGGER.info("Beginning ispyb deposition")
87
87
  data_collection_group_info = populate_data_collection_group(self.params)
88
88
  data_collection_info = populate_data_collection_info_for_rotation(
89
- cast(RotationScan, self.params)
89
+ cast(SingleRotationScan, self.params)
90
90
  )
91
91
  data_collection_info = populate_remaining_data_collection_info(
92
92
  self.params.comment,
@@ -1,17 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from mx_bluesky.common.external_interaction.ispyb.data_model import DataCollectionInfo
4
- from mx_bluesky.hyperion.parameters.rotation import RotationScan
4
+ from mx_bluesky.hyperion.parameters.rotation import SingleRotationScan
5
5
 
6
6
 
7
- def populate_data_collection_info_for_rotation(params: RotationScan):
7
+ def populate_data_collection_info_for_rotation(params: SingleRotationScan):
8
8
  info = DataCollectionInfo(
9
9
  omega_start=params.omega_start_deg,
10
10
  data_collection_number=params.detector_params.run_number, # type:ignore # the validator always makes this int
11
11
  n_images=params.num_images,
12
12
  axis_range=params.rotation_increment_deg,
13
13
  axis_start=params.omega_start_deg,
14
- axis_end=(params.omega_start_deg + params.scan_width_deg),
14
+ axis_end=(
15
+ params.omega_start_deg
16
+ + params.scan_width_deg * params.rotation_direction.multiplier
17
+ ),
15
18
  kappa_start=params.kappa_start_deg,
16
19
  )
17
20
  return info
@@ -16,7 +16,7 @@ from mx_bluesky.common.external_interaction.nexus.nexus_utils import (
16
16
  from mx_bluesky.common.external_interaction.nexus.write_nexus import NexusWriter
17
17
  from mx_bluesky.common.utils.log import NEXUS_LOGGER
18
18
  from mx_bluesky.hyperion.parameters.constants import CONST
19
- from mx_bluesky.hyperion.parameters.rotation import RotationScan
19
+ from mx_bluesky.hyperion.parameters.rotation import SingleRotationScan
20
20
 
21
21
  if TYPE_CHECKING:
22
22
  from event_model.documents import Event, EventDescriptor, RunStart
@@ -85,7 +85,7 @@ class RotationNexusFileCallback(PlanReactiveCallback):
85
85
  NEXUS_LOGGER.info(
86
86
  f"Nexus writer received start document with experiment parameters {hyperion_params}"
87
87
  )
88
- parameters = RotationScan.model_validate_json(hyperion_params)
88
+ parameters = SingleRotationScan.model_validate_json(hyperion_params)
89
89
  NEXUS_LOGGER.info("Setting up nexus file...")
90
90
 
91
91
  det_size = (
@@ -1,4 +1,9 @@
1
+ import dataclasses
1
2
  import re
3
+ from collections.abc import Iterator
4
+ from datetime import datetime
5
+ from math import cos, radians, sin
6
+ from pathlib import Path
2
7
 
3
8
  from dodal.devices.oav.snapshots.snapshot_image_processing import (
4
9
  compute_beam_centre_pixel_xy_for_mm_position,
@@ -10,17 +15,35 @@ from PIL import Image
10
15
  from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import (
11
16
  PlanReactiveCallback,
12
17
  )
13
- from mx_bluesky.common.parameters.constants import DocDescriptorNames
18
+ from mx_bluesky.common.parameters.components import WithSnapshot
19
+ from mx_bluesky.common.parameters.constants import DocDescriptorNames, PlanNameConstants
14
20
  from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER as CALLBACK_LOGGER
15
21
 
22
+ COMPRESSION_LEVEL = 6 # 6 is the default compression level for PIL if not specified
23
+
24
+
25
+ @dataclasses.dataclass
26
+ class _SnapshotInfo:
27
+ beam_centre: tuple[int, int]
28
+ microns_per_pixel: tuple[float, float]
29
+ snapshot_path: str
30
+ omega: int
31
+ sample_pos_mm: tuple[float, float, float]
32
+
33
+ @property
34
+ def snapshot_basename(self) -> str:
35
+ match = re.match("(.*)\\.png", self.snapshot_path)
36
+ assert match, f"Snapshot {self.snapshot_path} was not a .png file"
37
+ return match.groups()[0]
38
+
16
39
 
17
40
  class BeamDrawingCallback(PlanReactiveCallback):
18
41
  """
19
42
  Callback that monitors for OAV_ROTATION_SNAPSHOT_TRIGGERED events and
20
43
  draws a crosshair at the beam centre, saving the snapshot to a file.
21
- The callback assumes an OAV device "oav"
44
+ The callback assumes an OAV device "oav" and Smargon "smargon"
22
45
  Examples:
23
- Take a snapshot at the current location
46
+ Take a rotation snapshot at the current location
24
47
  >>> from bluesky.run_engine import RunEngine
25
48
  >>> import bluesky.preprocessors as bpp
26
49
  >>> import bluesky.plan_stubs as bps
@@ -39,69 +62,198 @@ class BeamDrawingCallback(PlanReactiveCallback):
39
62
  ... yield from bps.create(DocDescriptorNames.OAV_ROTATION_SNAPSHOT_TRIGGERED)
40
63
  ... yield from bps.read(oav)
41
64
  ... yield from bps.save()
65
+
66
+ Generate rotation snapshots from a previously taken base gridscan snapshot.
67
+ WithSnapshot.snapshot_omegas_deg is ignored and snapshots are generated for the previously captured
68
+ 0, 90 base images named "my_snapshot_prefix_0" and "my_snapshot_prefix_90"
69
+ >>> from dodal.devices.smargon import Smargon
70
+ >>> def take_snapshot(params: WithSnapshot, oav: OAV, smargon: Smargon, run_engine: RunEngine):
71
+ ... run_engine.subscribe(BeamDrawingCallback())
72
+ ... @bpp.run_decorator(md={
73
+ ... "activate_callbacks": ["BeamDrawingCallback"],
74
+ ... "with_snapshot": params.model_dump_json(),
75
+ ... })
76
+ ... def inner_plan():
77
+ ... for omega in (0, 90,):
78
+ ... yield from bps.abs_set(smargon.omega, omega, wait=True)
79
+ ... yield from bps.abs_set(oav.grid_snapshot.directory, "/path/to/grid_snapshot_folder", wait=True)
80
+ ... yield from bps.abs_set(oav.grid_snapshot.filename, f"my_grid_snapshot_prefix_{omega}", wait=True)
81
+ ... yield from bps.trigger(oav.grid_snapshot, wait=True)
82
+ ... yield from bps.create(DocDescriptorNames.OAV_GRID_SNAPSHOT_TRIGGERED)
83
+ ... yield from bps.read(oav) # Capture base image path
84
+ ... yield from bps.read(smargon) # Capture base image sample x, y, z, omega
85
+ ... yield from bps.save()
86
+ ... # Rest of gridscan here...
87
+ ... # Later on...
88
+ ... for omega in (0, 90,):
89
+ ... yield from bps.abs_set(oav.snapshot.last_saved_path,
90
+ ... f"/path/to/snapshot_folder/my_snapshot_prefix_{omega}.png", wait=True)
91
+ ... yield from bps.create(DocDescriptorNames.OAV_ROTATION_SNAPSHOT_TRIGGERED)
92
+ ... yield from bps.read(oav) # Capture path info for generated snapshot
93
+ ... yield from bps.read(smargon) # Capture the current sample x, y, z
94
+ ... yield from bps.save()
42
95
  """
43
96
 
44
97
  def __init__(self, *args, **kwargs):
45
98
  super().__init__(*args, log=CALLBACK_LOGGER, **kwargs)
46
- self._snapshot_files: list[str] = []
47
- self._microns_per_pixel: tuple[float, float]
48
- self._beam_centre: tuple[int, int]
99
+ self._base_snapshots: list[_SnapshotInfo] = []
49
100
  self._rotation_snapshot_descriptor: str = ""
101
+ self._grid_snapshot_descriptor: str = ""
102
+ self._next_snapshot_info: Iterator | None = None
103
+ self._use_grid_snapshots: bool = False
104
+
105
+ def _reset(self):
106
+ self._base_snapshots = []
50
107
 
51
108
  def activity_gated_start(self, doc: RunStart):
52
109
  if self.activity_uid == doc.get("uid"):
53
- with_snapshot_json = doc.get("with_snapshot") # type: ignore
54
- assert with_snapshot_json, (
55
- "run start event did not have expected snapshot json"
56
- )
110
+ self._reset()
111
+ with_snapshot = WithSnapshot.model_validate_json(doc.get("with_snapshot")) # type: ignore
112
+ self._use_grid_snapshots = with_snapshot.use_grid_snapshots
113
+ CALLBACK_LOGGER.info(f"Snapshot callback initialised with {with_snapshot}")
114
+ elif doc.get("subplan_name") == PlanNameConstants.ROTATION_MAIN:
115
+ self._next_snapshot_info = None
116
+ CALLBACK_LOGGER.info("Snapshot callback start rotation")
57
117
  return doc
58
118
 
59
119
  def activity_gated_descriptor(self, doc: EventDescriptor) -> EventDescriptor | None:
60
120
  if doc.get("name") == DocDescriptorNames.OAV_ROTATION_SNAPSHOT_TRIGGERED:
61
121
  self._rotation_snapshot_descriptor = doc["uid"]
122
+ elif doc.get("name") == DocDescriptorNames.OAV_GRID_SNAPSHOT_TRIGGERED:
123
+ self._grid_snapshot_descriptor = doc["uid"]
62
124
  return doc
63
125
 
64
126
  def activity_gated_event(self, doc: Event) -> Event:
65
127
  if doc["descriptor"] == self._rotation_snapshot_descriptor:
66
128
  self._handle_rotation_snapshot(doc)
129
+ elif doc["descriptor"] == self._grid_snapshot_descriptor:
130
+ self._handle_grid_snapshot(doc)
67
131
  return doc
68
132
 
69
- def _extract_base_snapshot_params(self, doc: Event):
133
+ def _extract_base_snapshot_params(
134
+ self, snapshot_device_prefix: str, doc: Event
135
+ ) -> _SnapshotInfo:
70
136
  data = doc["data"]
71
- self._snapshot_files.append(data["oav-snapshot-last_saved_path"])
72
- self._microns_per_pixel = (
73
- data["oav-microns_per_pixel_x"],
74
- data["oav-microns_per_pixel_y"],
137
+ base_snapshot_path = data[f"oav-{snapshot_device_prefix}-last_saved_path"]
138
+ return _SnapshotInfo(
139
+ beam_centre=(data["oav-beam_centre_i"], data["oav-beam_centre_j"]),
140
+ microns_per_pixel=(
141
+ data["oav-microns_per_pixel_x"],
142
+ data["oav-microns_per_pixel_y"],
143
+ ),
144
+ snapshot_path=base_snapshot_path,
145
+ sample_pos_mm=(
146
+ data.get("smargon-x", 0.0),
147
+ data.get("smargon-y", 0.0),
148
+ data.get("smargon-z", 0.0),
149
+ ),
150
+ omega=round(data.get("smargon-omega", 0.0)),
75
151
  )
76
- self._beam_centre = (data["oav-beam_centre_i"], data["oav-beam_centre_j"])
77
152
 
78
- def _handle_rotation_snapshot(self, doc: Event):
79
- self._extract_base_snapshot_params(doc)
153
+ def _handle_grid_snapshot(self, doc: Event):
154
+ snapshot_info = self._extract_base_snapshot_params("grid_snapshot", doc)
155
+ self._base_snapshots.append(snapshot_info)
156
+
157
+ def _handle_rotation_snapshot(self, doc: Event) -> Event:
80
158
  data = doc["data"]
81
- snapshot_path = data["oav-snapshot-last_saved_path"]
82
- match = re.match("(.*)\\.png", snapshot_path)
83
- assert match, f"Snapshot {snapshot_path} was not a .png file"
84
- snapshot_base = match.groups()[0]
85
- output_snapshot_path = f"{snapshot_base}_with_beam_centre.png"
86
- self._generate_snapshot_at(snapshot_path, output_snapshot_path, 0, 0)
159
+ if self._use_grid_snapshots:
160
+ if not self._next_snapshot_info:
161
+ self._next_snapshot_info = iter(self._base_snapshots)
162
+ snapshot_info = next(self._next_snapshot_info, None)
163
+ assert snapshot_info, (
164
+ "Insufficient base gridscan snapshots to generate required rotation snapshots"
165
+ )
166
+ current_sample_pos_mm = (
167
+ data["smargon-x"],
168
+ data["smargon-y"],
169
+ data["smargon-z"],
170
+ )
171
+ CALLBACK_LOGGER.info(
172
+ f"Generating snapshot at {current_sample_pos_mm} from base snapshot {snapshot_info}"
173
+ )
174
+ output_snapshot_directory = data["oav-snapshot-directory"]
175
+ base_file_stem = Path(snapshot_info.snapshot_path).stem
176
+ output_snapshot_filename = _snapshot_filename(base_file_stem)
177
+ output_snapshot_path = (
178
+ f"{output_snapshot_directory}/{output_snapshot_filename}.png"
179
+ )
180
+ self._generate_snapshot_at(
181
+ snapshot_info,
182
+ output_snapshot_path,
183
+ *self._image_plane_offset_mm(snapshot_info, current_sample_pos_mm),
184
+ )
185
+ else:
186
+ snapshot_info = self._extract_base_snapshot_params("snapshot", doc)
187
+ output_snapshot_path = (
188
+ f"{snapshot_info.snapshot_basename}_with_beam_centre.png"
189
+ )
190
+ CALLBACK_LOGGER.info(
191
+ f"Annotating snapshot {output_snapshot_path} from base snapshot {snapshot_info}"
192
+ )
193
+ self._generate_snapshot_zero_offset(
194
+ snapshot_info,
195
+ output_snapshot_path,
196
+ )
87
197
  data["oav-snapshot-last_saved_path"] = output_snapshot_path
88
198
  return doc
89
199
 
200
+ def _image_plane_offset_mm(
201
+ self,
202
+ snapshot_info: _SnapshotInfo,
203
+ current_sample_pos_mm: tuple[float, float, float],
204
+ ) -> tuple[float, float]:
205
+ return self._project_xyz_to_xy(
206
+ (
207
+ (current_sample_pos_mm[0] - snapshot_info.sample_pos_mm[0]),
208
+ (current_sample_pos_mm[1] - snapshot_info.sample_pos_mm[1]),
209
+ (current_sample_pos_mm[2] - snapshot_info.sample_pos_mm[2]),
210
+ ),
211
+ snapshot_info.omega,
212
+ )
213
+
214
+ def _project_xyz_to_xy(
215
+ self, xyz: tuple[float, float, float], omega_deg: float
216
+ ) -> tuple[float, float]:
217
+ return (
218
+ xyz[0],
219
+ xyz[1] * cos(-radians(omega_deg)) + xyz[2] * sin(-radians(omega_deg)),
220
+ )
221
+
222
+ def _generate_snapshot_zero_offset(
223
+ self,
224
+ base_snapshot_info: _SnapshotInfo,
225
+ output_snapshot_path: str,
226
+ ):
227
+ self._generate_snapshot_at(base_snapshot_info, output_snapshot_path, 0, 0)
228
+
90
229
  def _generate_snapshot_at(
91
- self, input_snapshot_path: str, output_snapshot_path: str, x_mm: int, y_mm: int
230
+ self,
231
+ base_snapshot_info: _SnapshotInfo,
232
+ output_snapshot_path: str,
233
+ image_plane_dx_mm: float,
234
+ image_plane_dy_mm: float,
92
235
  ):
93
236
  """
94
237
  Save a snapshot to the specified path, with an annotated crosshair at the specified
95
238
  position
96
239
  Args:
97
- input_snapshot_path: The non-annotated image path.
240
+ base_snapshot_info: Metadata about the base snapshot image from which the annotated
241
+ image will be derived.
98
242
  output_snapshot_path: The path to the image that will be annotated.
99
- x_mm: Relative x location of the sample to the original image (mm)
100
- y_mm: Relative y location of the sample to the original image (mm)
243
+ image_plane_dx_mm: Relative x location of the sample to the original image in the image plane (mm)
244
+ image_plane_dy_mm: Relative y location of the sample to the original image in the image plane (mm)
101
245
  """
102
- image = Image.open(input_snapshot_path)
246
+ image = Image.open(base_snapshot_info.snapshot_path)
103
247
  x_px, y_px = compute_beam_centre_pixel_xy_for_mm_position(
104
- (x_mm, y_mm), self._beam_centre, self._microns_per_pixel
248
+ (image_plane_dx_mm, image_plane_dy_mm),
249
+ base_snapshot_info.beam_centre,
250
+ base_snapshot_info.microns_per_pixel,
105
251
  )
106
252
  draw_crosshair(image, x_px, y_px)
107
- image.save(output_snapshot_path, format="png")
253
+ image.save(output_snapshot_path, format="png", compress_level=COMPRESSION_LEVEL)
254
+
255
+
256
+ def _snapshot_filename(grid_snapshot_name):
257
+ time_now = datetime.now()
258
+ filename = f"{time_now.strftime('%H%M%S%f')[:8]}_oav_snapshot_{grid_snapshot_name}"
259
+ return filename
@@ -21,7 +21,9 @@ class HyperionFeatureFlags(FeatureFlags):
21
21
  set_stub_offsets: If True then set the stub offsets after moving to the crystal (ignored for
22
22
  multi-centre)
23
23
  omega_flip: If True then invert the smargon omega motor rotation commands with respect to
24
- the hyperion request.
24
+ the hyperion request. See "Hyperion Coordinate Systems" in the documentation.
25
+ alternate_rotation_direction: If True then the for multi-sample pins the rotation direction of
26
+ successive rotation scans is alternated between positive and negative.
25
27
  """
26
28
 
27
29
  @staticmethod
@@ -41,3 +43,4 @@ class HyperionFeatureFlags(FeatureFlags):
41
43
  use_gpu_results: bool = CONST.I03.USE_GPU_RESULTS
42
44
  set_stub_offsets: bool = CONST.I03.SET_STUB_OFFSETS
43
45
  omega_flip: bool = CONST.I03.OMEGA_FLIP
46
+ alternate_rotation_direction: bool = CONST.I03.ALTERNATE_ROTATION_DIRECTION
@@ -8,8 +8,6 @@ from mx_bluesky._version import version
8
8
  @dataclass
9
9
  class HyperionArgs:
10
10
  dev_mode: bool = False
11
- verbose_event_logging: bool = False
12
- skip_startup_connection: bool = False
13
11
 
14
12
 
15
13
  def _add_callback_relevant_args(parser: argparse.ArgumentParser) -> None:
@@ -17,7 +15,7 @@ def _add_callback_relevant_args(parser: argparse.ArgumentParser) -> None:
17
15
  parser.add_argument(
18
16
  "--dev",
19
17
  action="store_true",
20
- help="Use dev options, such as local graylog instances and S03",
18
+ help="Use dev options, such as local graylog instances",
21
19
  )
22
20
 
23
21
 
@@ -30,22 +28,11 @@ def parse_callback_dev_mode_arg() -> bool:
30
28
 
31
29
 
32
30
  def parse_cli_args() -> HyperionArgs:
33
- """Parses all arguments relevant to hyperion. Returns an HyperionArgs dataclass with
34
- the fields: (verbose_event_logging: bool,
35
- dev_mode: bool,
36
- skip_startup_connection: bool)"""
31
+ """Parses all arguments relevant to hyperion.
32
+ Returns:
33
+ an HyperionArgs dataclass with the fields: (dev_mode: bool)"""
37
34
  parser = argparse.ArgumentParser()
38
35
  _add_callback_relevant_args(parser)
39
- parser.add_argument(
40
- "--verbose-event-logging",
41
- action="store_true",
42
- help="Log all bluesky event documents to graylog",
43
- )
44
- parser.add_argument(
45
- "--skip-startup-connection",
46
- action="store_true",
47
- help="Skip connecting to EPICS PVs on startup",
48
- )
49
36
  parser.add_argument(
50
37
  "--version",
51
38
  help="Print hyperion version string",
@@ -54,7 +41,5 @@ def parse_cli_args() -> HyperionArgs:
54
41
  )
55
42
  args = parser.parse_args()
56
43
  return HyperionArgs(
57
- verbose_event_logging=args.verbose_event_logging or False,
58
44
  dev_mode=args.dev or False,
59
- skip_startup_connection=args.skip_startup_connection or False,
60
45
  )
@@ -12,8 +12,6 @@ from mx_bluesky.common.parameters.constants import (
12
12
  OavConstants,
13
13
  PlanGroupCheckpointConstants,
14
14
  PlanNameConstants,
15
- SimConstants,
16
- TriggerConstants,
17
15
  )
18
16
 
19
17
  TEST_MODE = os.environ.get("HYPERION_TEST_MODE")
@@ -30,6 +28,7 @@ class I03Constants:
30
28
  USE_PANDA_FOR_GRIDSCAN = False
31
29
  SET_STUB_OFFSETS = False
32
30
  OMEGA_FLIP = True
31
+ ALTERNATE_ROTATION_DIRECTION = True
33
32
 
34
33
  # Turns on GPU processing for zocalo and logs a comparison between GPU and CPU-
35
34
  # processed results.
@@ -42,15 +41,12 @@ class I03Constants:
42
41
  @dataclass(frozen=True)
43
42
  class HyperionConstants:
44
43
  DESCRIPTORS = DocDescriptorNames()
45
- TRIGGER = TriggerConstants()
46
44
  ZOCALO_ENV = EnvironmentConstants.ZOCALO_ENV
47
45
  HARDWARE = HardwareConstants()
48
46
  I03 = I03Constants()
49
47
  PARAM = ExperimentParamConstants()
50
48
  PLAN = PlanNameConstants()
51
49
  WAIT = PlanGroupCheckpointConstants()
52
- SIM = SimConstants()
53
- TRIGGER = TriggerConstants()
54
50
  CALLBACK_0MQ_PROXY_PORTS = (5577, 5578)
55
51
  DESCRIPTORS = DocDescriptorNames()
56
52
  CONFIG_SERVER_URL = (
@@ -6,13 +6,17 @@ from dodal.devices.aperturescatterguard import (
6
6
  )
7
7
  from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
8
8
  from dodal.devices.backlight import Backlight
9
- from dodal.devices.dcm import DCM
9
+ from dodal.devices.common_dcm import BaseDCM
10
+ from dodal.devices.detector.detector_motion import DetectorMotion
10
11
  from dodal.devices.eiger import EigerDetector
11
12
  from dodal.devices.fast_grid_scan import (
12
13
  PandAFastGridScan,
13
14
  ZebraFastGridScan,
14
15
  )
15
16
  from dodal.devices.flux import Flux
17
+ from dodal.devices.i03 import Beamstop
18
+ from dodal.devices.oav.oav_detector import OAV
19
+ from dodal.devices.oav.pin_image_recognition import PinTipDetection
16
20
  from dodal.devices.robot import BartRobot
17
21
  from dodal.devices.s4_slit_gaps import S4SlitGaps
18
22
  from dodal.devices.smargon import Smargon
@@ -24,22 +28,53 @@ from dodal.devices.zebra.zebra_controlled_shutter import ZebraShutter
24
28
  from dodal.devices.zocalo import ZocaloResults
25
29
  from ophyd_async.fastcs.panda import HDFPanda
26
30
 
31
+ from mx_bluesky.common.plans.common_flyscan_xray_centre_plan import (
32
+ FlyScanEssentialDevices,
33
+ )
34
+
27
35
 
28
36
  @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
29
- class HyperionFlyScanXRayCentreComposite:
37
+ class HyperionFlyScanXRayCentreComposite(FlyScanEssentialDevices):
30
38
  """All devices which are directly or indirectly required by this plan"""
31
39
 
32
40
  aperture_scatterguard: ApertureScatterguard
33
41
  attenuator: BinaryFilterAttenuator
42
+ dcm: BaseDCM
43
+ eiger: EigerDetector
44
+ flux: Flux
45
+ s4_slit_gaps: S4SlitGaps
46
+ undulator: Undulator
47
+ synchrotron: Synchrotron
48
+ zebra: Zebra
49
+ zocalo: ZocaloResults
50
+ panda: HDFPanda
51
+ panda_fast_grid_scan: PandAFastGridScan
52
+ robot: BartRobot
53
+ sample_shutter: ZebraShutter
34
54
  backlight: Backlight
35
- dcm: DCM
55
+ xbpm_feedback: XBPMFeedback
56
+ zebra_fast_grid_scan: ZebraFastGridScan
57
+
58
+
59
+ @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
60
+ class GridDetectThenXRayCentreComposite:
61
+ """All devices which are directly or indirectly required by this plan"""
62
+
63
+ aperture_scatterguard: ApertureScatterguard
64
+ attenuator: BinaryFilterAttenuator
65
+ backlight: Backlight
66
+ beamstop: Beamstop
67
+ dcm: BaseDCM
68
+ detector_motion: DetectorMotion
36
69
  eiger: EigerDetector
37
70
  zebra_fast_grid_scan: ZebraFastGridScan
38
71
  flux: Flux
39
- s4_slit_gaps: S4SlitGaps
72
+ oav: OAV
73
+ pin_tip_detection: PinTipDetection
40
74
  smargon: Smargon
41
- undulator: Undulator
42
75
  synchrotron: Synchrotron
76
+ s4_slit_gaps: S4SlitGaps
77
+ undulator: Undulator
43
78
  xbpm_feedback: XBPMFeedback
44
79
  zebra: Zebra
45
80
  zocalo: ZocaloResults
@@ -1,8 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dodal.devices.detector import (
4
- DetectorParams,
5
- )
6
3
  from dodal.devices.fast_grid_scan import (
7
4
  PandAGridScanParams,
8
5
  ZebraGridScanParams,
@@ -13,7 +10,6 @@ from mx_bluesky.common.parameters.gridscan import (
13
10
  SpecifiedThreeDGridScan,
14
11
  )
15
12
  from mx_bluesky.hyperion.parameters.components import WithHyperionUDCFeatures
16
- from mx_bluesky.hyperion.parameters.constants import CONST, I03Constants
17
13
 
18
14
 
19
15
  class GridCommonWithHyperionDetectorParams(GridCommon, WithHyperionUDCFeatures):
@@ -23,34 +19,11 @@ class GridCommonWithHyperionDetectorParams(GridCommon, WithHyperionUDCFeatures):
23
19
  # https://github.com/DiamondLightSource/hyperion/issues/1395"""
24
20
  @property
25
21
  def detector_params(self):
26
- self.det_dist_to_beam_converter_path = (
27
- self.det_dist_to_beam_converter_path
28
- or CONST.PARAM.DETECTOR.BEAM_XY_LUT_PATH
29
- )
30
- optional_args = {}
31
- if self.run_number:
32
- optional_args["run_number"] = self.run_number
33
- assert self.detector_distance_mm is not None, (
34
- "Detector distance must be filled before generating DetectorParams"
35
- )
36
- return DetectorParams(
37
- detector_size_constants=I03Constants.DETECTOR,
38
- expected_energy_ev=self.demand_energy_ev,
39
- exposure_time_s=self.exposure_time_s,
40
- directory=self.storage_directory,
41
- prefix=self.file_name,
42
- detector_distance=self.detector_distance_mm,
43
- omega_start=self.omega_start_deg or 0,
44
- omega_increment=0,
45
- num_images_per_trigger=1,
46
- num_triggers=self.num_images,
47
- use_roi_mode=self.use_roi_mode,
48
- det_dist_to_beam_converter_path=self.det_dist_to_beam_converter_path,
49
- trigger_mode=self.trigger_mode,
50
- enable_dev_shm=self.features.compare_cpu_and_gpu_zocalo
51
- or self.features.use_gpu_results,
52
- **optional_args,
22
+ params = super().detector_params
23
+ params.enable_dev_shm = (
24
+ self.features.compare_cpu_and_gpu_zocalo or self.features.use_gpu_results
53
25
  )
26
+ return params
54
27
 
55
28
 
56
29
  class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGridScan):
@@ -58,36 +31,14 @@ class HyperionSpecifiedThreeDGridScan(WithHyperionUDCFeatures, SpecifiedThreeDGr
58
31
 
59
32
  # These detector params only exist so that we can properly select enable_dev_shm. Remove in
60
33
  # https://github.com/DiamondLightSource/hyperion/issues/1395"""
34
+
61
35
  @property
62
36
  def detector_params(self):
63
- self.det_dist_to_beam_converter_path = (
64
- self.det_dist_to_beam_converter_path
65
- or CONST.PARAM.DETECTOR.BEAM_XY_LUT_PATH
66
- )
67
- optional_args = {}
68
- if self.run_number:
69
- optional_args["run_number"] = self.run_number
70
- assert self.detector_distance_mm is not None, (
71
- "Detector distance must be filled before generating DetectorParams"
72
- )
73
- return DetectorParams(
74
- detector_size_constants=I03Constants.DETECTOR,
75
- expected_energy_ev=self.demand_energy_ev,
76
- exposure_time_s=self.exposure_time_s,
77
- directory=self.storage_directory,
78
- prefix=self.file_name,
79
- detector_distance=self.detector_distance_mm,
80
- omega_start=self.omega_start_deg or 0,
81
- omega_increment=0,
82
- num_images_per_trigger=1,
83
- num_triggers=self.num_images,
84
- use_roi_mode=self.use_roi_mode,
85
- det_dist_to_beam_converter_path=self.det_dist_to_beam_converter_path,
86
- trigger_mode=self.trigger_mode,
87
- enable_dev_shm=self.features.compare_cpu_and_gpu_zocalo
88
- or self.features.use_gpu_results,
89
- **optional_args,
37
+ params = super().detector_params
38
+ params.enable_dev_shm = (
39
+ self.features.compare_cpu_and_gpu_zocalo or self.features.use_gpu_results
90
40
  )
41
+ return params
91
42
 
92
43
  # Relative to common grid scan, stub offsets are defined by config server
93
44
  @property