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.
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/METADATA +3 -4
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/RECORD +82 -66
- dodal/_version.py +2 -2
- dodal/beamlines/aithre.py +21 -2
- dodal/beamlines/i03.py +102 -198
- dodal/beamlines/i04.py +40 -4
- dodal/beamlines/i05.py +28 -1
- dodal/beamlines/i06.py +62 -0
- dodal/beamlines/i07.py +20 -0
- dodal/beamlines/i09_1.py +32 -3
- dodal/beamlines/i09_2.py +57 -2
- dodal/beamlines/i10_optics.py +46 -17
- dodal/beamlines/i17.py +7 -3
- dodal/beamlines/i18.py +3 -3
- dodal/beamlines/i19_1.py +26 -14
- dodal/beamlines/i19_2.py +49 -38
- dodal/beamlines/i21.py +2 -2
- dodal/beamlines/i22.py +19 -4
- dodal/beamlines/p38.py +3 -3
- dodal/beamlines/training_rig.py +0 -16
- dodal/cli.py +26 -12
- dodal/common/coordination.py +3 -2
- dodal/device_manager.py +604 -0
- dodal/devices/aithre_lasershaping/goniometer.py +26 -9
- dodal/devices/aperturescatterguard.py +3 -2
- dodal/devices/areadetector/plugins/mjpg.py +10 -3
- dodal/devices/beamsize/__init__.py +0 -0
- dodal/devices/beamsize/beamsize.py +6 -0
- dodal/devices/cryostream.py +28 -57
- dodal/devices/detector/det_resolution.py +4 -2
- dodal/devices/eiger.py +26 -18
- dodal/devices/fast_grid_scan.py +14 -2
- dodal/devices/i03/beamsize.py +35 -0
- dodal/devices/i03/constants.py +7 -0
- dodal/devices/i03/undulator_dcm.py +2 -2
- dodal/devices/i04/beamsize.py +45 -0
- dodal/devices/i04/max_pixel.py +38 -0
- dodal/devices/i04/murko_results.py +36 -26
- dodal/devices/i04/transfocator.py +23 -29
- dodal/devices/i07/id.py +38 -0
- dodal/devices/i09_1_shared/__init__.py +13 -2
- dodal/devices/i09_1_shared/hard_energy.py +112 -0
- dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
- dodal/devices/i09_2_shared/__init__.py +0 -0
- dodal/devices/i09_2_shared/i09_apple2.py +86 -0
- dodal/devices/i10/i10_apple2.py +39 -331
- dodal/devices/i17/i17_apple2.py +37 -22
- dodal/devices/i19/access_controlled/attenuator_motor_squad.py +61 -0
- dodal/devices/i19/access_controlled/blueapi_device.py +9 -1
- dodal/devices/i19/access_controlled/shutter.py +2 -4
- dodal/devices/insertion_device/__init__.py +0 -0
- dodal/devices/{apple2_undulator.py → insertion_device/apple2_undulator.py} +122 -69
- dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
- dodal/devices/insertion_device/lookup_table_models.py +287 -0
- dodal/devices/ipin.py +20 -2
- dodal/devices/motors.py +33 -3
- dodal/devices/mx_phase1/beamstop.py +31 -12
- dodal/devices/oav/oav_calculations.py +9 -4
- dodal/devices/oav/oav_detector.py +65 -7
- dodal/devices/oav/oav_parameters.py +3 -1
- dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
- dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
- dodal/devices/oav/pin_image_recognition/utils.py +23 -1
- dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
- dodal/devices/oav/utils.py +16 -6
- dodal/devices/robot.py +33 -18
- dodal/devices/scintillator.py +36 -14
- dodal/devices/smargon.py +2 -3
- dodal/devices/thawer.py +7 -45
- dodal/devices/undulator.py +152 -68
- dodal/plans/__init__.py +1 -1
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
- dodal/plans/load_panda_yaml.py +9 -0
- dodal/plans/verify_undulator_gap.py +2 -2
- dodal/testing/fixtures/devices/__init__.py +0 -0
- dodal/testing/fixtures/devices/apple2.py +78 -0
- dodal/utils.py +6 -3
- dodal/beamline_specific_utils/i03.py +0 -17
- dodal/testing/__init__.py +0 -3
- dodal/testing/setup.py +0 -67
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/top_level.txt +0 -0
- /dodal/plans/{scanspec.py → spec_path.py} +0 -0
|
@@ -23,11 +23,13 @@ from ophyd_async.core import (
|
|
|
23
23
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
|
|
24
24
|
from ophyd_async.epics.motor import Motor
|
|
25
25
|
|
|
26
|
+
from dodal.common.enums import EnabledDisabledUpper
|
|
26
27
|
from dodal.log import LOGGER
|
|
27
28
|
|
|
28
29
|
T = TypeVar("T")
|
|
29
30
|
|
|
30
31
|
DEFAULT_MOTOR_MIN_TIMEOUT = 10
|
|
32
|
+
MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
class UndulatorGateStatus(StrictEnum):
|
|
@@ -36,16 +38,24 @@ class UndulatorGateStatus(StrictEnum):
|
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
@dataclass
|
|
39
|
-
class
|
|
41
|
+
class Apple2LockedPhasesVal:
|
|
40
42
|
top_outer: str
|
|
41
|
-
top_inner: str
|
|
42
43
|
btm_inner: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Apple2PhasesVal(Apple2LockedPhasesVal):
|
|
48
|
+
top_inner: str
|
|
43
49
|
btm_outer: str
|
|
44
50
|
|
|
45
51
|
|
|
46
52
|
@dataclass
|
|
47
|
-
class Apple2Val
|
|
53
|
+
class Apple2Val:
|
|
48
54
|
gap: str
|
|
55
|
+
phase: Apple2LockedPhasesVal | Apple2PhasesVal
|
|
56
|
+
|
|
57
|
+
def extract_phase_val(self):
|
|
58
|
+
return self.phase
|
|
49
59
|
|
|
50
60
|
|
|
51
61
|
class Pol(StrictEnum):
|
|
@@ -80,10 +90,7 @@ class SafeUndulatorMover(StandardReadable, Movable[T], Generic[T]):
|
|
|
80
90
|
def __init__(self, set_move: SignalW, prefix: str, name: str = ""):
|
|
81
91
|
# Gate keeper open when move is requested, closed when move is completed
|
|
82
92
|
self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
|
|
83
|
-
|
|
84
|
-
split_pv = prefix.split("-")
|
|
85
|
-
fault_pv = f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT"
|
|
86
|
-
self.fault = epics_signal_r(float, fault_pv)
|
|
93
|
+
self.status = epics_signal_r(EnabledDisabledUpper, prefix + "IDBLENA")
|
|
87
94
|
self.set_move = set_move
|
|
88
95
|
super().__init__(name)
|
|
89
96
|
|
|
@@ -91,14 +98,14 @@ class SafeUndulatorMover(StandardReadable, Movable[T], Generic[T]):
|
|
|
91
98
|
async def set(self, value: T) -> None:
|
|
92
99
|
LOGGER.info(f"Setting {self.name} to {value}")
|
|
93
100
|
await self.raise_if_cannot_move()
|
|
94
|
-
await self.
|
|
101
|
+
await self.set_demand_positions(value)
|
|
95
102
|
timeout = await self.get_timeout()
|
|
96
103
|
LOGGER.info(f"Moving {self.name} to {value} with timeout = {timeout}")
|
|
97
104
|
await self.set_move.set(value=1, timeout=timeout)
|
|
98
105
|
await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
|
|
99
106
|
|
|
100
107
|
@abc.abstractmethod
|
|
101
|
-
async def
|
|
108
|
+
async def set_demand_positions(self, value: T) -> None:
|
|
102
109
|
"""Set the demand positions on the device without actually hitting move."""
|
|
103
110
|
|
|
104
111
|
@abc.abstractmethod
|
|
@@ -106,8 +113,8 @@ class SafeUndulatorMover(StandardReadable, Movable[T], Generic[T]):
|
|
|
106
113
|
"""Get the timeout for the move based on an estimate of how long it will take."""
|
|
107
114
|
|
|
108
115
|
async def raise_if_cannot_move(self) -> None:
|
|
109
|
-
if await self.
|
|
110
|
-
raise RuntimeError(f"{self.name} is
|
|
116
|
+
if await self.status.get_value() is not EnabledDisabledUpper.ENABLED:
|
|
117
|
+
raise RuntimeError(f"{self.name} is DISABLED and cannot move.")
|
|
111
118
|
if await self.gate.get_value() == UndulatorGateStatus.OPEN:
|
|
112
119
|
raise RuntimeError(f"{self.name} is already in motion.")
|
|
113
120
|
|
|
@@ -158,7 +165,7 @@ class UndulatorGap(SafeUndulatorMover[float]):
|
|
|
158
165
|
self.user_readback = epics_signal_r(float, prefix + "CURRGAPD")
|
|
159
166
|
super().__init__(self.set_move, prefix, name)
|
|
160
167
|
|
|
161
|
-
async def
|
|
168
|
+
async def set_demand_positions(self, value: float) -> None:
|
|
162
169
|
await self.user_setpoint.set(str(value))
|
|
163
170
|
|
|
164
171
|
async def get_timeout(self) -> float:
|
|
@@ -206,49 +213,39 @@ class UndulatorPhaseMotor(StandardReadable):
|
|
|
206
213
|
super().__init__(name=name)
|
|
207
214
|
|
|
208
215
|
|
|
209
|
-
|
|
210
|
-
"""
|
|
211
|
-
A collection of 4 phase Motor to make up the full id phase motion. We are using the diamond pv convention.
|
|
212
|
-
e.g. top_outer == Q1
|
|
213
|
-
top_inner == Q2
|
|
214
|
-
btm_inner == q3
|
|
215
|
-
btm_outer == q4
|
|
216
|
+
Apple2PhaseValType = TypeVar("Apple2PhaseValType", bound=Apple2LockedPhasesVal)
|
|
216
217
|
|
|
217
|
-
|
|
218
|
+
|
|
219
|
+
class UndulatorLockedPhaseAxes(SafeUndulatorMover[Apple2PhaseValType]):
|
|
220
|
+
"""Two phase Motor to make up the locked id phase motion."""
|
|
218
221
|
|
|
219
222
|
def __init__(
|
|
220
223
|
self,
|
|
221
224
|
prefix: str,
|
|
222
225
|
top_outer: str,
|
|
223
|
-
top_inner: str,
|
|
224
226
|
btm_inner: str,
|
|
225
|
-
btm_outer: str,
|
|
226
227
|
name: str = "",
|
|
227
228
|
):
|
|
228
229
|
# Gap demand set point and readback
|
|
229
230
|
with self.add_children_as_readables():
|
|
230
231
|
self.top_outer = UndulatorPhaseMotor(prefix=prefix, infix=top_outer)
|
|
231
|
-
self.top_inner = UndulatorPhaseMotor(prefix=prefix, infix=top_inner)
|
|
232
232
|
self.btm_inner = UndulatorPhaseMotor(prefix=prefix, infix=btm_inner)
|
|
233
|
-
self.btm_outer = UndulatorPhaseMotor(prefix=prefix, infix=btm_outer)
|
|
234
233
|
# Nothing move until this is set to 1 and it will return to 0 when done.
|
|
235
234
|
self.set_move = epics_signal_rw(int, f"{prefix}BL{top_outer}" + "MOVE")
|
|
236
|
-
|
|
235
|
+
self.axes = [self.top_outer, self.btm_inner]
|
|
237
236
|
super().__init__(self.set_move, prefix, name)
|
|
238
237
|
|
|
239
|
-
async def
|
|
238
|
+
async def set_demand_positions(self, value: Apple2PhaseValType) -> None:
|
|
240
239
|
await asyncio.gather(
|
|
241
240
|
self.top_outer.user_setpoint.set(value=value.top_outer),
|
|
242
|
-
self.top_inner.user_setpoint.set(value=value.top_inner),
|
|
243
241
|
self.btm_inner.user_setpoint.set(value=value.btm_inner),
|
|
244
|
-
self.btm_outer.user_setpoint.set(value=value.btm_outer),
|
|
245
242
|
)
|
|
246
243
|
|
|
247
244
|
async def get_timeout(self) -> float:
|
|
248
245
|
"""
|
|
249
246
|
Get all four motor speed, current positions and target positions to calculate required timeout.
|
|
250
247
|
"""
|
|
251
|
-
|
|
248
|
+
|
|
252
249
|
timeouts = await asyncio.gather(
|
|
253
250
|
*[
|
|
254
251
|
estimate_motor_timeout(
|
|
@@ -256,7 +253,7 @@ class UndulatorPhaseAxes(SafeUndulatorMover[Apple2PhasesVal]):
|
|
|
256
253
|
axis.user_readback,
|
|
257
254
|
axis.velocity,
|
|
258
255
|
)
|
|
259
|
-
for axis in axes
|
|
256
|
+
for axis in self.axes
|
|
260
257
|
]
|
|
261
258
|
)
|
|
262
259
|
"""A 2.0 multiplier is required to prevent premature motor timeouts in phase
|
|
@@ -266,6 +263,42 @@ class UndulatorPhaseAxes(SafeUndulatorMover[Apple2PhasesVal]):
|
|
|
266
263
|
return np.max(timeouts) * 2.0
|
|
267
264
|
|
|
268
265
|
|
|
266
|
+
class UndulatorPhaseAxes(UndulatorLockedPhaseAxes[Apple2PhasesVal]):
|
|
267
|
+
"""
|
|
268
|
+
A collection of 4 phase Motor to make up the full id phase motion. We are using the diamond pv convention.
|
|
269
|
+
e.g. top_outer == Q1
|
|
270
|
+
top_inner == Q2
|
|
271
|
+
btm_inner == q3
|
|
272
|
+
btm_outer == q4
|
|
273
|
+
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def __init__(
|
|
277
|
+
self,
|
|
278
|
+
prefix: str,
|
|
279
|
+
top_outer: str,
|
|
280
|
+
top_inner: str,
|
|
281
|
+
btm_inner: str,
|
|
282
|
+
btm_outer: str,
|
|
283
|
+
name: str = "",
|
|
284
|
+
):
|
|
285
|
+
# Gap demand set point and readback
|
|
286
|
+
with self.add_children_as_readables():
|
|
287
|
+
self.top_inner = UndulatorPhaseMotor(prefix=prefix, infix=top_inner)
|
|
288
|
+
self.btm_outer = UndulatorPhaseMotor(prefix=prefix, infix=btm_outer)
|
|
289
|
+
|
|
290
|
+
super().__init__(prefix, top_outer=top_outer, btm_inner=btm_inner, name=name)
|
|
291
|
+
self.axes.extend([self.top_inner, self.btm_outer])
|
|
292
|
+
|
|
293
|
+
async def set_demand_positions(self, value: Apple2PhasesVal) -> None:
|
|
294
|
+
await asyncio.gather(
|
|
295
|
+
self.top_outer.user_setpoint.set(value=value.top_outer),
|
|
296
|
+
self.top_inner.user_setpoint.set(value=value.top_inner),
|
|
297
|
+
self.btm_inner.user_setpoint.set(value=value.btm_inner),
|
|
298
|
+
self.btm_outer.user_setpoint.set(value=value.btm_outer),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
269
302
|
class UndulatorJawPhase(SafeUndulatorMover[float]):
|
|
270
303
|
"""
|
|
271
304
|
A JawPhase movable, this is use for moving the jaw phase which is use to control the
|
|
@@ -287,7 +320,7 @@ class UndulatorJawPhase(SafeUndulatorMover[float]):
|
|
|
287
320
|
|
|
288
321
|
super().__init__(self.set_move, prefix, name)
|
|
289
322
|
|
|
290
|
-
async def
|
|
323
|
+
async def set_demand_positions(self, value: float) -> None:
|
|
291
324
|
await self.jaw_phase.user_setpoint.set(value=str(value))
|
|
292
325
|
|
|
293
326
|
async def get_timeout(self) -> float:
|
|
@@ -301,7 +334,10 @@ class UndulatorJawPhase(SafeUndulatorMover[float]):
|
|
|
301
334
|
)
|
|
302
335
|
|
|
303
336
|
|
|
304
|
-
|
|
337
|
+
PhaseAxesType = TypeVar("PhaseAxesType", bound=UndulatorLockedPhaseAxes)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class Apple2(StandardReadable, Movable[Apple2Val], Generic[PhaseAxesType]):
|
|
305
341
|
"""
|
|
306
342
|
Device representing the combined motor controls for an Apple2 undulator.
|
|
307
343
|
|
|
@@ -313,7 +349,7 @@ class Apple2(StandardReadable, Movable):
|
|
|
313
349
|
The undulator phase axes device, consisting of four phase motors.
|
|
314
350
|
"""
|
|
315
351
|
|
|
316
|
-
def __init__(self, id_gap: UndulatorGap, id_phase:
|
|
352
|
+
def __init__(self, id_gap: UndulatorGap, id_phase: PhaseAxesType, name=""):
|
|
317
353
|
"""
|
|
318
354
|
Parameters
|
|
319
355
|
----------
|
|
@@ -339,12 +375,12 @@ class Apple2(StandardReadable, Movable):
|
|
|
339
375
|
|
|
340
376
|
# Only need to check gap as the phase motors share both fault and gate with gap.
|
|
341
377
|
await self.gap().raise_if_cannot_move()
|
|
378
|
+
|
|
342
379
|
await asyncio.gather(
|
|
343
|
-
self.phase().
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
self.
|
|
347
|
-
self.gap().user_setpoint.set(value=id_motor_values.gap),
|
|
380
|
+
self.phase().set_demand_positions(
|
|
381
|
+
value=id_motor_values.extract_phase_val()
|
|
382
|
+
),
|
|
383
|
+
self.gap().set_demand_positions(value=float(id_motor_values.gap)),
|
|
348
384
|
)
|
|
349
385
|
timeout = np.max(
|
|
350
386
|
await asyncio.gather(self.gap().get_timeout(), self.phase().get_timeout())
|
|
@@ -362,12 +398,12 @@ class Apple2(StandardReadable, Movable):
|
|
|
362
398
|
|
|
363
399
|
|
|
364
400
|
class EnergyMotorConvertor(Protocol):
|
|
365
|
-
def __call__(self, energy: float, pol: Pol) ->
|
|
401
|
+
def __call__(self, energy: float, pol: Pol) -> float:
|
|
366
402
|
"""Protocol to provide energy to motor position conversion"""
|
|
367
403
|
...
|
|
368
404
|
|
|
369
405
|
|
|
370
|
-
Apple2Type = TypeVar("Apple2Type", bound=
|
|
406
|
+
Apple2Type = TypeVar("Apple2Type", bound=Apple2)
|
|
371
407
|
|
|
372
408
|
|
|
373
409
|
class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
|
|
@@ -390,19 +426,18 @@ class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
|
|
|
390
426
|
Soft signal for the polarisation setpoint.
|
|
391
427
|
polarisation : derived_signal_rw
|
|
392
428
|
Hardware-backed signal for polarisation readback and control.
|
|
393
|
-
|
|
394
|
-
Callable that converts energy and polarisation to motor positions.
|
|
429
|
+
gap_energy_to_motor_converter : EnergyMotorConvertor
|
|
430
|
+
Callable that converts energy and polarisation to gap motor positions.
|
|
431
|
+
phase_energy_to_motor_converter : EnergyMotorConvertor
|
|
432
|
+
Callable that converts energy and polarisation to phase motor positions.
|
|
395
433
|
|
|
396
434
|
Abstract Methods
|
|
397
435
|
----------------
|
|
398
|
-
|
|
399
|
-
Abstract method to
|
|
400
|
-
energy_to_motor : EnergyMotorConvertor
|
|
401
|
-
A callable that converts energy and polarisation to motor positions.
|
|
402
|
-
|
|
436
|
+
_get_apple2_value(gap: float, phase: float) -> Apple2Val
|
|
437
|
+
Abstract method to return the Apple2Val used to set the apple2 with.
|
|
403
438
|
Notes
|
|
404
439
|
-----
|
|
405
|
-
- Subclasses must implement `
|
|
440
|
+
- Subclasses must implement `_get_apple2_value` for beamline-specific logic.
|
|
406
441
|
- LH3 polarisation is indistinguishable from LH in hardware; special handling is provided.
|
|
407
442
|
- Supports multiple polarisation modes, including linear horizontal (LH), linear vertical (LV),
|
|
408
443
|
positive circular (PC), negative circular (NC), and linear arbitrary (LA).
|
|
@@ -412,7 +447,9 @@ class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
|
|
|
412
447
|
def __init__(
|
|
413
448
|
self,
|
|
414
449
|
apple2: Apple2Type,
|
|
415
|
-
|
|
450
|
+
gap_energy_motor_converter: EnergyMotorConvertor,
|
|
451
|
+
phase_energy_motor_converter: EnergyMotorConvertor,
|
|
452
|
+
units: str = "eV",
|
|
416
453
|
name: str = "",
|
|
417
454
|
) -> None:
|
|
418
455
|
"""
|
|
@@ -424,19 +461,20 @@ class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
|
|
|
424
461
|
name: str
|
|
425
462
|
Name of the device.
|
|
426
463
|
"""
|
|
427
|
-
self.energy_to_motor = energy_to_motor_converter
|
|
428
464
|
self.apple2 = Reference(apple2)
|
|
465
|
+
self.gap_energy_motor_converter = gap_energy_motor_converter
|
|
466
|
+
self.phase_energy_motor_converter = phase_energy_motor_converter
|
|
429
467
|
|
|
430
468
|
# Store the set energy for readback.
|
|
431
469
|
self._energy, self._energy_set = soft_signal_r_and_setter(
|
|
432
|
-
float, initial_value=None, units=
|
|
470
|
+
float, initial_value=None, units=units
|
|
433
471
|
)
|
|
434
472
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
435
473
|
self.energy = derived_signal_rw(
|
|
436
474
|
raw_to_derived=self._read_energy,
|
|
437
475
|
set_derived=self._set_energy,
|
|
438
476
|
energy=self._energy,
|
|
439
|
-
derived_units=
|
|
477
|
+
derived_units=units,
|
|
440
478
|
)
|
|
441
479
|
|
|
442
480
|
# Store the polarisation for setpoint. And provide readback for LH3.
|
|
@@ -444,30 +482,51 @@ class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
|
|
|
444
482
|
self.polarisation_setpoint, self._polarisation_setpoint_set = (
|
|
445
483
|
soft_signal_r_and_setter(Pol)
|
|
446
484
|
)
|
|
485
|
+
phase = self.apple2().phase()
|
|
486
|
+
# check if undulator phase is unlocked.
|
|
487
|
+
if isinstance(phase, UndulatorPhaseAxes):
|
|
488
|
+
top_inner = phase.top_inner.user_readback
|
|
489
|
+
btm_outer = phase.btm_outer.user_readback
|
|
490
|
+
else:
|
|
491
|
+
# If locked phase axes make the locked phase 0.
|
|
492
|
+
top_inner = btm_outer = soft_signal_rw(float, initial_value=0.0)
|
|
493
|
+
|
|
447
494
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
448
495
|
# Hardware backed read/write for polarisation.
|
|
496
|
+
|
|
449
497
|
self.polarisation = derived_signal_rw(
|
|
450
498
|
raw_to_derived=self._read_pol,
|
|
451
499
|
set_derived=self._set_pol,
|
|
452
500
|
pol=self.polarisation_setpoint,
|
|
453
|
-
top_outer=
|
|
454
|
-
top_inner=
|
|
455
|
-
btm_inner=
|
|
456
|
-
btm_outer=
|
|
501
|
+
top_outer=phase.top_outer.user_readback,
|
|
502
|
+
top_inner=top_inner,
|
|
503
|
+
btm_inner=phase.btm_inner.user_readback,
|
|
504
|
+
btm_outer=btm_outer,
|
|
457
505
|
gap=self.apple2().gap().user_readback,
|
|
458
506
|
)
|
|
459
507
|
super().__init__(name)
|
|
460
508
|
|
|
461
509
|
@abc.abstractmethod
|
|
462
|
-
|
|
510
|
+
def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
|
|
463
511
|
"""
|
|
464
512
|
This method should be implemented by the beamline specific ID class as the
|
|
465
513
|
motor positions will be different for each beamline depending on the
|
|
466
|
-
undulator design
|
|
514
|
+
undulator design.
|
|
467
515
|
"""
|
|
468
516
|
|
|
517
|
+
async def _set_motors_from_energy_and_polarisation(
|
|
518
|
+
self, energy: float, pol: Pol
|
|
519
|
+
) -> None:
|
|
520
|
+
"""Set the undulator motors for a given energy and polarisation."""
|
|
521
|
+
gap = self.gap_energy_motor_converter(energy=energy, pol=pol)
|
|
522
|
+
phase = self.phase_energy_motor_converter(energy=energy, pol=pol)
|
|
523
|
+
apple2_val = self._get_apple2_value(gap, phase, pol)
|
|
524
|
+
LOGGER.info(f"Setting polarisation to {pol}, with values: {apple2_val}")
|
|
525
|
+
await self.apple2().set(id_motor_values=apple2_val)
|
|
526
|
+
|
|
469
527
|
async def _set_energy(self, energy: float) -> None:
|
|
470
|
-
await self.
|
|
528
|
+
pol = await self._check_and_get_pol_setpoint()
|
|
529
|
+
await self._set_motors_from_energy_and_polarisation(energy, pol)
|
|
471
530
|
self._energy_set(energy)
|
|
472
531
|
|
|
473
532
|
def _read_energy(self, energy: float) -> float:
|
|
@@ -479,7 +538,6 @@ class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
|
|
|
479
538
|
Check the polarisation setpoint and if it is NONE try to read it from
|
|
480
539
|
hardware.
|
|
481
540
|
"""
|
|
482
|
-
|
|
483
541
|
pol = await self.polarisation_setpoint.get_value()
|
|
484
542
|
|
|
485
543
|
if pol == Pol.NONE:
|
|
@@ -501,7 +559,7 @@ class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
|
|
|
501
559
|
) -> None:
|
|
502
560
|
# This changes the pol setpoint and then changes polarisation via set energy.
|
|
503
561
|
self._polarisation_setpoint_set(value)
|
|
504
|
-
await self.energy.set(await self.energy.get_value())
|
|
562
|
+
await self.energy.set(await self.energy.get_value(), timeout=MAXIMUM_MOVE_TIME)
|
|
505
563
|
|
|
506
564
|
def _read_pol(
|
|
507
565
|
self,
|
|
@@ -672,16 +730,11 @@ class InsertionDeviceEnergy(InsertionDeviceEnergyBase):
|
|
|
672
730
|
self.energy = Reference(id_controller.energy)
|
|
673
731
|
super().__init__(name=name)
|
|
674
732
|
|
|
675
|
-
self.add_readables(
|
|
676
|
-
[
|
|
677
|
-
self.energy(),
|
|
678
|
-
],
|
|
679
|
-
StandardReadableFormat.HINTED_SIGNAL,
|
|
680
|
-
)
|
|
733
|
+
self.add_readables([self.energy()], StandardReadableFormat.HINTED_SIGNAL)
|
|
681
734
|
|
|
682
735
|
@AsyncStatus.wrap
|
|
683
736
|
async def set(self, energy: float) -> None:
|
|
684
|
-
await self.energy().set(energy)
|
|
737
|
+
await self.energy().set(energy, timeout=MAXIMUM_MOVE_TIME)
|
|
685
738
|
|
|
686
739
|
|
|
687
740
|
class InsertionDevicePolarisation(StandardReadable, Locatable[Pol]):
|
|
@@ -696,7 +749,7 @@ class InsertionDevicePolarisation(StandardReadable, Locatable[Pol]):
|
|
|
696
749
|
|
|
697
750
|
@AsyncStatus.wrap
|
|
698
751
|
async def set(self, pol: Pol) -> None:
|
|
699
|
-
await self.polarisation().set(pol)
|
|
752
|
+
await self.polarisation().set(pol, timeout=MAXIMUM_MOVE_TIME)
|
|
700
753
|
|
|
701
754
|
async def locate(self) -> Location[Pol]:
|
|
702
755
|
"""Return the current polarisation"""
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from daq_config_server.client import ConfigServer
|
|
4
|
+
|
|
5
|
+
from dodal.devices.insertion_device.apple2_undulator import Pol
|
|
6
|
+
from dodal.devices.insertion_device.lookup_table_models import (
|
|
7
|
+
LookupTable,
|
|
8
|
+
LookupTableColumnConfig,
|
|
9
|
+
convert_csv_to_lookup,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EnergyMotorLookup:
|
|
14
|
+
"""
|
|
15
|
+
Handles a lookup table for Apple2 ID, converting energy/polarisation to a motor
|
|
16
|
+
position.
|
|
17
|
+
|
|
18
|
+
After update_lookup_table() has populated the lookup table, `find_value_in_lookup_table()`
|
|
19
|
+
can be used to compute gap / phase for a requested energy / polarisation pair.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, lut: LookupTable | None = None):
|
|
23
|
+
if lut is None:
|
|
24
|
+
lut = LookupTable()
|
|
25
|
+
self.lut = lut
|
|
26
|
+
|
|
27
|
+
def update_lookup_table(self) -> None:
|
|
28
|
+
"""Do nothing by default. Sub classes may override this method to provide logic
|
|
29
|
+
on what updating lookup table does."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def find_value_in_lookup_table(self, energy: float, pol: Pol) -> float:
|
|
33
|
+
"""
|
|
34
|
+
Convert energy and polarisation to a value from the lookup table.
|
|
35
|
+
|
|
36
|
+
Parameters:
|
|
37
|
+
-----------
|
|
38
|
+
energy : float
|
|
39
|
+
Desired energy.
|
|
40
|
+
pol : Pol
|
|
41
|
+
Polarisation mode.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
----------
|
|
45
|
+
float
|
|
46
|
+
gap / phase motor position from the lookup table.
|
|
47
|
+
"""
|
|
48
|
+
# if lut is empty, force an update to pull updated lut incase subclasses have
|
|
49
|
+
# implemented it.
|
|
50
|
+
if not self.lut.root:
|
|
51
|
+
self.update_lookup_table()
|
|
52
|
+
poly = self.lut.get_poly(energy=energy, pol=pol)
|
|
53
|
+
return poly(energy)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ConfigServerEnergyMotorLookup(EnergyMotorLookup):
|
|
57
|
+
"""Fetches and parses lookup table (csv) from a config server, supports dynamic
|
|
58
|
+
updates, and validates input."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
config_client: ConfigServer,
|
|
63
|
+
lut_config: LookupTableColumnConfig,
|
|
64
|
+
path: Path,
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Parameters:
|
|
68
|
+
-----------
|
|
69
|
+
config_client:
|
|
70
|
+
The config server client to fetch the look up table data.
|
|
71
|
+
lut_config:
|
|
72
|
+
Configuration that defines how to process file contents into a LookupTable
|
|
73
|
+
path:
|
|
74
|
+
File path to the lookup table.
|
|
75
|
+
"""
|
|
76
|
+
self.path = path
|
|
77
|
+
self.config_client = config_client
|
|
78
|
+
self.lut_config = lut_config
|
|
79
|
+
super().__init__()
|
|
80
|
+
|
|
81
|
+
def read_lut(self) -> LookupTable:
|
|
82
|
+
file_contents = self.config_client.get_file_contents(
|
|
83
|
+
self.path, reset_cached_result=True
|
|
84
|
+
)
|
|
85
|
+
return convert_csv_to_lookup(file_contents, lut_config=self.lut_config)
|
|
86
|
+
|
|
87
|
+
def update_lookup_table(self) -> None:
|
|
88
|
+
self.lut = self.read_lut()
|