dls-dodal 1.55.0__py3-none-any.whl → 1.56.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 (101) hide show
  1. {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/METADATA +3 -3
  2. {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/RECORD +100 -86
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/b01_1.py +6 -13
  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/i19_1.py +1 -0
  14. dodal/beamlines/i19_2.py +7 -0
  15. dodal/beamlines/i22.py +2 -2
  16. dodal/beamlines/i23.py +3 -3
  17. dodal/beamlines/i24.py +6 -14
  18. dodal/beamlines/p38.py +1 -0
  19. dodal/beamlines/p60.py +3 -2
  20. dodal/cli.py +11 -1
  21. dodal/common/__init__.py +4 -0
  22. dodal/common/beamlines/beamline_parameters.py +1 -1
  23. dodal/common/beamlines/beamline_utils.py +5 -1
  24. dodal/common/enums.py +19 -0
  25. dodal/common/watcher_utils.py +83 -0
  26. dodal/devices/aithre_lasershaping/laser_robot.py +4 -9
  27. dodal/devices/aperturescatterguard.py +52 -12
  28. dodal/devices/apple2_undulator.py +0 -1
  29. dodal/devices/attenuator/attenuator.py +3 -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/dcm.py +11 -15
  75. dodal/devices/i24/dual_backlight.py +11 -12
  76. dodal/devices/i24/pmac.py +8 -7
  77. dodal/devices/mx_phase1/beamstop.py +10 -11
  78. dodal/devices/oav/pin_image_recognition/__init__.py +0 -3
  79. dodal/devices/p60/enums.py +8 -8
  80. dodal/devices/p60/lab_xray_source.py +3 -2
  81. dodal/devices/pressure_jump_cell.py +77 -123
  82. dodal/devices/scintillator.py +76 -4
  83. dodal/devices/smargon.py +2 -2
  84. dodal/devices/synchrotron.py +1 -2
  85. dodal/devices/tetramm.py +13 -0
  86. dodal/devices/thawer.py +6 -11
  87. dodal/devices/undulator.py +3 -8
  88. dodal/devices/util/epics_util.py +1 -1
  89. dodal/devices/util/test_utils.py +19 -0
  90. dodal/devices/watsonmarlow323_pump.py +7 -7
  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 +0 -0
  95. dodal/testing/electron_analyser/__init__.py +6 -0
  96. dodal/testing/electron_analyser/device_factory.py +59 -0
  97. dodal/devices/CTAB.py +0 -41
  98. {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/WHEEL +0 -0
  99. {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/entry_points.txt +0 -0
  100. {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/licenses/LICENSE +0 -0
  101. {dls_dodal-1.55.0.dist-info → dls_dodal-1.56.0.dist-info}/top_level.txt +0 -0
@@ -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
@@ -42,7 +42,7 @@ class StubOffsets(Device):
42
42
  set them so that the current position is zero or to pre-defined positions.
43
43
  """
44
44
 
45
- def __init__(self, name: str = "", prefix: str = ""):
45
+ def __init__(self, prefix: str, name: str = ""):
46
46
  self.center_at_current_position = SetWhenEnabled(prefix=prefix + "CENTER_CS")
47
47
  self.to_robot_load = SetWhenEnabled(prefix=prefix + "SET_STUBS_TO_RL")
48
48
  super().__init__(name)
@@ -123,7 +123,7 @@ class Smargon(XYZStage, Movable):
123
123
  Robot loading can nudge these and lead to errors.
124
124
  """
125
125
 
126
- def __init__(self, prefix: str = "", name: str = ""):
126
+ def __init__(self, prefix: str, name: str = ""):
127
127
  with self.add_children_as_readables():
128
128
  self.chi = Motor(prefix + "CHI")
129
129
  self.phi = Motor(prefix + "PHI")
@@ -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/tetramm.py CHANGED
@@ -116,6 +116,19 @@ class TetrammController(DetectorController):
116
116
  self.set_exposure(trigger_info.livetime),
117
117
  )
118
118
 
119
+ # raise an error if asked to trigger faster than the max.
120
+ # possible speed for a tetramm
121
+ self._validate_deadtime(trigger_info)
122
+
123
+ def _validate_deadtime(self, value: TriggerInfo) -> None:
124
+ minimum_deadtime = self.get_deadtime(value.livetime)
125
+ if minimum_deadtime > value.deadtime:
126
+ msg = (
127
+ f"Tetramm {self} needs at least {minimum_deadtime}s "
128
+ f"deadtime, but trigger logic provides only {value.deadtime}s"
129
+ )
130
+ raise ValueError(msg)
131
+
119
132
  async def arm(self):
120
133
  self._arm_status = await self.start_acquiring_driver_and_ensure_status()
121
134
 
dodal/devices/thawer.py CHANGED
@@ -4,10 +4,10 @@ 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
 
@@ -16,27 +16,22 @@ class ThawingException(Exception):
16
16
  pass
17
17
 
18
18
 
19
- class ThawerStates(StrictEnum):
20
- OFF = "Off"
21
- ON = "On"
22
-
23
-
24
19
  class ThawingTimer(Device, Stoppable, Movable[float]):
25
- def __init__(self, control_signal: SignalRW[ThawerStates]) -> None:
20
+ def __init__(self, control_signal: SignalRW[OnOff]) -> None:
26
21
  self._control_signal_ref = Reference(control_signal)
27
22
  self._thawing_task: Task | None = None
28
23
  super().__init__("thaw_for_time_s")
29
24
 
30
25
  @AsyncStatus.wrap
31
26
  async def set(self, value: float):
32
- await self._control_signal_ref().set(ThawerStates.ON)
27
+ await self._control_signal_ref().set(OnOff.ON)
33
28
  if self._thawing_task and not self._thawing_task.done():
34
29
  raise ThawingException("Thawing task already in progress")
35
30
  self._thawing_task = create_task(sleep(value))
36
31
  try:
37
32
  await self._thawing_task
38
33
  finally:
39
- await self._control_signal_ref().set(ThawerStates.OFF)
34
+ await self._control_signal_ref().set(OnOff.OFF)
40
35
 
41
36
  @AsyncStatus.wrap
42
37
  async def stop(self, *args, **kwargs):
@@ -46,11 +41,11 @@ class ThawingTimer(Device, Stoppable, Movable[float]):
46
41
 
47
42
  class Thawer(StandardReadable, Stoppable):
48
43
  def __init__(self, prefix: str, name: str = "") -> None:
49
- self.control = epics_signal_rw(ThawerStates, prefix + ":CTRL")
44
+ self.control = epics_signal_rw(OnOff, prefix + ":CTRL")
50
45
  self.thaw_for_time_s = ThawingTimer(self.control)
51
46
  super().__init__(name)
52
47
 
53
48
  @AsyncStatus.wrap
54
49
  async def stop(self, *args, **kwargs):
55
50
  await self.thaw_for_time_s.stop()
56
- await self.control.set(ThawerStates.OFF)
51
+ await self.control.set(OnOff.OFF)
@@ -7,12 +7,12 @@ from ophyd_async.core import (
7
7
  AsyncStatus,
8
8
  StandardReadable,
9
9
  StandardReadableFormat,
10
- StrictEnum,
11
10
  soft_signal_r_and_setter,
12
11
  )
13
12
  from ophyd_async.epics.core import epics_signal_r
14
13
  from ophyd_async.epics.motor import Motor
15
14
 
15
+ from dodal.common.enums import EnabledDisabledUpper
16
16
  from dodal.log import LOGGER
17
17
 
18
18
  from .util.lookup_tables import energy_distance_table
@@ -33,11 +33,6 @@ UNDULATOR_DISCREPANCY_THRESHOLD_MM = 2e-3
33
33
  STATUS_TIMEOUT_S: float = 10.0
34
34
 
35
35
 
36
- class UndulatorGapAccess(StrictEnum):
37
- ENABLED = "ENABLED"
38
- DISABLED = "DISABLED"
39
-
40
-
41
36
  def _get_gap_for_energy(
42
37
  dcm_energy_ev: float, energy_to_distance_table: ndarray
43
38
  ) -> float:
@@ -73,7 +68,7 @@ class Undulator(StandardReadable, Movable[float]):
73
68
  with self.add_children_as_readables():
74
69
  self.gap_motor = Motor(prefix + "BLGAPMTR")
75
70
  self.current_gap = epics_signal_r(float, prefix + "CURRGAPD")
76
- self.gap_access = epics_signal_r(UndulatorGapAccess, prefix + "IDBLENA")
71
+ self.gap_access = epics_signal_r(EnabledDisabledUpper, prefix + "IDBLENA")
77
72
 
78
73
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
79
74
  self.gap_discrepancy_tolerance_mm, _ = soft_signal_r_and_setter(
@@ -110,7 +105,7 @@ class Undulator(StandardReadable, Movable[float]):
110
105
 
111
106
  async def raise_if_not_enabled(self):
112
107
  access_level = await self.gap_access.get_value()
113
- if access_level is UndulatorGapAccess.DISABLED and not TEST_MODE:
108
+ if access_level is EnabledDisabledUpper.DISABLED and not TEST_MODE:
114
109
  raise AccessError("Undulator gap access is disabled. Contact Control Room")
115
110
 
116
111
  async def _set_undulator_gap(self, energy_kev: float) -> None:
@@ -117,7 +117,7 @@ def call_func(func: Callable[[], StatusBase]) -> StatusBase:
117
117
  class SetWhenEnabled(OphydAsyncDevice, Movable[int]):
118
118
  """A device that sets the proc field of a PV when it becomes enabled."""
119
119
 
120
- def __init__(self, name: str = "", prefix: str = ""):
120
+ def __init__(self, prefix: str, name: str = ""):
121
121
  self.proc = epics_signal_rw(int, prefix + ".PROC")
122
122
  self.disp = epics_signal_r(int, prefix + ".DISP")
123
123
  super().__init__(name)
@@ -1,3 +1,6 @@
1
+ from contextlib import ExitStack
2
+
3
+ from ophyd_async.core import Device
1
4
  from ophyd_async.epics.motor import Motor
2
5
  from ophyd_async.testing import (
3
6
  callback_on_mock_put,
@@ -16,3 +19,19 @@ def patch_motor(motor: Motor, initial_position=0):
16
19
  motor.user_setpoint,
17
20
  lambda pos, *args, **kwargs: set_mock_value(motor.user_readback, pos),
18
21
  )
22
+
23
+
24
+ def patch_all_motors(parent_device: Device):
25
+ motors = []
26
+
27
+ def recursively_find_motors(device: Device):
28
+ for _, child_device in device.children():
29
+ if isinstance(child_device, Motor):
30
+ motors.append(child_device)
31
+ recursively_find_motors(child_device)
32
+
33
+ recursively_find_motors(parent_device)
34
+ motor_patch_stack = ExitStack()
35
+ for motor in motors:
36
+ motor_patch_stack.enter_context(patch_motor(motor))
37
+ return motor_patch_stack
@@ -1,12 +1,12 @@
1
- from ophyd_async.core import StandardReadable, StandardReadableFormat, StrictEnum
1
+ from ophyd_async.core import (
2
+ EnabledDisabled,
3
+ StandardReadable,
4
+ StandardReadableFormat,
5
+ StrictEnum,
6
+ )
2
7
  from ophyd_async.epics.core import epics_signal_rw
3
8
 
4
9
 
5
- class WatsonMarlow323PumpEnable(StrictEnum):
6
- DISABLED = "Disabled"
7
- ENABLED = "Enabled"
8
-
9
-
10
10
  class WatsonMarlow323PumpDirection(StrictEnum):
11
11
  CLOCKWISE = "CW"
12
12
  COUNTER_CLOCKWISE = "CCW"
@@ -38,7 +38,7 @@ class WatsonMarlow323Pump(StandardReadable):
38
38
 
39
39
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
40
40
  self.enabled = epics_signal_rw(
41
- WatsonMarlow323PumpEnable,
41
+ EnabledDisabled,
42
42
  prefix + "DISABLE",
43
43
  )
44
44
 
@@ -14,12 +14,10 @@ class XBPMFeedback(Device, Triggerable):
14
14
  """The XBPM feedback device is an IOC that moves the DCM, HFM and VFM to automatically
15
15
  hold the beam into place, as measured by the XBPM sensor."""
16
16
 
17
- def __init__(self, prefix: str = "", name: str = "xbpm_feedback") -> None:
18
- self.pos_ok = epics_signal_r(float, prefix + "-EA-FDBK-01:XBPM2POSITION_OK")
19
- self.pos_stable = epics_signal_r(float, prefix + "-EA-FDBK-01:XBPM2_STABLE")
20
- self.pause_feedback = epics_signal_rw(Pause, prefix + "-EA-FDBK-01:FB_PAUSE")
21
- self.x = epics_signal_r(float, prefix + "-EA-XBPM-02:PosX:MeanValue_RBV")
22
- self.y = epics_signal_r(float, prefix + "-EA-XBPM-02:PosY:MeanValue_RBV")
17
+ def __init__(self, prefix: str, name: str = "") -> None:
18
+ self.pos_ok = epics_signal_r(float, prefix + "XBPM2POSITION_OK")
19
+ self.pos_stable = epics_signal_r(float, prefix + "XBPM2_STABLE")
20
+ self.pause_feedback = epics_signal_rw(Pause, prefix + "FB_PAUSE")
23
21
  super().__init__(name=name)
24
22
 
25
23
  @AsyncStatus.wrap
@@ -34,11 +34,6 @@ class TriggerMode(StrictEnum):
34
34
  LVDS_BOTH = "LVDS Both"
35
35
 
36
36
 
37
- class UpdateRBV(StrictEnum):
38
- DISABLED = "Disabled"
39
- ENABLED = "Enabled"
40
-
41
-
42
37
  class AcquireRBVState(StrictEnum):
43
38
  DONE = "Done"
44
39
  ACQUIRE = "Acquiring"