dls-dodal 1.64.0__py3-none-any.whl → 1.66.0__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 (75) hide show
  1. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/METADATA +3 -4
  2. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/RECORD +72 -66
  3. dodal/_version.py +2 -2
  4. dodal/beamline_specific_utils/i05_shared.py +6 -3
  5. dodal/beamlines/aithre.py +21 -2
  6. dodal/beamlines/b01_1.py +1 -1
  7. dodal/beamlines/b07.py +6 -3
  8. dodal/beamlines/b07_1.py +6 -3
  9. dodal/beamlines/i03.py +32 -4
  10. dodal/beamlines/i04.py +18 -3
  11. dodal/beamlines/i05.py +30 -3
  12. dodal/beamlines/i05_1.py +2 -2
  13. dodal/beamlines/i06.py +62 -0
  14. dodal/beamlines/i07.py +20 -0
  15. dodal/beamlines/i09.py +3 -3
  16. dodal/beamlines/i09_1.py +12 -1
  17. dodal/beamlines/i09_2.py +6 -3
  18. dodal/beamlines/i10_optics.py +21 -11
  19. dodal/beamlines/i17.py +3 -3
  20. dodal/beamlines/i18.py +3 -3
  21. dodal/beamlines/i19_2.py +22 -0
  22. dodal/beamlines/i21.py +3 -3
  23. dodal/beamlines/i22.py +3 -20
  24. dodal/beamlines/k07.py +6 -3
  25. dodal/beamlines/p38.py +3 -3
  26. dodal/devices/aithre_lasershaping/goniometer.py +26 -9
  27. dodal/devices/aperturescatterguard.py +3 -2
  28. dodal/devices/apple2_undulator.py +89 -44
  29. dodal/devices/areadetector/plugins/mjpg.py +10 -3
  30. dodal/devices/beamsize/__init__.py +0 -0
  31. dodal/devices/beamsize/beamsize.py +6 -0
  32. dodal/devices/cryostream.py +21 -0
  33. dodal/devices/detector/det_resolution.py +4 -2
  34. dodal/devices/fast_grid_scan.py +14 -2
  35. dodal/devices/i03/beamsize.py +35 -0
  36. dodal/devices/i03/constants.py +7 -0
  37. dodal/devices/i03/undulator_dcm.py +2 -2
  38. dodal/devices/i04/beamsize.py +45 -0
  39. dodal/devices/i04/murko_results.py +36 -26
  40. dodal/devices/i04/transfocator.py +23 -29
  41. dodal/devices/i07/id.py +38 -0
  42. dodal/devices/i09_1_shared/__init__.py +6 -2
  43. dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
  44. dodal/devices/i10/i10_apple2.py +22 -316
  45. dodal/devices/i17/i17_apple2.py +7 -4
  46. dodal/devices/i22/nxsas.py +5 -24
  47. dodal/devices/ipin.py +20 -2
  48. dodal/devices/motors.py +19 -3
  49. dodal/devices/mx_phase1/beamstop.py +31 -12
  50. dodal/devices/oav/oav_calculations.py +9 -4
  51. dodal/devices/oav/oav_detector.py +65 -7
  52. dodal/devices/oav/oav_parameters.py +3 -1
  53. dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
  54. dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
  55. dodal/devices/oav/pin_image_recognition/utils.py +23 -1
  56. dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
  57. dodal/devices/oav/utils.py +16 -6
  58. dodal/devices/pgm.py +1 -1
  59. dodal/devices/robot.py +17 -7
  60. dodal/devices/scintillator.py +40 -14
  61. dodal/devices/smargon.py +2 -3
  62. dodal/devices/thawer.py +7 -45
  63. dodal/devices/undulator.py +178 -66
  64. dodal/devices/util/lookup_tables_apple2.py +390 -0
  65. dodal/plan_stubs/__init__.py +3 -0
  66. dodal/plans/load_panda_yaml.py +9 -0
  67. dodal/plans/verify_undulator_gap.py +2 -2
  68. dodal/testing/fixtures/run_engine.py +79 -7
  69. dodal/beamline_specific_utils/i03.py +0 -17
  70. dodal/testing/__init__.py +0 -3
  71. dodal/testing/setup.py +0 -67
  72. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/WHEEL +0 -0
  73. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/entry_points.txt +0 -0
  74. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/licenses/LICENSE +0 -0
  75. {dls_dodal-1.64.0.dist-info → dls_dodal-1.66.0.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,9 @@ def camera_coordinates_to_xyz_mm(
7
7
  omega: float,
8
8
  microns_per_i_pixel: float,
9
9
  microns_per_j_pixel: float,
10
+ x_horizontal_sign: int,
11
+ y_vertical_sign: int,
12
+ z_vertical_sign: int,
10
13
  ) -> np.ndarray:
11
14
  """
12
15
  Converts from (horizontal,vertical) pixel measurements from the OAV camera into to (x, y, z) motor coordinates in millimetres.
@@ -18,13 +21,16 @@ def camera_coordinates_to_xyz_mm(
18
21
  omega (float): The omega angle of the smargon that the horizontal, vertical measurements were obtained at.
19
22
  microns_per_i_pixel (float): The number of microns per i pixel, adjusted for the zoom level horizontal was measured at.
20
23
  microns_per_j_pixel (float): The number of microns per j pixel, adjusted for the zoom level vertical was measured at.
24
+ x_horizontal_sign (int): Direction mapping for x, positive means the oav and motor are on same direction, default from hyperion
25
+ y_vertical_sign (int): Direction mapping for y
26
+ z_vertical_sign (int): Direction mapping for z
21
27
  """
22
28
  # Convert the vertical and horizontal into mm.
23
29
  horizontal *= microns_per_i_pixel * 1e-3
24
30
  vertical *= microns_per_j_pixel * 1e-3
25
31
 
26
32
  # +ve x in the OAV camera becomes -ve x in the smargon motors.
27
- x = -horizontal
33
+ x = x_horizontal_sign * horizontal
28
34
 
29
35
  # Rotating the camera causes the position on the vertical horizontal to change by raising or lowering the centre.
30
36
  # We can negate this change by multiplying sin and cosine of the omega.
@@ -33,9 +39,8 @@ def camera_coordinates_to_xyz_mm(
33
39
  sine = np.sin(radians)
34
40
 
35
41
  # +ve y in the OAV camera becomes -ve y in the smargon motors/
36
- y = -vertical * cosine
37
-
38
- z = vertical * sine
42
+ y = y_vertical_sign * vertical * cosine
43
+ z = z_vertical_sign * vertical * sine
39
44
  return np.array([x, y, z], dtype=np.float64)
40
45
 
41
46
 
@@ -46,9 +46,14 @@ class NullZoomController(BaseZoomController):
46
46
  def __init__(self):
47
47
  self.level = soft_signal_rw(str, "1.0x")
48
48
  self.percentage = soft_signal_rw(float, 100)
49
+ super().__init__()
49
50
 
50
- def set(self, value):
51
- raise Exception("Attempting to set zoom level of a null zoom controller")
51
+ @AsyncStatus.wrap
52
+ async def set(self, value: str) -> None:
53
+ if value != "1.0x":
54
+ raise Exception("Attempting to set zoom level of a null zoom controller")
55
+ else:
56
+ await self.level.set(value, wait=True)
52
57
 
53
58
 
54
59
  class ZoomController(BaseZoomController):
@@ -74,6 +79,16 @@ class ZoomController(BaseZoomController):
74
79
 
75
80
 
76
81
  class OAV(StandardReadable):
82
+ """
83
+ Class for oav device
84
+
85
+ x_direction(int): Should only be 1 or -1, with 1 indicating the oav x direction is the same with motor x
86
+ y_direction(int): Same with x_direction but for motor y
87
+ z_direction(int): Same with x_direction but for motor z
88
+ mjpg_x_size_pv(str): PV infix for x_size in mjpg
89
+ mjpg_y_size_pv(str): PV infix for y_size in mjpg
90
+ """
91
+
77
92
  beam_centre_i: SignalR[int]
78
93
  beam_centre_j: SignalR[int]
79
94
 
@@ -82,7 +97,13 @@ class OAV(StandardReadable):
82
97
  prefix: str,
83
98
  config: OAVConfigBase,
84
99
  name: str = "",
100
+ mjpeg_prefix: str = "MJPG",
85
101
  zoom_controller: BaseZoomController | None = None,
102
+ x_direction: int = -1,
103
+ y_direction: int = -1,
104
+ z_direction: int = 1,
105
+ mjpg_x_size_pv: str = "ArraySize1_RBV",
106
+ mjpg_y_size_pv: str = "ArraySize2_RBV",
86
107
  ):
87
108
  self.oav_config = config
88
109
  self._prefix = prefix
@@ -98,9 +119,15 @@ class OAV(StandardReadable):
98
119
 
99
120
  self.cam = Cam(f"{prefix}CAM:", name=name)
100
121
  with self.add_children_as_readables():
101
- self.grid_snapshot = SnapshotWithGrid(f"{prefix}MJPG:", name)
122
+ self.grid_snapshot = SnapshotWithGrid(
123
+ f"{prefix}{mjpeg_prefix}:", name, mjpg_x_size_pv, mjpg_y_size_pv
124
+ )
102
125
 
103
126
  self.sizes = [self.grid_snapshot.x_size, self.grid_snapshot.y_size]
127
+ with self.add_children_as_readables():
128
+ self.x_direction = soft_signal_rw(int, x_direction, name="x_direction")
129
+ self.y_direction = soft_signal_rw(int, y_direction, name="y_direction")
130
+ self.z_direction = soft_signal_rw(int, z_direction, name="z_direction")
104
131
 
105
132
  with self.add_children_as_readables():
106
133
  self.microns_per_pixel_x = derived_signal_r(
@@ -116,7 +143,7 @@ class OAV(StandardReadable):
116
143
  coord=soft_signal_rw(datatype=int, initial_value=Coords.Y.value),
117
144
  )
118
145
  self.snapshot = Snapshot(
119
- f"{self._prefix}MJPG:",
146
+ f"{self._prefix}{mjpeg_prefix}:",
120
147
  self._name,
121
148
  )
122
149
 
@@ -143,9 +170,16 @@ class OAV(StandardReadable):
143
170
 
144
171
 
145
172
  class OAVBeamCentreFile(OAV):
146
- """OAV device that reads its beam centre values from a file. The config parameter
173
+ """
174
+ OAV device that reads its beam centre values from a file. The config parameter
147
175
  must be a OAVConfigBeamCentre object, as this contains a filepath to where the beam
148
176
  centre values are stored.
177
+
178
+ x_direction(int): Should only be 1 or -1, with 1 indicating the oav x direction is the same with motor x
179
+ y_direction(int): Same with x_direction but for motor y
180
+ z_direction(int): Same with x_direction but for motor z
181
+ mjpg_x_size_pv(str): PV infix for x_size in mjpg
182
+ mjpg_y_size_pv(str): PV infix for y_size in mjpg
149
183
  """
150
184
 
151
185
  def __init__(
@@ -153,9 +187,26 @@ class OAVBeamCentreFile(OAV):
153
187
  prefix: str,
154
188
  config: OAVConfigBeamCentre,
155
189
  name: str = "",
190
+ mjpeg_prefix: str = "MJPG",
156
191
  zoom_controller: BaseZoomController | None = None,
192
+ mjpg_x_size_pv: str = "ArraySize1_RBV",
193
+ mjpg_y_size_pv: str = "ArraySize2_RBV",
194
+ x_direction: int = -1,
195
+ y_direction: int = -1,
196
+ z_direction: int = 1,
157
197
  ):
158
- super().__init__(prefix, config, name, zoom_controller)
198
+ super().__init__(
199
+ prefix=prefix,
200
+ config=config,
201
+ name=name,
202
+ mjpeg_prefix=mjpeg_prefix,
203
+ zoom_controller=zoom_controller,
204
+ mjpg_x_size_pv=mjpg_x_size_pv,
205
+ mjpg_y_size_pv=mjpg_y_size_pv,
206
+ x_direction=x_direction,
207
+ y_direction=y_direction,
208
+ z_direction=z_direction,
209
+ )
159
210
 
160
211
  with self.add_children_as_readables():
161
212
  self.beam_centre_i = derived_signal_r(
@@ -189,6 +240,7 @@ class OAVBeamCentrePV(OAV):
189
240
  prefix: str,
190
241
  config: OAVConfig,
191
242
  name: str = "",
243
+ mjpeg_prefix: str = "MJPG",
192
244
  zoom_controller: BaseZoomController | None = None,
193
245
  overlay_channel: int = 1,
194
246
  ):
@@ -199,4 +251,10 @@ class OAVBeamCentrePV(OAV):
199
251
  self.beam_centre_j = epics_signal_r(
200
252
  int, prefix + f"OVER:{overlay_channel}:CenterY"
201
253
  )
202
- super().__init__(prefix, config, name, zoom_controller)
254
+ super().__init__(
255
+ prefix=prefix,
256
+ config=config,
257
+ name=name,
258
+ mjpeg_prefix=mjpeg_prefix,
259
+ zoom_controller=zoom_controller,
260
+ )
@@ -92,8 +92,10 @@ class OAVParameters:
92
92
  self.preprocess_K_size: int = update(
93
93
  "preProcessKSize", int
94
94
  ) # length scale for blur preprocessing
95
+ self.preprocess_iter: int = update("preProcessIteration", int, default=5)
95
96
  self.detection_script_filename: str = update("filename", str)
96
- self.close_ksize: int = update("close_ksize", int, default=11)
97
+ self.open_ksize: int = update("open_ksize", int, default=0)
98
+ self.close_ksize: int = update("close_ksize", int, default=5)
97
99
  self.min_callback_time: float = update("min_callback_time", float, default=0.08)
98
100
  self.direction: int = update("direction", int)
99
101
  self.max_tip_distance: float = update("max_tip_distance", float, default=300)
@@ -9,6 +9,7 @@ from bluesky.protocols import Flyable, Stoppable
9
9
  from ophyd_async.core import (
10
10
  AsyncStatus,
11
11
  DeviceVector,
12
+ Reference,
12
13
  StandardReadable,
13
14
  observe_value,
14
15
  soft_signal_r_and_setter,
@@ -17,6 +18,7 @@ from ophyd_async.core import (
17
18
  from ophyd_async.epics.core import epics_signal_r
18
19
  from redis.asyncio import StrictRedis
19
20
 
21
+ from dodal.devices.oav.oav_detector import OAV
20
22
  from dodal.log import LOGGER
21
23
 
22
24
 
@@ -35,13 +37,10 @@ class Source(IntEnum):
35
37
 
36
38
 
37
39
  class OAVSource(StandardReadable):
38
- def __init__(
39
- self,
40
- prefix: str,
41
- oav_name: str,
42
- ):
43
- self.url = epics_signal_r(str, f"{prefix}MJPG_URL_RBV")
44
- self.oav_name = oav_name
40
+ def __init__(self, oav: OAV, label: str):
41
+ self.url_ref = Reference(oav.snapshot.video_url)
42
+ self.oav_ref = Reference(oav)
43
+ self.label = label
45
44
  super().__init__()
46
45
 
47
46
 
@@ -62,6 +61,8 @@ class OAVToRedisForwarder(StandardReadable, Flyable, Stoppable):
62
61
  def __init__(
63
62
  self,
64
63
  prefix: str,
64
+ oav_roi: OAV,
65
+ oav_fs: OAV,
65
66
  redis_host: str,
66
67
  redis_password: str,
67
68
  redis_db: int = 0,
@@ -78,13 +79,13 @@ class OAVToRedisForwarder(StandardReadable, Flyable, Stoppable):
78
79
  name: str the name of this device
79
80
  """
80
81
  self.counter = epics_signal_r(int, f"{prefix}CAM:ArrayCounter_RBV")
81
-
82
82
  self.sources = DeviceVector(
83
83
  {
84
- Source.ROI.value: OAVSource(f"{prefix}MJPG:", "roi"),
85
- Source.FULL_SCREEN.value: OAVSource(f"{prefix}XTAL:", "fullscreen"),
84
+ Source.ROI.value: OAVSource(oav_roi, "roi"),
85
+ Source.FULL_SCREEN.value: OAVSource(oav_fs, "fullscreen"),
86
86
  }
87
87
  )
88
+
88
89
  self.selected_source = soft_signal_rw(int)
89
90
 
90
91
  self.forwarding_task = None
@@ -120,11 +121,11 @@ class OAVToRedisForwarder(StandardReadable, Flyable, Stoppable):
120
121
  self, function_to_do: Callable[[ClientResponse, OAVSource], Awaitable]
121
122
  ):
122
123
  source_idx = await self.selected_source.get_value()
124
+ source = self.sources[source_idx]
125
+ stream_url = await source.url_ref().get_value()
123
126
  LOGGER.info(
124
- f"Forwarding data from sample {await self.sample_id.get_value()} and OAV {source_idx}"
127
+ f"Forwarding data from sample {await self.sample_id.get_value()} and OAV {source_idx} from URL {stream_url}"
125
128
  )
126
- source = self.sources[source_idx]
127
- stream_url = await source.url.get_value()
128
129
  async with ClientSession() as session:
129
130
  async with session.get(stream_url) as response:
130
131
  await function_to_do(response, source)
@@ -141,12 +142,14 @@ class OAVToRedisForwarder(StandardReadable, Flyable, Stoppable):
141
142
  asyncio.wait_for(self._stop_flag.wait(), timeout=self.TIMEOUT)
142
143
  )
143
144
  async for frame_count in observe_value(self.counter, done_status=done_status):
144
- redis_uuid = f"{source.oav_name}-{frame_count}-{uuid4()}"
145
+ redis_uuid = f"{source.label}-{frame_count}-{uuid4()}"
145
146
  await self._get_frame_and_put_to_redis(redis_uuid, response)
146
147
 
147
148
  async def _confirm_mjpg_stream(self, response: ClientResponse, source: OAVSource):
148
149
  if response.content_type != "multipart/x-mixed-replace":
149
- raise ValueError(f"{await source.url.get_value()} is not an MJPG stream")
150
+ raise ValueError(
151
+ f"{await source.url_ref().get_value()} is not an MJPG stream"
152
+ )
150
153
 
151
154
  @AsyncStatus.wrap
152
155
  async def kickoff(self):
@@ -67,6 +67,8 @@ class PinTipDetection(StandardReadable):
67
67
  )
68
68
  self.canny_upper_threshold = soft_signal_rw(int, 100, name="canny_upper")
69
69
  self.canny_lower_threshold = soft_signal_rw(int, 50, name="canny_lower")
70
+ self.open_ksize = soft_signal_rw(int, 0, name="open_ksize")
71
+ self.open_iterations = soft_signal_rw(int, 5, name="open_iterations")
70
72
  self.close_ksize = soft_signal_rw(int, 5, name="close_ksize")
71
73
  self.close_iterations = soft_signal_rw(int, 5, name="close_iterations")
72
74
  self.scan_direction = soft_signal_rw(
@@ -105,7 +107,7 @@ class PinTipDetection(StandardReadable):
105
107
 
106
108
  try:
107
109
  preprocess_func = ARRAY_PROCESSING_FUNCTIONS_MAP[preprocess_key](
108
- iter=preprocess_iter, ksize=preprocess_ksize
110
+ iterations=preprocess_iter, ksize=preprocess_ksize
109
111
  )
110
112
  except KeyError:
111
113
  LOGGER.error("Invalid preprocessing function, using identity")
@@ -121,6 +123,8 @@ class PinTipDetection(StandardReadable):
121
123
  preprocess=preprocess_func,
122
124
  canny_lower=await self.canny_lower_threshold.get_value(),
123
125
  canny_upper=await self.canny_upper_threshold.get_value(),
126
+ open_ksize=await self.open_ksize.get_value(),
127
+ open_iterations=await self.open_iterations.get_value(),
124
128
  close_ksize=await self.close_ksize.get_value(),
125
129
  close_iterations=await self.close_iterations.get_value(),
126
130
  scan_direction=direction,
@@ -14,6 +14,12 @@ class ScanDirections(Enum):
14
14
  REVERSE = -1
15
15
 
16
16
 
17
+ """
18
+ See https://opencv24-python-tutorials.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_morphological_ops/py_morphological_ops.html
19
+ for description of functions below.
20
+ """
21
+
22
+
17
23
  def identity(*args, **kwargs) -> Callable[[np.ndarray], np.ndarray]:
18
24
  return lambda arr: arr
19
25
 
@@ -109,6 +115,8 @@ class MxSampleDetect:
109
115
  self,
110
116
  *,
111
117
  preprocess: Callable[[np.ndarray], np.ndarray] = lambda arr: arr,
118
+ open_ksize: int = 0,
119
+ open_iterations: int = 5,
112
120
  canny_upper: int = 100,
113
121
  canny_lower: int = 50,
114
122
  close_ksize: int = 5,
@@ -123,6 +131,12 @@ class MxSampleDetect:
123
131
  preprocess: A preprocessing function applied to the array after conversion to grayscale.
124
132
  See implementations of common functions above for predefined conversions
125
133
  Defaults to a no-op (i.e. no preprocessing)
134
+
135
+ for open and close operations, please read:
136
+ https://docs.opencv.org/4.x/d9/d61/tutorial_py_morphological_ops.html
137
+
138
+ open_ksize: kernel size for "open" operation, if set to zero then ignore "open" operation
139
+ open_iterations: number of iterations for "open" operation
126
140
  canny_upper: upper threshold for canny edge detection
127
141
  canny_lower: lower threshold for canny edge detection
128
142
  close_ksize: kernel size for "close" operation
@@ -132,6 +146,8 @@ class MxSampleDetect:
132
146
  """
133
147
 
134
148
  self.preprocess = preprocess
149
+ self.open_ksize = open_ksize
150
+ self.open_iterations = open_iterations
135
151
  self.canny_upper = canny_upper
136
152
  self.canny_lower = canny_lower
137
153
  self.close_ksize = close_ksize
@@ -148,8 +164,14 @@ class MxSampleDetect:
148
164
  assert arr.ndim == 2
149
165
  gray_arr = arr
150
166
 
167
+ # Remove Noises if open set to non-zero
168
+ if self.open_ksize == 0:
169
+ open_arr = gray_arr
170
+ else:
171
+ open_arr = open_morph(self.open_ksize, self.open_iterations)(gray_arr)
172
+
151
173
  # Preprocess the array. (Use the greyscale one.)
152
- pp_arr = self.preprocess(gray_arr)
174
+ pp_arr = self.preprocess(open_arr)
153
175
 
154
176
  # Find some edges.
155
177
  edge_arr = cv2.Canny(pp_arr, self.canny_upper, self.canny_lower)
@@ -12,7 +12,13 @@ from dodal.log import LOGGER
12
12
 
13
13
 
14
14
  class SnapshotWithGrid(MJPG):
15
- def __init__(self, prefix: str, name: str = "") -> None:
15
+ def __init__(
16
+ self,
17
+ prefix: str,
18
+ name: str = "",
19
+ x_size_pv: str = "ArraySize1_RBV",
20
+ y_size_pv: str = "ArraySize2_RBV",
21
+ ) -> None:
16
22
  with self.add_children_as_readables():
17
23
  self.top_left_x = soft_signal_rw(float)
18
24
  self.top_left_y = soft_signal_rw(float)
@@ -23,7 +29,7 @@ class SnapshotWithGrid(MJPG):
23
29
  self.last_path_outer = soft_signal_rw(str)
24
30
  self.last_path_full_overlay = soft_signal_rw(str)
25
31
 
26
- super().__init__(prefix, name)
32
+ super().__init__(prefix, name, x_size_pv, y_size_pv)
27
33
 
28
34
  async def post_processing(self, image: Image):
29
35
  # Save an unmodified image with no suffix
@@ -5,13 +5,13 @@ import bluesky.plan_stubs as bps
5
5
  import numpy as np
6
6
  from bluesky.utils import Msg
7
7
 
8
+ from dodal.devices.motors import XYZOmegaStage
8
9
  from dodal.devices.oav.oav_calculations import (
9
10
  calculate_beam_distance,
10
11
  camera_coordinates_to_xyz_mm,
11
12
  )
12
13
  from dodal.devices.oav.oav_detector import OAV
13
14
  from dodal.devices.oav.pin_image_recognition import PinTipDetection
14
- from dodal.devices.smargon import Smargon
15
15
 
16
16
  Pixel = tuple[int, int]
17
17
 
@@ -52,24 +52,29 @@ class EdgeOutputArrayImageType(IntEnum):
52
52
 
53
53
 
54
54
  def get_move_required_so_that_beam_is_at_pixel(
55
- smargon: Smargon, pixel: Pixel, oav: OAV
55
+ gonio: XYZOmegaStage,
56
+ pixel: Pixel,
57
+ oav: OAV,
56
58
  ) -> Generator[Msg, None, np.ndarray]:
57
59
  """Calculate the required move so that the given pixel is in the centre of the beam."""
58
60
 
59
61
  current_motor_xyz = np.array(
60
62
  [
61
- (yield from bps.rd(smargon.x)),
62
- (yield from bps.rd(smargon.y)),
63
- (yield from bps.rd(smargon.z)),
63
+ (yield from bps.rd(gonio.x)),
64
+ (yield from bps.rd(gonio.y)),
65
+ (yield from bps.rd(gonio.z)),
64
66
  ],
65
67
  dtype=np.float64,
66
68
  )
67
- current_angle = yield from bps.rd(smargon.omega)
69
+ current_angle = yield from bps.rd(gonio.omega)
68
70
 
69
71
  beam_x = yield from bps.rd(oav.beam_centre_i)
70
72
  beam_y = yield from bps.rd(oav.beam_centre_j)
71
73
  microns_per_pixel_x = yield from bps.rd(oav.microns_per_pixel_x)
72
74
  microns_per_pixel_y = yield from bps.rd(oav.microns_per_pixel_y)
75
+ x_direction = yield from bps.rd(oav.x_direction)
76
+ y_direction = yield from bps.rd(oav.y_direction)
77
+ z_direction = yield from bps.rd(oav.z_direction)
73
78
 
74
79
  return calculate_x_y_z_of_pixel(
75
80
  current_motor_xyz,
@@ -77,6 +82,7 @@ def get_move_required_so_that_beam_is_at_pixel(
77
82
  pixel,
78
83
  (beam_x, beam_y),
79
84
  (microns_per_pixel_x, microns_per_pixel_y),
85
+ (x_direction, y_direction, z_direction),
80
86
  )
81
87
 
82
88
 
@@ -86,6 +92,7 @@ def calculate_x_y_z_of_pixel(
86
92
  pixel: Pixel,
87
93
  beam_centre: tuple[int, int],
88
94
  microns_per_pixel: tuple[float, float],
95
+ xyz_direction: tuple[int, int, int],
89
96
  ) -> np.ndarray:
90
97
  """Get the x, y, z position of a pixel in mm"""
91
98
  beam_distance_px: Pixel = calculate_beam_distance(beam_centre, *pixel)
@@ -96,6 +103,9 @@ def calculate_x_y_z_of_pixel(
96
103
  current_omega,
97
104
  microns_per_pixel[0],
98
105
  microns_per_pixel[1],
106
+ xyz_direction[0],
107
+ xyz_direction[1],
108
+ xyz_direction[2],
99
109
  )
100
110
 
101
111
 
dodal/devices/pgm.py CHANGED
@@ -7,7 +7,7 @@ from ophyd_async.epics.core import epics_signal_rw
7
7
  from ophyd_async.epics.motor import Motor
8
8
 
9
9
 
10
- class PGM(StandardReadable):
10
+ class PlaneGratingMonochromator(StandardReadable):
11
11
  """
12
12
  Plane grating monochromator, it is use in soft x-ray beamline to generate monochromic beam.
13
13
  """
dodal/devices/robot.py CHANGED
@@ -61,7 +61,7 @@ class ErrorStatus(Device):
61
61
  raise RobotLoadError(int(error_code), error_string) from raise_from
62
62
 
63
63
 
64
- class BartRobot(StandardReadable, Movable[SampleLocation]):
64
+ class BartRobot(StandardReadable, Movable[SampleLocation | None]):
65
65
  """The sample changing robot."""
66
66
 
67
67
  # How long to wait for the robot if it is busy soaking/drying
@@ -104,7 +104,6 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
104
104
  self.init = epics_signal_x(prefix + "INIT.PROC")
105
105
  self.soak = epics_signal_x(prefix + "SOAK.PROC")
106
106
  self.home = epics_signal_x(prefix + "GOHM.PROC")
107
- self.unload = epics_signal_x(prefix + "UNLD.PROC")
108
107
  self.dry = epics_signal_x(prefix + "DRY.PROC")
109
108
  self.open = epics_signal_x(prefix + "COLO.PROC")
110
109
  self.close = epics_signal_x(prefix + "COLC.PROC")
@@ -175,12 +174,23 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
175
174
  await self.pin_mounted_or_no_pin_found()
176
175
 
177
176
  @AsyncStatus.wrap
178
- async def set(self, value: SampleLocation):
177
+ async def set(self, value: SampleLocation | None):
178
+ """
179
+ Perform a sample load from the specified sample location
180
+ Args:
181
+ value: The pin and puck to load, or None to unload the sample.
182
+ Raises:
183
+ RobotLoadError if a timeout occurs, or if an error occurs loading the smaple.
184
+ """
179
185
  try:
180
- await wait_for(
181
- self._load_pin_and_puck(value),
182
- timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
183
- )
186
+ if value is not None:
187
+ await wait_for(
188
+ self._load_pin_and_puck(value),
189
+ timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
190
+ )
191
+ else:
192
+ await self.unload.trigger(timeout=self.LOAD_TIMEOUT)
193
+ await wait_for_value(self.program_running, False, self.NOT_BUSY_TIMEOUT)
184
194
  except TimeoutError as e:
185
195
  await self.prog_error.raise_if_error(e)
186
196
  await self.controller_error.raise_if_error(e)
@@ -8,9 +8,10 @@ from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureVal
8
8
 
9
9
 
10
10
  class InOut(StrictEnum):
11
- """Currently Hyperion only needs to move the scintillator out for data collection."""
11
+ """Moves scintillator in and out of the beam."""
12
12
 
13
- OUT = "Out"
13
+ OUT = "Out" # Out of beam
14
+ IN = "In" # In to beam
14
15
  UNKNOWN = "Unknown"
15
16
 
16
17
 
@@ -45,38 +46,63 @@ class Scintillator(StandardReadable):
45
46
  self._scintillator_out_yz_mm = [
46
47
  float(beamline_parameters[f"scin_{axis}_SCIN_OUT"]) for axis in ("y", "z")
47
48
  ]
49
+ self._scintillator_in_yz_mm = [
50
+ float(beamline_parameters[f"scin_{axis}_SCIN_IN"]) for axis in ("y", "z")
51
+ ]
48
52
  self._yz_tolerance_mm = [
49
53
  float(beamline_parameters[f"scin_{axis}_tolerance"]) for axis in ("y", "z")
50
54
  ]
51
55
 
52
56
  super().__init__(name)
53
57
 
54
- def _get_selected_position(self, y: float, z: float) -> InOut:
55
- current_pos = [y, z]
56
- if all(
58
+ def _check_position(self, current_pos: list[float], pos_to_check: list[float]):
59
+ return all(
57
60
  isclose(axis_pos, axis_in_beam, abs_tol=axis_tolerance)
58
61
  for axis_pos, axis_in_beam, axis_tolerance in zip(
59
62
  current_pos,
60
- self._scintillator_out_yz_mm,
63
+ pos_to_check,
61
64
  self._yz_tolerance_mm,
62
65
  strict=False,
63
66
  )
64
- ):
67
+ )
68
+
69
+ def _get_selected_position(self, y: float, z: float) -> InOut:
70
+ current_pos = [y, z]
71
+ if self._check_position(current_pos, self._scintillator_out_yz_mm):
65
72
  return InOut.OUT
73
+
74
+ elif self._check_position(current_pos, self._scintillator_in_yz_mm):
75
+ return InOut.IN
76
+
66
77
  else:
67
78
  return InOut.UNKNOWN
68
79
 
80
+ async def _check_aperture_parked(self):
81
+ if (
82
+ await self._aperture_scatterguard().selected_aperture.get_value()
83
+ != ApertureValue.PARKED
84
+ ):
85
+ raise ValueError(
86
+ f"Cannot move scintillator if aperture/scatterguard is not parked. Position is currently {await self._aperture_scatterguard().selected_aperture.get_value()}"
87
+ )
88
+
69
89
  async def _set_selected_position(self, position: InOut) -> None:
70
90
  match position:
71
91
  case InOut.OUT:
72
- if (
73
- self._aperture_scatterguard().selected_aperture.get_value()
74
- != ApertureValue.PARKED
75
- ):
76
- raise ValueError(
77
- "Cannot move scintillator out if aperture/scatterguard is not parked"
78
- )
92
+ current_y = await self.y_mm.user_readback.get_value()
93
+ current_z = await self.z_mm.user_readback.get_value()
94
+ if self._get_selected_position(current_y, current_z) == InOut.OUT:
95
+ return
96
+ await self._check_aperture_parked()
79
97
  await self.y_mm.set(self._scintillator_out_yz_mm[0])
80
98
  await self.z_mm.set(self._scintillator_out_yz_mm[1])
99
+ case InOut.IN:
100
+ current_y = await self.y_mm.user_readback.get_value()
101
+ current_z = await self.z_mm.user_readback.get_value()
102
+ if self._get_selected_position(current_y, current_z) == InOut.IN:
103
+ return
104
+ await self._check_aperture_parked()
105
+ await self.z_mm.set(self._scintillator_in_yz_mm[1])
106
+ await self.y_mm.set(self._scintillator_in_yz_mm[0])
81
107
  case _:
82
108
  raise ValueError(f"Cannot set scintillator to position {position}")
dodal/devices/smargon.py CHANGED
@@ -14,7 +14,7 @@ from ophyd_async.core import (
14
14
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
15
15
  from ophyd_async.epics.motor import Motor
16
16
 
17
- from dodal.devices.motors import XYZStage
17
+ from dodal.devices.motors import XYZOmegaStage
18
18
  from dodal.devices.util.epics_util import SetWhenEnabled
19
19
 
20
20
 
@@ -78,7 +78,7 @@ class CombinedMove(TypedDict, total=False):
78
78
  chi: float | None
79
79
 
80
80
 
81
- class Smargon(XYZStage, Movable):
81
+ class Smargon(XYZOmegaStage, Movable):
82
82
  """
83
83
  Real motors added to allow stops following pin load (e.g. real_x1.stop() )
84
84
  X1 and X2 real motors provide compound chi motion as well as the compound X travel,
@@ -92,7 +92,6 @@ class Smargon(XYZStage, Movable):
92
92
  with self.add_children_as_readables():
93
93
  self.chi = Motor(prefix + "CHI")
94
94
  self.phi = Motor(prefix + "PHI")
95
- self.omega = Motor(prefix + "OMEGA")
96
95
  self.real_x1 = Motor(prefix + "MOTOR_3")
97
96
  self.real_x2 = Motor(prefix + "MOTOR_4")
98
97
  self.real_y = Motor(prefix + "MOTOR_1")