dls-dodal 1.55.1__py3-none-any.whl → 1.57.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 (104) hide show
  1. {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/METADATA +3 -3
  2. {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/RECORD +101 -87
  3. dodal/_version.py +16 -3
  4. dodal/beamlines/b01_1.py +6 -1
  5. dodal/beamlines/b07.py +2 -1
  6. dodal/beamlines/b07_1.py +2 -1
  7. dodal/beamlines/b21.py +4 -24
  8. dodal/beamlines/i03.py +53 -53
  9. dodal/beamlines/i04.py +16 -38
  10. dodal/beamlines/i09.py +3 -2
  11. dodal/beamlines/i09_1.py +2 -1
  12. dodal/beamlines/i11.py +143 -0
  13. dodal/beamlines/i17.py +37 -0
  14. dodal/beamlines/i19_1.py +1 -0
  15. dodal/beamlines/i19_2.py +7 -0
  16. dodal/beamlines/i22.py +5 -5
  17. dodal/beamlines/i23.py +3 -3
  18. dodal/beamlines/i24.py +6 -33
  19. dodal/beamlines/p38.py +1 -0
  20. dodal/beamlines/p60.py +3 -2
  21. dodal/cli.py +11 -1
  22. dodal/common/__init__.py +4 -0
  23. dodal/common/beamlines/beamline_parameters.py +1 -1
  24. dodal/common/beamlines/beamline_utils.py +5 -1
  25. dodal/common/enums.py +19 -0
  26. dodal/common/watcher_utils.py +83 -0
  27. dodal/devices/aithre_lasershaping/laser_robot.py +4 -9
  28. dodal/devices/aperturescatterguard.py +52 -12
  29. dodal/devices/apple2_undulator.py +0 -1
  30. dodal/devices/b16/detector.py +1 -10
  31. dodal/devices/backlight.py +8 -20
  32. dodal/devices/bimorph_mirror.py +4 -7
  33. dodal/devices/collimation_table.py +36 -0
  34. dodal/devices/controllers.py +21 -0
  35. dodal/devices/cryostream.py +97 -7
  36. dodal/devices/current_amplifiers/femto.py +1 -1
  37. dodal/devices/detector/detector_motion.py +1 -7
  38. dodal/devices/eiger.py +22 -8
  39. dodal/devices/eiger_odin.py +2 -0
  40. dodal/devices/electron_analyser/__init__.py +2 -1
  41. dodal/devices/electron_analyser/abstract/__init__.py +0 -1
  42. dodal/devices/electron_analyser/abstract/base_detector.py +3 -25
  43. dodal/devices/electron_analyser/abstract/base_driver_io.py +18 -9
  44. dodal/devices/electron_analyser/abstract/base_region.py +34 -3
  45. dodal/devices/electron_analyser/detector.py +24 -0
  46. dodal/devices/electron_analyser/enums.py +5 -0
  47. dodal/devices/electron_analyser/specs/detector.py +2 -1
  48. dodal/devices/electron_analyser/specs/driver_io.py +21 -26
  49. dodal/devices/electron_analyser/specs/region.py +1 -1
  50. dodal/devices/electron_analyser/util.py +20 -0
  51. dodal/devices/electron_analyser/vgscienta/__init__.py +3 -3
  52. dodal/devices/electron_analyser/vgscienta/detector.py +2 -1
  53. dodal/devices/electron_analyser/vgscienta/driver_io.py +24 -32
  54. dodal/devices/electron_analyser/vgscienta/enums.py +0 -8
  55. dodal/devices/electron_analyser/vgscienta/region.py +2 -31
  56. dodal/devices/eurotherm.py +126 -0
  57. dodal/devices/fluorescence_detector_motion.py +3 -10
  58. dodal/devices/focusing_mirror.py +1 -1
  59. dodal/devices/i03/undulator_dcm.py +0 -1
  60. dodal/devices/i09/enums.py +8 -8
  61. dodal/devices/i10/diagnostics.py +4 -4
  62. dodal/devices/i10/i10_apple2.py +3 -6
  63. dodal/devices/i11/cyberstar_blower.py +34 -0
  64. dodal/devices/i11/diff_stages.py +55 -0
  65. dodal/devices/i11/mythen.py +165 -0
  66. dodal/devices/i11/nx100robot.py +153 -0
  67. dodal/devices/i11/spinner.py +30 -0
  68. dodal/devices/i13_1/merlin_controller.py +4 -4
  69. dodal/devices/i19/diffractometer.py +34 -0
  70. dodal/devices/i19/shutter.py +11 -1
  71. dodal/devices/i22/dcm.py +1 -1
  72. dodal/devices/i22/fswitch.py +3 -12
  73. dodal/devices/i24/aperture.py +3 -3
  74. dodal/devices/i24/beam_center.py +1 -2
  75. dodal/devices/i24/dcm.py +11 -15
  76. dodal/devices/i24/dual_backlight.py +11 -12
  77. dodal/devices/i24/pmac.py +8 -7
  78. dodal/devices/mx_phase1/beamstop.py +10 -11
  79. dodal/devices/oav/pin_image_recognition/__init__.py +0 -3
  80. dodal/devices/p60/enums.py +8 -8
  81. dodal/devices/p60/lab_xray_source.py +3 -2
  82. dodal/devices/pressure_jump_cell.py +77 -123
  83. dodal/devices/scintillator.py +76 -4
  84. dodal/devices/smargon.py +35 -18
  85. dodal/devices/synchrotron.py +1 -2
  86. dodal/devices/thawer.py +22 -15
  87. dodal/devices/undulator.py +3 -8
  88. dodal/devices/util/epics_util.py +1 -1
  89. dodal/devices/watsonmarlow323_pump.py +7 -7
  90. dodal/devices/webcam.py +1 -0
  91. dodal/devices/xbpm_feedback.py +4 -6
  92. dodal/devices/xspress3/xspress3.py +0 -5
  93. dodal/devices/zocalo/zocalo_results.py +1 -3
  94. dodal/testing/__init__.py +3 -0
  95. dodal/testing/electron_analyser/__init__.py +6 -0
  96. dodal/testing/electron_analyser/device_factory.py +59 -0
  97. dodal/testing/setup.py +67 -0
  98. dodal/devices/CTAB.py +0 -41
  99. dodal/devices/i24/pilatus_metadata.py +0 -44
  100. dodal/devices/util/test_utils.py +0 -37
  101. {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/WHEEL +0 -0
  102. {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/entry_points.txt +0 -0
  103. {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/licenses/LICENSE +0 -0
  104. {dls_dodal-1.55.1.dist-info → dls_dodal-1.57.0.dist-info}/top_level.txt +0 -0
dodal/devices/i24/pmac.py CHANGED
@@ -17,8 +17,9 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
17
17
 
18
18
  from dodal.devices.motors import XYZStage
19
19
 
20
- HOME_STR = r"\#1hmz\#2hmz\#3hmz" # Command to home the PMAC motors
20
+ HOME_STR = r"\#5hmz\#6hmz\#7hmz" # Command to home the PMAC motors
21
21
  ZERO_STR = "!x0y0z0" # Command to blend any ongoing move into new position
22
+ CS_STR = "&2"
22
23
 
23
24
 
24
25
  class ScanState(IntEnum):
@@ -196,12 +197,12 @@ class PMAC(XYZStage):
196
197
  """Device to control the chip stage on I24."""
197
198
 
198
199
  def __init__(self, prefix: str, name: str = "") -> None:
199
- self.pmac_string = epics_signal_rw(str, prefix + "PMAC_STRING")
200
+ self.pmac_string = epics_signal_rw(str, f"{prefix}-MO-IOC-13:PMAC:console")
200
201
  self.home = PMACStringMove(
201
202
  self.pmac_string,
202
- HOME_STR,
203
+ f"{CS_STR}{HOME_STR}",
203
204
  )
204
- self.to_xyz_zero = PMACStringMove(self.pmac_string, ZERO_STR)
205
+ self.to_xyz_zero = PMACStringMove(self.pmac_string, f"{CS_STR}{ZERO_STR}")
205
206
 
206
207
  self.laser = PMACStringLaser(self.pmac_string)
207
208
 
@@ -211,8 +212,8 @@ class PMAC(XYZStage):
211
212
 
212
213
  # These next signals are readback values on PVARS which are set by the motion
213
214
  # program.
214
- self.scanstatus = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2401")
215
- self.counter = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2402")
215
+ self.scanstatus = epics_signal_r(float, f"{prefix}-MO-STEP-13:pmac:read:P2401")
216
+ self.counter = epics_signal_r(float, f"{prefix}-MO-STEP-13:pmac:read:P2402")
216
217
 
217
218
  # A couple of soft signals for running a collection: program number to send to
218
219
  # the PMAC_STRING and expected collection time.
@@ -228,4 +229,4 @@ class PMAC(XYZStage):
228
229
  )
229
230
  self.abort_program = ProgramAbort(self.pmac_string, self.scanstatus)
230
231
 
231
- super().__init__(prefix, name)
232
+ super().__init__(f"{prefix}-MO-CHIP-01:", name)
@@ -18,9 +18,6 @@ class BeamstopPositions(StrictEnum):
18
18
  robot load however all 3 resolution positions are the same. We also
19
19
  do not use the robot load position in Hyperion.
20
20
 
21
- Until we support moving the beamstop it is only necessary to check whether the
22
- beamstop is in beam or not.
23
-
24
21
  See Also:
25
22
  https://github.com/DiamondLightSource/mx-bluesky/issues/484
26
23
 
@@ -86,11 +83,13 @@ class Beamstop(StandardReadable):
86
83
  return BeamstopPositions.UNKNOWN
87
84
 
88
85
  async def _set_selected_position(self, position: BeamstopPositions) -> None:
89
- if position == BeamstopPositions.DATA_COLLECTION:
90
- await asyncio.gather(
91
- self.x_mm.set(self._in_beam_xyz_mm[0]),
92
- self.y_mm.set(self._in_beam_xyz_mm[1]),
93
- self.z_mm.set(self._in_beam_xyz_mm[2]),
94
- )
95
- elif position == BeamstopPositions.UNKNOWN:
96
- raise ValueError(f"Cannot set beamstop to position {position}")
86
+ match position:
87
+ case BeamstopPositions.DATA_COLLECTION:
88
+ # Move z first as it could be under the table
89
+ await self.z_mm.set(self._in_beam_xyz_mm[2])
90
+ await asyncio.gather(
91
+ self.x_mm.set(self._in_beam_xyz_mm[0]),
92
+ self.y_mm.set(self._in_beam_xyz_mm[1]),
93
+ )
94
+ case _:
95
+ raise ValueError(f"Cannot set beamstop to position {position}")
@@ -48,9 +48,6 @@ class PinTipDetection(StandardReadable):
48
48
  INVALID_POSITION = np.array([np.iinfo(np.int32).min, np.iinfo(np.int32).min])
49
49
 
50
50
  def __init__(self, prefix: str, name: str = ""):
51
- self._prefix: str = prefix
52
- self._name = name
53
-
54
51
  self.triggered_tip, self._tip_setter = soft_signal_r_and_setter(
55
52
  Tip, name="triggered_tip"
56
53
  )
@@ -16,11 +16,11 @@ class PsuMode(StrictEnum):
16
16
 
17
17
 
18
18
  class PassEnergy(StrictEnum):
19
- E1 = 1
20
- E2 = 2
21
- E5 = 5
22
- E10 = 10
23
- E20 = 20
24
- E50 = 50
25
- E100 = 100
26
- E200 = 200
19
+ E1 = "1"
20
+ E2 = "2"
21
+ E5 = "5"
22
+ E10 = "10"
23
+ E20 = "20"
24
+ E50 = "50"
25
+ E100 = "100"
26
+ E200 = "200"
@@ -1,11 +1,12 @@
1
+ from enum import Enum
2
+
1
3
  from ophyd_async.core import (
2
4
  StandardReadable,
3
- StrictEnum,
4
5
  soft_signal_r_and_setter,
5
6
  )
6
7
 
7
8
 
8
- class LabXraySource(StrictEnum):
9
+ class LabXraySource(float, Enum):
9
10
  AL_KALPHA = 1486.6
10
11
  MG_KALPHA = 1253.6
11
12
 
@@ -1,7 +1,8 @@
1
1
  import asyncio
2
- from dataclasses import dataclass
2
+ from enum import IntEnum
3
+ from typing import Generic, TypeVar
3
4
 
4
- from bluesky.protocols import HasName, Movable
5
+ from bluesky.protocols import Movable
5
6
  from ophyd_async.core import (
6
7
  AsyncStatus,
7
8
  DeviceVector,
@@ -26,23 +27,23 @@ class StopState(StrictEnum):
26
27
  STOP = "STOP"
27
28
 
28
29
 
29
- class FastValveControlRequest(StrictEnum):
30
+ class ValveControlRequest(StrictEnum):
30
31
  OPEN = "Open"
31
32
  CLOSE = "Close"
32
33
  RESET = "Reset"
33
- ARM = "Arm"
34
- DISARM = "Disarm"
35
34
 
36
35
 
37
- class ValveControlRequest(StrictEnum):
38
- OPEN = "Open"
39
- CLOSE = "Close"
40
- RESET = "Reset"
36
+ class FastValveControlRequest(StrictEnum):
37
+ OPEN = ValveControlRequest.OPEN.value
38
+ CLOSE = ValveControlRequest.CLOSE.value
39
+ RESET = ValveControlRequest.RESET.value
40
+ ARM = "Arm"
41
+ DISARM = "Disarm"
41
42
 
42
43
 
43
- class ValveOpenSeqRequest(StrictEnum):
44
- INACTIVE = "0"
45
- OPEN_SEQ = "1"
44
+ class ValveOpenSeqRequest(IntEnum):
45
+ INACTIVE = 0
46
+ OPEN_SEQ = 1
46
47
 
47
48
 
48
49
  class PumpMotorDirectionState(StrictEnum):
@@ -60,138 +61,89 @@ class ValveState(StrictEnum):
60
61
 
61
62
 
62
63
  class FastValveState(StrictEnum):
63
- FAULT = "Fault"
64
- OPEN = "Open"
64
+ FAULT = ValveState.FAULT.value
65
+ OPEN = ValveState.OPEN.value
65
66
  OPEN_ARMED = "Open Armed"
66
- CLOSED = "Closed"
67
+ CLOSED = ValveState.CLOSED.value
67
68
  CLOSED_ARMED = "Closed Armed"
68
69
  NONE = "Unused"
69
70
 
70
71
 
71
- @dataclass
72
- class AllValvesControlState:
73
- valve_1: ValveControlRequest | None = None
74
- valve_3: ValveControlRequest | None = None
75
- valve_5: FastValveControlRequest | None = None
76
- valve_6: FastValveControlRequest | None = None
72
+ TValveControlRequest = TypeVar(
73
+ "TValveControlRequest", bound=ValveControlRequest | FastValveControlRequest
74
+ )
75
+
76
+
77
+ class ValveControl(
78
+ StandardReadable, Movable[TValveControlRequest], Generic[TValveControlRequest]
79
+ ):
80
+ def __init__(
81
+ self,
82
+ prefix: str,
83
+ valve_control_type: type[TValveControlRequest],
84
+ name: str = "",
85
+ ):
86
+ with self.add_children_as_readables():
87
+ self.control = epics_signal_rw(valve_control_type, prefix + ":CON")
88
+ self.open = epics_signal_rw(int, prefix + ":OPENSEQ")
89
+ super().__init__(name)
90
+
91
+ @AsyncStatus.wrap
92
+ async def set(self, value: TValveControlRequest):
93
+ if value.value == "Open":
94
+ await self.open.set(ValveOpenSeqRequest.OPEN_SEQ)
95
+ await asyncio.sleep(OPENSEQ_PULSE_LENGTH)
96
+ await self.open.set(ValveOpenSeqRequest.INACTIVE)
97
+ else:
98
+ await self.control.set(value)
77
99
 
78
100
 
79
- class AllValvesControl(StandardReadable, Movable[AllValvesControlState]):
101
+ class AllValvesControl(StandardReadable):
80
102
  """
81
- valves 2, 4, 7, 8 are not controlled by the IOC,
82
- as they are under manual control.
83
- fast_valves: tuple[int, ...] = (5, 6)
84
- slow_valves: tuple[int, ...] = (1, 3)
103
+ The default IOC for this device only controls
104
+ specific valves. Other valves are under manual
105
+ control.
85
106
  """
86
107
 
87
108
  def __init__(
88
109
  self,
89
110
  prefix: str,
90
111
  name: str = "",
91
- fast_valves: tuple[int, ...] = (5, 6),
92
- slow_valves: tuple[int, ...] = (1, 3),
112
+ fast_valves_numbers: tuple[int, ...] = (5, 6),
113
+ slow_valves_numbers: tuple[int, ...] = (1, 3),
93
114
  ) -> None:
94
- self.fast_valves = fast_valves
95
- self.slow_valves = slow_valves
96
115
  with self.add_children_as_readables():
97
116
  self.valve_states: DeviceVector[SignalR[ValveState]] = DeviceVector(
98
117
  {
99
118
  i: epics_signal_r(ValveState, f"{prefix}V{i}:STA")
100
- for i in self.slow_valves
119
+ for i in slow_valves_numbers
101
120
  }
102
121
  )
103
122
  self.fast_valve_states: DeviceVector[SignalR[FastValveState]] = (
104
123
  DeviceVector(
105
124
  {
106
125
  i: epics_signal_r(FastValveState, f"{prefix}V{i}:STA")
107
- for i in self.fast_valves
126
+ for i in fast_valves_numbers
108
127
  }
109
128
  )
110
129
  )
111
130
 
112
- self.fast_valve_control: DeviceVector[FastValveControl] = DeviceVector(
113
- {i: FastValveControl(f"{prefix}V{i}") for i in self.fast_valves}
114
- )
115
-
116
- self.valve_control: DeviceVector[ValveControl] = DeviceVector(
117
- {i: ValveControl(f"{prefix}V{i}") for i in self.slow_valves}
118
- )
119
-
120
- super().__init__(name)
121
-
122
- async def set_valve(
123
- self,
124
- valve: int,
125
- value: ValveControlRequest | FastValveControlRequest,
126
- ):
127
- if valve in self.slow_valves and (isinstance(value, ValveControlRequest)):
128
- if value == ValveControlRequest.OPEN:
129
- await self.valve_control[valve].set(ValveOpenSeqRequest.OPEN_SEQ)
130
- await asyncio.sleep(OPENSEQ_PULSE_LENGTH)
131
- await self.valve_control[valve].set(ValveOpenSeqRequest.INACTIVE)
132
- else:
133
- await self.valve_control[valve].set(value)
134
-
135
- elif valve in self.fast_valves and (isinstance(value, FastValveControlRequest)):
136
- if value == FastValveControlRequest.OPEN:
137
- await self.fast_valve_control[valve].set(ValveOpenSeqRequest.OPEN_SEQ)
138
- await asyncio.sleep(OPENSEQ_PULSE_LENGTH)
139
- await self.fast_valve_control[valve].set(ValveOpenSeqRequest.INACTIVE)
140
- else:
141
- await self.fast_valve_control[valve].set(value)
142
-
143
- @AsyncStatus.wrap
144
- async def set(self, value: AllValvesControlState):
145
- await asyncio.gather(
146
- *(
147
- self.set_valve(int(i[-1]), value)
148
- for i, value in value.__dict__.items()
149
- if value is not None
150
- )
151
- )
152
-
153
-
154
- class ValveControl(
155
- StandardReadable, Movable[ValveControlRequest | ValveOpenSeqRequest]
156
- ):
157
- def __init__(self, prefix: str, name: str = "") -> None:
158
- with self.add_children_as_readables():
159
- self.close = epics_signal_rw(ValveControlRequest, prefix + ":CON")
160
- self.open = epics_signal_rw(int, prefix + ":OPENSEQ")
161
-
162
- super().__init__(name)
163
-
164
- def set(self, value: ValveControlRequest | ValveOpenSeqRequest) -> AsyncStatus:
165
- set_status = None
166
-
167
- if isinstance(value, ValveControlRequest):
168
- set_status = self.close.set(value)
169
- elif isinstance(value, ValveOpenSeqRequest):
170
- set_status = self.open.set(value.value)
131
+ self.fast_valve_control = {
132
+ i: ValveControl(f"{prefix}V{i}", FastValveControlRequest)
133
+ for i in fast_valves_numbers
134
+ }
171
135
 
172
- return set_status
136
+ self.slow_valve_control = {
137
+ i: ValveControl(f"{prefix}V{i}", ValveControlRequest)
138
+ for i in slow_valves_numbers
139
+ }
173
140
 
141
+ all_valves = self.fast_valve_control | self.slow_valve_control
174
142
 
175
- class FastValveControl(
176
- StandardReadable, Movable[FastValveControlRequest | ValveOpenSeqRequest]
177
- ):
178
- def __init__(self, prefix: str, name: str = "") -> None:
179
- with self.add_children_as_readables():
180
- self.close = epics_signal_rw(FastValveControlRequest, prefix + ":CON")
181
- self.open = epics_signal_rw(int, prefix + ":OPENSEQ")
143
+ self.valve_control: DeviceVector[ValveControl] = DeviceVector(all_valves)
182
144
 
183
145
  super().__init__(name)
184
146
 
185
- def set(self, value: FastValveControlRequest | ValveOpenSeqRequest) -> AsyncStatus:
186
- set_status = None
187
-
188
- if isinstance(value, FastValveControlRequest):
189
- set_status = self.close.set(value)
190
- elif isinstance(value, ValveOpenSeqRequest):
191
- set_status = self.open.set(value.value)
192
-
193
- return set_status
194
-
195
147
 
196
148
  class Pump(StandardReadable):
197
149
  def __init__(self, prefix: str, name: str = "") -> None:
@@ -249,25 +201,27 @@ class PressureTransducer(StandardReadable):
249
201
  super().__init__(name)
250
202
 
251
203
 
252
- class PressureJumpCellController(HasName):
204
+ class PressureJumpCellController(StandardReadable):
205
+ """
206
+ Top-level control for a fixed pressure or pressure jumps.
207
+ """
208
+
253
209
  def __init__(self, prefix: str, name: str = "") -> None:
254
- self.stop = epics_signal_rw(StopState, f"{prefix}STOP")
210
+ with self.add_children_as_readables():
211
+ self.stop = epics_signal_rw(StopState, f"{prefix}STOP")
255
212
 
256
- self.target_pressure = epics_signal_rw(float, f"{prefix}TARGET")
257
- self.timeout = epics_signal_rw(float, f"{prefix}TIMER.HIGH")
258
- self.go = epics_signal_rw(bool, f"{prefix}GO")
213
+ self.target_pressure = epics_signal_rw(float, f"{prefix}TARGET")
214
+ self.timeout = epics_signal_rw(float, f"{prefix}TIMER.HIGH")
215
+ self.go = epics_signal_rw(bool, f"{prefix}GO")
259
216
 
260
- ## Jump logic ##
261
- self.start_pressure = epics_signal_rw(float, f"{prefix}JUMPF")
262
- self.target_pressure = epics_signal_rw(float, f"{prefix}JUMPT")
263
- self.jump_ready = epics_signal_rw(bool, f"{prefix}SETJUMP")
217
+ self.from_pressure = epics_signal_rw(float, f"{prefix}JUMPF")
218
+ self.to_pressure = epics_signal_rw(float, f"{prefix}JUMPT")
219
+ self.jump_ready = epics_signal_rw(bool, f"{prefix}SETJUMP")
264
220
 
265
- self._name = name
266
- super().__init__()
221
+ self.result = epics_signal_r(str, f"{prefix}RESULT")
267
222
 
268
- @property
269
- def name(self):
270
- return self._name
223
+ self._name = name
224
+ super().__init__(name)
271
225
 
272
226
 
273
227
  class PressureJumpCell(StandardReadable):
@@ -1,10 +1,82 @@
1
- from ophyd_async.core import StandardReadable
1
+ from math import isclose
2
+
3
+ from ophyd_async.core import Reference, StandardReadable, StrictEnum, derived_signal_rw
2
4
  from ophyd_async.epics.motor import Motor
3
5
 
6
+ from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
7
+ from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue
8
+
9
+
10
+ class InOut(StrictEnum):
11
+ """Currently Hyperion only needs to move the scintillator out for data collection."""
12
+
13
+ OUT = "Out"
14
+ UNKNOWN = "Unknown"
15
+
4
16
 
5
17
  class Scintillator(StandardReadable):
6
- def __init__(self, prefix: str, name: str = ""):
18
+ """Moves a scintillator into and out of the beam.
19
+
20
+ The scintillator will light up when hit with xrays, this allows scientists to use it
21
+ in conjunction with the optical OAV camera to commission the beamline.
22
+
23
+ When moved out of the beam it is parked under the table. This parking has a potential
24
+ to collide with the aperture/scatterguard if that is not correctly parked already.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ prefix: str,
30
+ aperture_scatterguard: Reference[ApertureScatterguard],
31
+ beamline_parameters: GDABeamlineParameters,
32
+ name: str = "",
33
+ ):
7
34
  with self.add_children_as_readables():
8
- self.y = Motor(prefix + "-MO-SCIN-01:Y")
9
- self.z = Motor(prefix + "-MO-SCIN-01:Z")
35
+ self.y_mm = Motor(f"{prefix}Y")
36
+ self.z_mm = Motor(f"{prefix}Z")
37
+ self.selected_pos = derived_signal_rw(
38
+ self._get_selected_position,
39
+ self._set_selected_position,
40
+ y=self.y_mm,
41
+ z=self.z_mm,
42
+ )
43
+
44
+ self._aperture_scatterguard = aperture_scatterguard
45
+ self._scintillator_out_yz_mm = [
46
+ float(beamline_parameters[f"scin_{axis}_SCIN_OUT"]) for axis in ("y", "z")
47
+ ]
48
+ self._yz_tolerance_mm = [
49
+ float(beamline_parameters[f"scin_{axis}_tolerance"]) for axis in ("y", "z")
50
+ ]
51
+
10
52
  super().__init__(name)
53
+
54
+ def _get_selected_position(self, y: float, z: float) -> InOut:
55
+ current_pos = [y, z]
56
+ if all(
57
+ isclose(axis_pos, axis_in_beam, abs_tol=axis_tolerance)
58
+ for axis_pos, axis_in_beam, axis_tolerance in zip(
59
+ current_pos,
60
+ self._scintillator_out_yz_mm,
61
+ self._yz_tolerance_mm,
62
+ strict=False,
63
+ )
64
+ ):
65
+ return InOut.OUT
66
+ else:
67
+ return InOut.UNKNOWN
68
+
69
+ async def _set_selected_position(self, position: InOut) -> None:
70
+ match position:
71
+ 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
+ )
79
+ await self.y_mm.set(self._scintillator_out_yz_mm[0])
80
+ await self.z_mm.set(self._scintillator_out_yz_mm[1])
81
+ case _:
82
+ raise ValueError(f"Cannot set scintillator to position {position}")
dodal/devices/smargon.py CHANGED
@@ -3,7 +3,7 @@ from collections.abc import Collection, Generator
3
3
  from dataclasses import dataclass
4
4
  from enum import Enum
5
5
  from math import isclose
6
- from typing import NotRequired, TypedDict, cast
6
+ from typing import TypedDict, cast
7
7
 
8
8
  from bluesky import plan_stubs as bps
9
9
  from bluesky.protocols import Movable
@@ -12,6 +12,7 @@ from ophyd_async.core import (
12
12
  AsyncStatus,
13
13
  Device,
14
14
  StrictEnum,
15
+ set_and_wait_for_value,
15
16
  wait_for_value,
16
17
  )
17
18
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
@@ -42,7 +43,7 @@ class StubOffsets(Device):
42
43
  set them so that the current position is zero or to pre-defined positions.
43
44
  """
44
45
 
45
- def __init__(self, name: str = "", prefix: str = ""):
46
+ def __init__(self, prefix: str, name: str = ""):
46
47
  self.center_at_current_position = SetWhenEnabled(prefix=prefix + "CENTER_CS")
47
48
  self.to_robot_load = SetWhenEnabled(prefix=prefix + "SET_STUBS_TO_RL")
48
49
  super().__init__(name)
@@ -104,15 +105,15 @@ class DeferMoves(StrictEnum):
104
105
  OFF = "Defer Off"
105
106
 
106
107
 
107
- class CombinedMove(TypedDict):
108
+ class CombinedMove(TypedDict, total=False):
108
109
  """A move on multiple axes at once using a deferred move"""
109
110
 
110
- x: NotRequired[float | None]
111
- y: NotRequired[float | None]
112
- z: NotRequired[float | None]
113
- omega: NotRequired[float | None]
114
- phi: NotRequired[float | None]
115
- chi: NotRequired[float | None]
111
+ x: float | None
112
+ y: float | None
113
+ z: float | None
114
+ omega: float | None
115
+ phi: float | None
116
+ chi: float | None
116
117
 
117
118
 
118
119
  class Smargon(XYZStage, Movable):
@@ -123,7 +124,9 @@ class Smargon(XYZStage, Movable):
123
124
  Robot loading can nudge these and lead to errors.
124
125
  """
125
126
 
126
- def __init__(self, prefix: str = "", name: str = ""):
127
+ DEFERRED_MOVE_SET_TIMEOUT = 5
128
+
129
+ def __init__(self, prefix: str, name: str = ""):
127
130
  with self.add_children_as_readables():
128
131
  self.chi = Motor(prefix + "CHI")
129
132
  self.phi = Motor(prefix + "PHI")
@@ -161,15 +164,29 @@ class Smargon(XYZStage, Movable):
161
164
 
162
165
  @AsyncStatus.wrap
163
166
  async def set(self, value: CombinedMove):
167
+ """This will move all motion together in a deferred move.
168
+
169
+ Once defer_move is on, sets to any axis do not immediately move the axis. Instead
170
+ the setpoint will go to that value. Then, when defer_move is switched off all
171
+ axes will move at the same time. The put callbacks on the axes themselves will
172
+ only come back after the motion on that axis finished.
173
+ """
164
174
  await self.defer_move.set(DeferMoves.ON)
165
175
  try:
166
- tasks = []
167
- for k, v in value.items():
168
- if v is not None:
169
- tasks.append(getattr(self, k).set(v))
176
+ finished_moving = []
177
+ for motor_name, new_setpoint in value.items():
178
+ if new_setpoint is not None and isinstance(new_setpoint, int | float):
179
+ axis: Motor = getattr(self, motor_name)
180
+ await axis.check_motor_limit(
181
+ await axis.user_setpoint.get_value(), new_setpoint
182
+ )
183
+ put_completion = await set_and_wait_for_value(
184
+ axis.user_setpoint,
185
+ new_setpoint,
186
+ timeout=self.DEFERRED_MOVE_SET_TIMEOUT,
187
+ wait_for_set_completion=False,
188
+ )
189
+ finished_moving.append(put_completion)
170
190
  finally:
171
191
  await self.defer_move.set(DeferMoves.OFF)
172
- # The set() coroutines will not complete until after defer moves has been
173
- # switched back off so we cannot wait for them until this point.
174
- # see https://github.com/DiamondLightSource/dodal/issues/1315
175
- await asyncio.gather(*tasks)
192
+ await asyncio.gather(*finished_moving)
@@ -38,8 +38,7 @@ class SynchrotronMode(StrictEnum):
38
38
  class Synchrotron(StandardReadable):
39
39
  def __init__(
40
40
  self,
41
- prefix: str = "",
42
- name: str = "synchrotron",
41
+ name: str = "",
43
42
  *,
44
43
  signal_prefix=Prefix.SIGNAL,
45
44
  status_prefix=Prefix.STATUS,
dodal/devices/thawer.py CHANGED
@@ -1,56 +1,63 @@
1
- from asyncio import Task, create_task, sleep
1
+ from asyncio import CancelledError, Task, create_task, sleep
2
2
 
3
3
  from bluesky.protocols import Movable, Stoppable
4
4
  from ophyd_async.core import (
5
5
  AsyncStatus,
6
6
  Device,
7
+ OnOff,
7
8
  Reference,
8
9
  SignalRW,
9
10
  StandardReadable,
10
- StrictEnum,
11
11
  )
12
12
  from ophyd_async.epics.core import epics_signal_rw
13
13
 
14
+ from dodal.log import LOGGER
15
+
14
16
 
15
17
  class ThawingException(Exception):
16
18
  pass
17
19
 
18
20
 
19
- class ThawerStates(StrictEnum):
20
- OFF = "Off"
21
- ON = "On"
22
-
23
-
24
21
  class ThawingTimer(Device, Stoppable, Movable[float]):
25
- def __init__(self, control_signal: SignalRW[ThawerStates]) -> None:
22
+ def __init__(self, control_signal: SignalRW[OnOff]) -> None:
26
23
  self._control_signal_ref = Reference(control_signal)
27
24
  self._thawing_task: Task | None = None
28
25
  super().__init__("thaw_for_time_s")
29
26
 
30
27
  @AsyncStatus.wrap
31
28
  async def set(self, value: float):
32
- await self._control_signal_ref().set(ThawerStates.ON)
33
- if self._thawing_task and not self._thawing_task.done():
34
- raise ThawingException("Thawing task already in progress")
29
+ if self._thawing_task:
30
+ LOGGER.info("Thawing task already in progress, resetting timer")
31
+ self._thawing_task.cancel()
32
+ else:
33
+ LOGGER.info("Thawing started")
34
+ await self._control_signal_ref().set(OnOff.ON)
35
35
  self._thawing_task = create_task(sleep(value))
36
36
  try:
37
37
  await self._thawing_task
38
- finally:
39
- await self._control_signal_ref().set(ThawerStates.OFF)
38
+ except CancelledError:
39
+ LOGGER.info("Timer task cancelled.")
40
+ raise
41
+ else:
42
+ LOGGER.info("Thawing completed")
43
+ await self._control_signal_ref().set(OnOff.OFF)
40
44
 
41
45
  @AsyncStatus.wrap
42
46
  async def stop(self, *args, **kwargs):
43
47
  if self._thawing_task:
44
48
  self._thawing_task.cancel()
49
+ self._thawing_task = None
50
+ LOGGER.info("Thawer stopped.")
51
+ await self._control_signal_ref().set(OnOff.OFF)
45
52
 
46
53
 
47
54
  class Thawer(StandardReadable, Stoppable):
48
55
  def __init__(self, prefix: str, name: str = "") -> None:
49
- self.control = epics_signal_rw(ThawerStates, prefix + ":CTRL")
56
+ self.control = epics_signal_rw(OnOff, prefix + ":CTRL")
50
57
  self.thaw_for_time_s = ThawingTimer(self.control)
51
58
  super().__init__(name)
52
59
 
53
60
  @AsyncStatus.wrap
54
61
  async def stop(self, *args, **kwargs):
55
62
  await self.thaw_for_time_s.stop()
56
- await self.control.set(ThawerStates.OFF)
63
+ await self.control.set(OnOff.OFF)