dls-dodal 1.65.0__py3-none-any.whl → 1.67.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 (85) hide show
  1. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/METADATA +3 -4
  2. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/RECORD +82 -66
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/aithre.py +21 -2
  5. dodal/beamlines/i03.py +102 -198
  6. dodal/beamlines/i04.py +40 -4
  7. dodal/beamlines/i05.py +28 -1
  8. dodal/beamlines/i06.py +62 -0
  9. dodal/beamlines/i07.py +20 -0
  10. dodal/beamlines/i09_1.py +32 -3
  11. dodal/beamlines/i09_2.py +57 -2
  12. dodal/beamlines/i10_optics.py +46 -17
  13. dodal/beamlines/i17.py +7 -3
  14. dodal/beamlines/i18.py +3 -3
  15. dodal/beamlines/i19_1.py +26 -14
  16. dodal/beamlines/i19_2.py +49 -38
  17. dodal/beamlines/i21.py +2 -2
  18. dodal/beamlines/i22.py +19 -4
  19. dodal/beamlines/p38.py +3 -3
  20. dodal/beamlines/training_rig.py +0 -16
  21. dodal/cli.py +26 -12
  22. dodal/common/coordination.py +3 -2
  23. dodal/device_manager.py +604 -0
  24. dodal/devices/aithre_lasershaping/goniometer.py +26 -9
  25. dodal/devices/aperturescatterguard.py +3 -2
  26. dodal/devices/areadetector/plugins/mjpg.py +10 -3
  27. dodal/devices/beamsize/__init__.py +0 -0
  28. dodal/devices/beamsize/beamsize.py +6 -0
  29. dodal/devices/cryostream.py +28 -57
  30. dodal/devices/detector/det_resolution.py +4 -2
  31. dodal/devices/eiger.py +26 -18
  32. dodal/devices/fast_grid_scan.py +14 -2
  33. dodal/devices/i03/beamsize.py +35 -0
  34. dodal/devices/i03/constants.py +7 -0
  35. dodal/devices/i03/undulator_dcm.py +2 -2
  36. dodal/devices/i04/beamsize.py +45 -0
  37. dodal/devices/i04/max_pixel.py +38 -0
  38. dodal/devices/i04/murko_results.py +36 -26
  39. dodal/devices/i04/transfocator.py +23 -29
  40. dodal/devices/i07/id.py +38 -0
  41. dodal/devices/i09_1_shared/__init__.py +13 -2
  42. dodal/devices/i09_1_shared/hard_energy.py +112 -0
  43. dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
  44. dodal/devices/i09_2_shared/__init__.py +0 -0
  45. dodal/devices/i09_2_shared/i09_apple2.py +86 -0
  46. dodal/devices/i10/i10_apple2.py +39 -331
  47. dodal/devices/i17/i17_apple2.py +37 -22
  48. dodal/devices/i19/access_controlled/attenuator_motor_squad.py +61 -0
  49. dodal/devices/i19/access_controlled/blueapi_device.py +9 -1
  50. dodal/devices/i19/access_controlled/shutter.py +2 -4
  51. dodal/devices/insertion_device/__init__.py +0 -0
  52. dodal/devices/{apple2_undulator.py → insertion_device/apple2_undulator.py} +122 -69
  53. dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
  54. dodal/devices/insertion_device/lookup_table_models.py +287 -0
  55. dodal/devices/ipin.py +20 -2
  56. dodal/devices/motors.py +33 -3
  57. dodal/devices/mx_phase1/beamstop.py +31 -12
  58. dodal/devices/oav/oav_calculations.py +9 -4
  59. dodal/devices/oav/oav_detector.py +65 -7
  60. dodal/devices/oav/oav_parameters.py +3 -1
  61. dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
  62. dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
  63. dodal/devices/oav/pin_image_recognition/utils.py +23 -1
  64. dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
  65. dodal/devices/oav/utils.py +16 -6
  66. dodal/devices/robot.py +33 -18
  67. dodal/devices/scintillator.py +36 -14
  68. dodal/devices/smargon.py +2 -3
  69. dodal/devices/thawer.py +7 -45
  70. dodal/devices/undulator.py +152 -68
  71. dodal/plans/__init__.py +1 -1
  72. dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
  73. dodal/plans/load_panda_yaml.py +9 -0
  74. dodal/plans/verify_undulator_gap.py +2 -2
  75. dodal/testing/fixtures/devices/__init__.py +0 -0
  76. dodal/testing/fixtures/devices/apple2.py +78 -0
  77. dodal/utils.py +6 -3
  78. dodal/beamline_specific_utils/i03.py +0 -17
  79. dodal/testing/__init__.py +0 -3
  80. dodal/testing/setup.py +0 -67
  81. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/WHEEL +0 -0
  82. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/entry_points.txt +0 -0
  83. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/licenses/LICENSE +0 -0
  84. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/top_level.txt +0 -0
  85. /dodal/plans/{scanspec.py → spec_path.py} +0 -0
@@ -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/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
@@ -71,6 +71,7 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
71
71
  LOAD_TIMEOUT = 60
72
72
 
73
73
  # Error codes that we do special things on
74
+ NO_ERROR = 0
74
75
  NO_PIN_ERROR_CODE = 25
75
76
  LIGHT_CURTAIN_TRIPPED = 40
76
77
 
@@ -104,7 +105,6 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
104
105
  self.init = epics_signal_x(prefix + "INIT.PROC")
105
106
  self.soak = epics_signal_x(prefix + "SOAK.PROC")
106
107
  self.home = epics_signal_x(prefix + "GOHM.PROC")
107
- self.unload = epics_signal_x(prefix + "UNLD.PROC")
108
108
  self.dry = epics_signal_x(prefix + "DRY.PROC")
109
109
  self.open = epics_signal_x(prefix + "COLO.PROC")
110
110
  self.close = epics_signal_x(prefix + "COLC.PROC")
@@ -116,21 +116,25 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
116
116
  )
117
117
  super().__init__(name=name)
118
118
 
119
- async def pin_mounted_or_no_pin_found(self):
120
- """This co-routine will finish when either a pin is detected or the robot gives
121
- an error saying no pin was found (whichever happens first). In the case where no
122
- pin was found a RobotLoadError error is raised.
119
+ async def pin_state_or_error(self, expected_state=PinMounted.PIN_MOUNTED):
120
+ """This co-routine will finish when either the pin sensor reaches the specified
121
+ state or the robot gives an error (whichever happens first). In the case where
122
+ there is an error a RobotLoadError error is raised.
123
123
  """
124
124
 
125
- async def raise_if_no_pin():
126
- await wait_for_value(self.prog_error.code, self.NO_PIN_ERROR_CODE, None)
127
- raise RobotLoadError(self.NO_PIN_ERROR_CODE, "Pin was not detected")
125
+ async def raise_if_error():
126
+ await wait_for_value(
127
+ self.prog_error.code, lambda value: value != self.NO_ERROR, None
128
+ )
129
+ error_code = await self.prog_error.code.get_value()
130
+ error_msg = await self.prog_error.str.get_value()
131
+ raise RobotLoadError(error_code, error_msg)
128
132
 
129
133
  async def wfv():
130
- await wait_for_value(self.gonio_pin_sensor, PinMounted.PIN_MOUNTED, None)
134
+ await wait_for_value(self.gonio_pin_sensor, expected_state, None)
131
135
 
132
136
  tasks = [
133
- (Task(raise_if_no_pin())),
137
+ (Task(raise_if_error())),
134
138
  (Task(wfv())),
135
139
  ]
136
140
  try:
@@ -169,18 +173,29 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
169
173
  await self.load.trigger()
170
174
  if await self.gonio_pin_sensor.get_value() == PinMounted.PIN_MOUNTED:
171
175
  LOGGER.info(WAIT_FOR_OLD_PIN_MSG)
172
- await wait_for_value(self.gonio_pin_sensor, PinMounted.NO_PIN_MOUNTED, None)
176
+ await self.pin_state_or_error(PinMounted.NO_PIN_MOUNTED)
173
177
  LOGGER.info(WAIT_FOR_NEW_PIN_MSG)
174
178
 
175
- await self.pin_mounted_or_no_pin_found()
179
+ await self.pin_state_or_error()
176
180
 
177
181
  @AsyncStatus.wrap
178
- async def set(self, value: SampleLocation):
182
+ async def set(self, value: SampleLocation | None):
183
+ """
184
+ Perform a sample load from the specified sample location
185
+ Args:
186
+ value: The pin and puck to load, or None to unload the sample.
187
+ Raises:
188
+ RobotLoadError if a timeout occurs, or if an error occurs loading the smaple.
189
+ """
179
190
  try:
180
- await wait_for(
181
- self._load_pin_and_puck(value),
182
- timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
183
- )
191
+ if value is not None:
192
+ await wait_for(
193
+ self._load_pin_and_puck(value),
194
+ timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
195
+ )
196
+ else:
197
+ await self.unload.trigger(timeout=self.LOAD_TIMEOUT)
198
+ await wait_for_value(self.program_running, False, self.NOT_BUSY_TIMEOUT)
184
199
  except TimeoutError as e:
185
200
  await self.prog_error.raise_if_error(e)
186
201
  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,27 +46,46 @@ 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:
@@ -73,14 +93,16 @@ class Scintillator(StandardReadable):
73
93
  current_z = await self.z_mm.user_readback.get_value()
74
94
  if self._get_selected_position(current_y, current_z) == InOut.OUT:
75
95
  return
76
- if (
77
- self._aperture_scatterguard().selected_aperture.get_value()
78
- != ApertureValue.PARKED
79
- ):
80
- raise ValueError(
81
- "Cannot move scintillator out if aperture/scatterguard is not parked"
82
- )
96
+ await self._check_aperture_parked()
83
97
  await self.y_mm.set(self._scintillator_out_yz_mm[0])
84
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])
85
107
  case _:
86
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")
dodal/devices/thawer.py CHANGED
@@ -1,59 +1,21 @@
1
- from asyncio import CancelledError, Task, create_task, sleep
2
-
3
1
  from bluesky.protocols import Movable, Stoppable
4
2
  from ophyd_async.core import (
5
3
  AsyncStatus,
6
- Device,
7
4
  OnOff,
8
- Reference,
9
- SignalRW,
10
5
  StandardReadable,
11
6
  )
12
7
  from ophyd_async.epics.core import epics_signal_rw
13
8
 
14
- from dodal.log import LOGGER
15
-
16
-
17
- class ThawingTimer(Device, Stoppable, Movable[float]):
18
- def __init__(self, control_signal: SignalRW[OnOff]) -> None:
19
- self._control_signal_ref = Reference(control_signal)
20
- self._thawing_task: Task | None = None
21
- super().__init__("thaw_for_time_s")
22
9
 
23
- @AsyncStatus.wrap
24
- async def set(self, value: float):
25
- if self._thawing_task:
26
- LOGGER.info("Thawing task already in progress, resetting timer")
27
- self._thawing_task.cancel()
28
- else:
29
- LOGGER.info("Thawing started")
30
- await self._control_signal_ref().set(OnOff.ON)
31
- self._thawing_task = create_task(sleep(value))
32
- try:
33
- await self._thawing_task
34
- except CancelledError:
35
- LOGGER.info("Timer task cancelled.")
36
- raise
37
- else:
38
- LOGGER.info("Thawing completed")
39
- await self._control_signal_ref().set(OnOff.OFF)
40
-
41
- @AsyncStatus.wrap
42
- async def stop(self, *args, **kwargs):
43
- if self._thawing_task:
44
- self._thawing_task.cancel()
45
- self._thawing_task = None
46
- LOGGER.info("Thawer stopped.")
47
- await self._control_signal_ref().set(OnOff.OFF)
48
-
49
-
50
- class Thawer(StandardReadable, Stoppable):
10
+ class Thawer(StandardReadable, Stoppable, Movable[OnOff]):
51
11
  def __init__(self, prefix: str, name: str = "") -> None:
52
- self.control = epics_signal_rw(OnOff, prefix + ":CTRL")
53
- self.thaw_for_time_s = ThawingTimer(self.control)
12
+ self._control = epics_signal_rw(OnOff, prefix + ":CTRL")
54
13
  super().__init__(name)
55
14
 
15
+ @AsyncStatus.wrap
16
+ async def set(self, value: OnOff):
17
+ await self._control.set(value)
18
+
56
19
  @AsyncStatus.wrap
57
20
  async def stop(self, *args, **kwargs):
58
- await self.thaw_for_time_s.stop()
59
- await self.control.set(OnOff.OFF)
21
+ await self._control.set(OnOff.OFF)