dls-dodal 1.32.0__py3-none-any.whl → 1.34.1__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.32.0.dist-info → dls_dodal-1.34.1.dist-info}/METADATA +3 -3
- {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/RECORD +53 -43
- {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/WHEEL +1 -1
- dodal/__init__.py +8 -0
- dodal/_version.py +2 -2
- dodal/beamline_specific_utils/i03.py +6 -2
- dodal/beamlines/__init__.py +2 -3
- dodal/beamlines/b01_1.py +77 -0
- dodal/beamlines/i03.py +41 -9
- dodal/beamlines/i04.py +26 -4
- dodal/beamlines/i10.py +257 -0
- dodal/beamlines/i22.py +1 -2
- dodal/beamlines/i24.py +7 -7
- dodal/beamlines/p38.py +1 -2
- dodal/common/signal_utils.py +53 -0
- dodal/common/types.py +2 -7
- dodal/devices/aperturescatterguard.py +12 -15
- dodal/devices/apple2_undulator.py +602 -0
- dodal/devices/areadetector/plugins/CAM.py +31 -0
- dodal/devices/areadetector/plugins/MJPG.py +51 -106
- dodal/devices/backlight.py +7 -6
- dodal/devices/diamond_filter.py +47 -0
- dodal/devices/eiger.py +6 -2
- dodal/devices/eiger_odin.py +48 -39
- dodal/devices/focusing_mirror.py +14 -8
- dodal/devices/i10/i10_apple2.py +398 -0
- dodal/devices/i10/i10_setting_data.py +7 -0
- dodal/devices/i22/dcm.py +7 -8
- dodal/devices/i24/dual_backlight.py +5 -5
- dodal/devices/oav/oav_calculations.py +22 -0
- dodal/devices/oav/oav_detector.py +118 -97
- dodal/devices/oav/oav_parameters.py +50 -104
- dodal/devices/oav/oav_to_redis_forwarder.py +75 -34
- dodal/devices/oav/{grid_overlay.py → snapshots/grid_overlay.py} +0 -43
- dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +64 -0
- dodal/devices/oav/snapshots/snapshot_with_grid.py +57 -0
- dodal/devices/oav/utils.py +26 -25
- dodal/devices/pgm.py +41 -0
- dodal/devices/qbpm.py +18 -0
- dodal/devices/robot.py +2 -2
- dodal/devices/smargon.py +2 -2
- dodal/devices/tetramm.py +2 -2
- dodal/devices/undulator.py +2 -1
- dodal/devices/util/adjuster_plans.py +1 -1
- dodal/devices/util/lookup_tables.py +4 -5
- dodal/devices/zebra.py +5 -2
- dodal/devices/zocalo/zocalo_results.py +13 -10
- dodal/plans/data_session_metadata.py +2 -2
- dodal/plans/motor_util_plans.py +11 -9
- dodal/utils.py +7 -0
- dodal/beamlines/i04_1.py +0 -140
- dodal/devices/oav/oav_errors.py +0 -35
- {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/LICENSE +0 -0
- {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import asyncio
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from bluesky.protocols import Movable
|
|
9
|
+
from ophyd_async.core import (
|
|
10
|
+
AsyncStatus,
|
|
11
|
+
ConfigSignal,
|
|
12
|
+
HintedSignal,
|
|
13
|
+
StandardReadable,
|
|
14
|
+
soft_signal_r_and_setter,
|
|
15
|
+
wait_for_value,
|
|
16
|
+
)
|
|
17
|
+
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_w
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, RootModel
|
|
19
|
+
|
|
20
|
+
from dodal.log import LOGGER
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UndulatorGateStatus(str, Enum):
|
|
24
|
+
open = "Open"
|
|
25
|
+
close = "Closed"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Apple2PhasesVal:
|
|
30
|
+
top_outer: str
|
|
31
|
+
top_inner: str
|
|
32
|
+
btm_inner: str
|
|
33
|
+
btm_outer: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Apple2Val:
|
|
38
|
+
gap: str
|
|
39
|
+
top_outer: str
|
|
40
|
+
top_inner: str
|
|
41
|
+
btm_inner: str
|
|
42
|
+
btm_outer: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class EnergyMinMax(BaseModel):
|
|
46
|
+
Minimum: float
|
|
47
|
+
Maximum: float
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class EnergyCoverageEntry(BaseModel):
|
|
51
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
52
|
+
Low: float
|
|
53
|
+
High: float
|
|
54
|
+
Poly: np.poly1d
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class EnergyCoverage(RootModel):
|
|
58
|
+
root: dict[str, EnergyCoverageEntry]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LookupTableEntries(BaseModel):
|
|
62
|
+
Energies: EnergyCoverage
|
|
63
|
+
Limit: EnergyMinMax
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Lookuptable(RootModel):
|
|
67
|
+
"""
|
|
68
|
+
BaseModel class for the lookup table.
|
|
69
|
+
Apple2 lookup table should be in this format.
|
|
70
|
+
|
|
71
|
+
{mode: {'Energies': {Any: {'Low': float,
|
|
72
|
+
'High': float,
|
|
73
|
+
'Poly':np.poly1d
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
'Limit': {'Minimum': float,
|
|
77
|
+
'Maximum': float
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
root: dict[str, LookupTableEntries]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
87
|
+
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
88
|
+
MAXIMUM_GAP_MOTOR_POSITION = 100
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class UndulatorGap(StandardReadable, Movable):
|
|
92
|
+
"""A device with a collection of epics signals to set Apple 2 undulator gap motion.
|
|
93
|
+
Only PV used by beamline are added the full list is here:
|
|
94
|
+
/dls_sw/work/R3.14.12.7/support/insertionDevice/db/IDGapVelocityControl.template
|
|
95
|
+
/dls_sw/work/R3.14.12.7/support/insertionDevice/db/IDPhaseSoftMotor.template
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, prefix: str, name: str = ""):
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
prefix : str
|
|
104
|
+
Beamline specific part of the PV
|
|
105
|
+
name : str
|
|
106
|
+
Name of the Id device
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# Gap demand set point and readback
|
|
111
|
+
self.user_setpoint = epics_signal_rw(
|
|
112
|
+
str, prefix + "GAPSET.B", prefix + "BLGSET"
|
|
113
|
+
)
|
|
114
|
+
# Nothing move until this is set to 1 and it will return to 0 when done
|
|
115
|
+
self.set_move = epics_signal_rw(int, prefix + "BLGSETP")
|
|
116
|
+
# Gate keeper open when move is requested, closed when move is completed
|
|
117
|
+
self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
|
|
118
|
+
# These are gap velocity limit.
|
|
119
|
+
self.max_velocity = epics_signal_r(float, prefix + "BLGSETVEL.HOPR")
|
|
120
|
+
self.min_velocity = epics_signal_r(float, prefix + "BLGSETVEL.LOPR")
|
|
121
|
+
# These are gap limit.
|
|
122
|
+
self.high_limit_travel = epics_signal_r(float, prefix + "BLGAPMTR.HLM")
|
|
123
|
+
self.low_limit_travel = epics_signal_r(float, prefix + "BLGAPMTR.LLM")
|
|
124
|
+
split_pv = prefix.split("-")
|
|
125
|
+
self.fault = epics_signal_r(
|
|
126
|
+
float,
|
|
127
|
+
f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT",
|
|
128
|
+
)
|
|
129
|
+
# This is calculated acceleration from speed
|
|
130
|
+
self.acceleration_time = epics_signal_r(float, prefix + "IDGSETACC")
|
|
131
|
+
with self.add_children_as_readables(ConfigSignal):
|
|
132
|
+
# Unit
|
|
133
|
+
self.motor_egu = epics_signal_r(str, prefix + "BLGAPMTR.EGU")
|
|
134
|
+
# Gap velocity
|
|
135
|
+
self.velocity = epics_signal_rw(float, prefix + "BLGSETVEL")
|
|
136
|
+
with self.add_children_as_readables(HintedSignal):
|
|
137
|
+
# Gap readback value
|
|
138
|
+
self.user_readback = epics_signal_r(float, prefix + "CURRGAPD")
|
|
139
|
+
super().__init__(name)
|
|
140
|
+
|
|
141
|
+
@AsyncStatus.wrap
|
|
142
|
+
async def set(self, value) -> None:
|
|
143
|
+
LOGGER.info(f"Setting {self.name} to {value}")
|
|
144
|
+
await self.check_id_status()
|
|
145
|
+
await self.user_setpoint.set(value=str(value))
|
|
146
|
+
timeout = await self._cal_timeout()
|
|
147
|
+
LOGGER.info(f"Moving {self.name} to {value} with timeout = {timeout}")
|
|
148
|
+
await self.set_move.set(value=1, timeout=timeout)
|
|
149
|
+
await wait_for_value(self.gate, UndulatorGateStatus.close, timeout=timeout)
|
|
150
|
+
|
|
151
|
+
async def _cal_timeout(self) -> float:
|
|
152
|
+
vel = await self.velocity.get_value()
|
|
153
|
+
cur_pos = await self.user_readback.get_value()
|
|
154
|
+
target_pos = float(await self.user_setpoint.get_value())
|
|
155
|
+
return abs((target_pos - cur_pos) * 2.0 / vel) + 1
|
|
156
|
+
|
|
157
|
+
async def check_id_status(self) -> None:
|
|
158
|
+
if await self.fault.get_value() != 0:
|
|
159
|
+
raise RuntimeError(f"{self.name} is in fault state")
|
|
160
|
+
if await self.gate.get_value() == UndulatorGateStatus.open:
|
|
161
|
+
raise RuntimeError(f"{self.name} is already in motion.")
|
|
162
|
+
|
|
163
|
+
async def get_timeout(self) -> float:
|
|
164
|
+
return await self._cal_timeout()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class UndulatorPhaseMotor(StandardReadable):
|
|
168
|
+
"""A collection of epics signals for ID phase motion.
|
|
169
|
+
Only PV used by beamline are added the full list is here:
|
|
170
|
+
/dls_sw/work/R3.14.12.7/support/insertionDevice/db/IDPhaseSoftMotor.template
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def __init__(self, prefix: str, infix: str, name: str = ""):
|
|
174
|
+
"""
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
|
|
178
|
+
prefix : str
|
|
179
|
+
The setting prefix PV.
|
|
180
|
+
infix: str
|
|
181
|
+
Collection of pv that are different between beamlines
|
|
182
|
+
name : str
|
|
183
|
+
Name of the Id phase device
|
|
184
|
+
"""
|
|
185
|
+
fullPV = f"{prefix}BL{infix}"
|
|
186
|
+
self.user_setpoint = epics_signal_w(str, fullPV + "SET")
|
|
187
|
+
self.user_setpoint_demand_readback = epics_signal_r(float, fullPV + "DMD")
|
|
188
|
+
|
|
189
|
+
fullPV = fullPV + "MTR"
|
|
190
|
+
with self.add_children_as_readables(HintedSignal):
|
|
191
|
+
self.user_setpoint_readback = epics_signal_r(float, fullPV + ".RBV")
|
|
192
|
+
|
|
193
|
+
with self.add_children_as_readables(ConfigSignal):
|
|
194
|
+
self.motor_egu = epics_signal_r(str, fullPV + ".EGU")
|
|
195
|
+
self.velocity = epics_signal_rw(float, fullPV + ".VELO")
|
|
196
|
+
|
|
197
|
+
self.max_velocity = epics_signal_r(float, fullPV + ".VMAX")
|
|
198
|
+
self.acceleration_time = epics_signal_rw(float, fullPV + ".ACCL")
|
|
199
|
+
self.precision = epics_signal_r(int, fullPV + ".PREC")
|
|
200
|
+
self.deadband = epics_signal_r(float, fullPV + ".RDBD")
|
|
201
|
+
self.motor_done_move = epics_signal_r(int, fullPV + ".DMOV")
|
|
202
|
+
self.low_limit_travel = epics_signal_rw(float, fullPV + ".LLM")
|
|
203
|
+
self.high_limit_travel = epics_signal_rw(float, fullPV + ".HLM")
|
|
204
|
+
super().__init__(name=name)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class UndulatorPhaseAxes(StandardReadable, Movable):
|
|
208
|
+
"""
|
|
209
|
+
A collection of 4 phase Motor to make up the full id phase motion. We are using the diamond pv convention.
|
|
210
|
+
e.g. top_outer == Q1
|
|
211
|
+
top_inner == Q2
|
|
212
|
+
btm_inner == q3
|
|
213
|
+
btm_outer == q4
|
|
214
|
+
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def __init__(
|
|
218
|
+
self,
|
|
219
|
+
prefix: str,
|
|
220
|
+
top_outer: str,
|
|
221
|
+
top_inner: str,
|
|
222
|
+
btm_inner: str,
|
|
223
|
+
btm_outer: str,
|
|
224
|
+
name: str = "",
|
|
225
|
+
):
|
|
226
|
+
# Gap demand set point and readback
|
|
227
|
+
with self.add_children_as_readables():
|
|
228
|
+
self.top_outer = UndulatorPhaseMotor(prefix=prefix, infix=top_outer)
|
|
229
|
+
self.top_inner = UndulatorPhaseMotor(prefix=prefix, infix=top_inner)
|
|
230
|
+
self.btm_inner = UndulatorPhaseMotor(prefix=prefix, infix=btm_inner)
|
|
231
|
+
self.btm_outer = UndulatorPhaseMotor(prefix=prefix, infix=btm_outer)
|
|
232
|
+
# Nothing move until this is set to 1 and it will return to 0 when done.
|
|
233
|
+
self.set_move = epics_signal_rw(int, f"{prefix}BL{top_outer}" + "MOVE")
|
|
234
|
+
self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
|
|
235
|
+
split_pv = prefix.split("-")
|
|
236
|
+
temp_pv = f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT"
|
|
237
|
+
self.fault = epics_signal_r(float, temp_pv)
|
|
238
|
+
super().__init__(name=name)
|
|
239
|
+
|
|
240
|
+
@AsyncStatus.wrap
|
|
241
|
+
async def set(self, value: Apple2PhasesVal) -> None:
|
|
242
|
+
LOGGER.info(f"Setting {self.name} to {value}")
|
|
243
|
+
|
|
244
|
+
await self.check_id_status()
|
|
245
|
+
|
|
246
|
+
await asyncio.gather(
|
|
247
|
+
self.top_outer.user_setpoint.set(value=value.top_outer),
|
|
248
|
+
self.top_inner.user_setpoint.set(value=value.top_inner),
|
|
249
|
+
self.btm_inner.user_setpoint.set(value=value.btm_inner),
|
|
250
|
+
self.btm_outer.user_setpoint.set(value=value.btm_outer),
|
|
251
|
+
)
|
|
252
|
+
timeout = await self._cal_timeout()
|
|
253
|
+
await self.set_move.set(value=1, timeout=timeout)
|
|
254
|
+
await wait_for_value(self.gate, UndulatorGateStatus.close, timeout=timeout)
|
|
255
|
+
|
|
256
|
+
async def _cal_timeout(self) -> float:
|
|
257
|
+
"""
|
|
258
|
+
Get all four motor speed, current positions and target positions to calculate required timeout.
|
|
259
|
+
"""
|
|
260
|
+
velos = await asyncio.gather(
|
|
261
|
+
self.top_outer.velocity.get_value(),
|
|
262
|
+
self.top_inner.velocity.get_value(),
|
|
263
|
+
self.btm_inner.velocity.get_value(),
|
|
264
|
+
self.btm_outer.velocity.get_value(),
|
|
265
|
+
)
|
|
266
|
+
target_pos = await asyncio.gather(
|
|
267
|
+
self.top_outer.user_setpoint_demand_readback.get_value(),
|
|
268
|
+
self.top_inner.user_setpoint_demand_readback.get_value(),
|
|
269
|
+
self.btm_inner.user_setpoint_demand_readback.get_value(),
|
|
270
|
+
self.btm_outer.user_setpoint_demand_readback.get_value(),
|
|
271
|
+
)
|
|
272
|
+
cur_pos = await asyncio.gather(
|
|
273
|
+
self.top_outer.user_setpoint_readback.get_value(),
|
|
274
|
+
self.top_inner.user_setpoint_readback.get_value(),
|
|
275
|
+
self.btm_inner.user_setpoint_readback.get_value(),
|
|
276
|
+
self.btm_outer.user_setpoint_readback.get_value(),
|
|
277
|
+
)
|
|
278
|
+
move_distances = tuple(np.subtract(target_pos, cur_pos))
|
|
279
|
+
move_times = np.abs(np.divide(move_distances, velos))
|
|
280
|
+
longest_move_time = np.max(move_times)
|
|
281
|
+
return longest_move_time * 2 + 1
|
|
282
|
+
|
|
283
|
+
async def check_id_status(self) -> None:
|
|
284
|
+
if await self.fault.get_value() != 0:
|
|
285
|
+
raise RuntimeError(f"{self.name} is in fault state")
|
|
286
|
+
if await self.gate.get_value() == UndulatorGateStatus.open:
|
|
287
|
+
raise RuntimeError(f"{self.name} is already in motion.")
|
|
288
|
+
|
|
289
|
+
async def get_timeout(self) -> float:
|
|
290
|
+
return await self._cal_timeout()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class UndulatorJawPhase(StandardReadable, Movable):
|
|
294
|
+
"""
|
|
295
|
+
A JawPhase movable, this is use for moving the jaw phase which is use to control the
|
|
296
|
+
linear arbitrary polarisation but only one some of the beamline.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
def __init__(
|
|
300
|
+
self,
|
|
301
|
+
prefix: str,
|
|
302
|
+
move_pv: str,
|
|
303
|
+
jaw_phase: str = "JAW",
|
|
304
|
+
name: str = "",
|
|
305
|
+
):
|
|
306
|
+
# Gap demand set point and readback
|
|
307
|
+
with self.add_children_as_readables():
|
|
308
|
+
self.jaw_phase = UndulatorPhaseMotor(prefix=prefix, infix=jaw_phase)
|
|
309
|
+
# Nothing move until this is set to 1 and it will return to 0 when done
|
|
310
|
+
self.set_move = epics_signal_rw(int, f"{prefix}BL{move_pv}" + "MOVE")
|
|
311
|
+
self.gate = epics_signal_r(UndulatorGateStatus, prefix + "BLGATE")
|
|
312
|
+
split_pv = prefix.split("-")
|
|
313
|
+
temp_pv = f"{split_pv[0]}-{split_pv[1]}-STAT-{split_pv[3]}ANYFAULT"
|
|
314
|
+
self.fault = epics_signal_r(float, temp_pv)
|
|
315
|
+
super().__init__(name=name)
|
|
316
|
+
|
|
317
|
+
@AsyncStatus.wrap
|
|
318
|
+
async def set(self, value: float) -> None:
|
|
319
|
+
LOGGER.info(f"Setting {self.name} to {value}")
|
|
320
|
+
|
|
321
|
+
await self.check_id_status()
|
|
322
|
+
|
|
323
|
+
await asyncio.gather(
|
|
324
|
+
self.jaw_phase.user_setpoint.set(value=str(value)),
|
|
325
|
+
)
|
|
326
|
+
timeout = await self._cal_timeout()
|
|
327
|
+
await self.set_move.set(value=1, timeout=timeout)
|
|
328
|
+
await wait_for_value(self.gate, UndulatorGateStatus.close, timeout=timeout)
|
|
329
|
+
|
|
330
|
+
async def _cal_timeout(self) -> float:
|
|
331
|
+
"""
|
|
332
|
+
Get motor speed, current position and target position to calculate required timeout.
|
|
333
|
+
"""
|
|
334
|
+
velo, target_pos, cur_pos = await asyncio.gather(
|
|
335
|
+
self.jaw_phase.velocity.get_value(),
|
|
336
|
+
self.jaw_phase.user_setpoint_demand_readback.get_value(),
|
|
337
|
+
self.jaw_phase.user_setpoint_readback.get_value(),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
move_distances = target_pos - cur_pos
|
|
341
|
+
move_times = np.abs(move_distances / velo)
|
|
342
|
+
|
|
343
|
+
return move_times * 2 + 1
|
|
344
|
+
|
|
345
|
+
async def check_id_status(self) -> None:
|
|
346
|
+
if await self.fault.get_value() != 0:
|
|
347
|
+
raise RuntimeError(f"{self.name} is in fault state")
|
|
348
|
+
if await self.gate.get_value() == UndulatorGateStatus.open:
|
|
349
|
+
raise RuntimeError(f"{self.name} is already in motion.")
|
|
350
|
+
|
|
351
|
+
async def get_timeout(self) -> float:
|
|
352
|
+
return await self._cal_timeout()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class Apple2(StandardReadable, Movable):
|
|
356
|
+
"""
|
|
357
|
+
Apple 2 ID/undulator has 4 extra degrees of freedom compare to the standard Undulator,
|
|
358
|
+
each bank of magnet can move independently to each other,
|
|
359
|
+
which allow the production of different x-ray polarisation as well as energy.
|
|
360
|
+
This type of ID is use on I10, I21, I09, I17 and I06 for soft x-ray.
|
|
361
|
+
|
|
362
|
+
A pair of look up tables are needed to provide the conversion between motor position
|
|
363
|
+
and energy.
|
|
364
|
+
This conversion (update_lookuptable) and the way the id move (set) are two abstract
|
|
365
|
+
methods that are beamline specific and need to be implemented.
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
def __init__(
|
|
369
|
+
self,
|
|
370
|
+
id_gap: UndulatorGap,
|
|
371
|
+
id_phase: UndulatorPhaseAxes,
|
|
372
|
+
prefix: str = "",
|
|
373
|
+
name: str = "",
|
|
374
|
+
) -> None:
|
|
375
|
+
"""
|
|
376
|
+
Parameters
|
|
377
|
+
----------
|
|
378
|
+
id_gap:
|
|
379
|
+
An UndulatorGap device.
|
|
380
|
+
id_phase:
|
|
381
|
+
An UndulatorPhaseAxes device.
|
|
382
|
+
prefix:
|
|
383
|
+
Not in use but needed for device_instantiation.
|
|
384
|
+
name:
|
|
385
|
+
Name of the device.
|
|
386
|
+
"""
|
|
387
|
+
super().__init__(name)
|
|
388
|
+
|
|
389
|
+
# Attributes are set after super call so they are not renamed to
|
|
390
|
+
# <name>-undulator, etc.
|
|
391
|
+
with self.add_children_as_readables():
|
|
392
|
+
self.gap = id_gap
|
|
393
|
+
self.phase = id_phase
|
|
394
|
+
with self.add_children_as_readables(HintedSignal):
|
|
395
|
+
# Store the polarisation for readback.
|
|
396
|
+
self.polarisation, self._polarisation_set = soft_signal_r_and_setter(
|
|
397
|
+
str, initial_value=None
|
|
398
|
+
)
|
|
399
|
+
# Store the set energy for readback.
|
|
400
|
+
self.energy, self._energy_set = soft_signal_r_and_setter(
|
|
401
|
+
float, initial_value=None
|
|
402
|
+
)
|
|
403
|
+
# This store two lookup tables, Gap and Phase in the Lookuptable format
|
|
404
|
+
self.lookup_tables: dict[str, dict[str | None, dict[str, dict[str, Any]]]] = {
|
|
405
|
+
"Gap": {},
|
|
406
|
+
"Phase": {},
|
|
407
|
+
}
|
|
408
|
+
# List of available polarisation according to the lookup table.
|
|
409
|
+
self._available_pol = []
|
|
410
|
+
# The polarisation state of the id that are use for internal checking before setting.
|
|
411
|
+
self._pol = None
|
|
412
|
+
"""
|
|
413
|
+
Abstract method that run at start up to load lookup tables into self.lookup_tables
|
|
414
|
+
and set available_pol.
|
|
415
|
+
"""
|
|
416
|
+
self.update_lookuptable()
|
|
417
|
+
|
|
418
|
+
@property
|
|
419
|
+
def pol(self):
|
|
420
|
+
return self._pol
|
|
421
|
+
|
|
422
|
+
@pol.setter
|
|
423
|
+
def pol(self, pol: str):
|
|
424
|
+
# This set the polarisation but does not actually move hardware.
|
|
425
|
+
if pol in self._available_pol:
|
|
426
|
+
self._pol = pol
|
|
427
|
+
else:
|
|
428
|
+
raise ValueError(
|
|
429
|
+
f"Polarisation {pol} is not available:"
|
|
430
|
+
+ f"/n Polarisations available: {self._available_pol}"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
async def _set(self, value: Apple2Val, energy: float) -> None:
|
|
434
|
+
"""
|
|
435
|
+
Check ID is in a movable state and set all the demand value before moving.
|
|
436
|
+
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
# Only need to check gap as the phase motors share both fault and gate with gap.
|
|
440
|
+
await self.gap.check_id_status()
|
|
441
|
+
await asyncio.gather(
|
|
442
|
+
self.phase.top_outer.user_setpoint.set(value=value.top_outer),
|
|
443
|
+
self.phase.top_inner.user_setpoint.set(value=value.top_inner),
|
|
444
|
+
self.phase.btm_inner.user_setpoint.set(value=value.btm_inner),
|
|
445
|
+
self.phase.btm_outer.user_setpoint.set(value=value.btm_outer),
|
|
446
|
+
self.gap.user_setpoint.set(value=value.gap),
|
|
447
|
+
)
|
|
448
|
+
timeout = np.max(
|
|
449
|
+
await asyncio.gather(self.gap.get_timeout(), self.phase.get_timeout())
|
|
450
|
+
)
|
|
451
|
+
LOGGER.info(
|
|
452
|
+
f"Moving f{self.name} energy and polorisation to {energy}, {self.pol}"
|
|
453
|
+
+ f"with motor position {value}, timeout = {timeout}"
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
await asyncio.gather(
|
|
457
|
+
self.gap.set_move.set(value=1, timeout=timeout),
|
|
458
|
+
self.phase.set_move.set(value=1, timeout=timeout),
|
|
459
|
+
)
|
|
460
|
+
await wait_for_value(self.gap.gate, UndulatorGateStatus.close, timeout=timeout)
|
|
461
|
+
self._energy_set(energy) # Update energy for after move for readback.
|
|
462
|
+
|
|
463
|
+
def _get_id_gap_phase(self, energy: float) -> tuple[float, float]:
|
|
464
|
+
"""
|
|
465
|
+
Converts energy and polarisation to gap and phase.
|
|
466
|
+
"""
|
|
467
|
+
gap_poly = self._get_poly(
|
|
468
|
+
lookup_table=self.lookup_tables["Gap"], new_energy=energy
|
|
469
|
+
)
|
|
470
|
+
phase_poly = self._get_poly(
|
|
471
|
+
lookup_table=self.lookup_tables["Phase"], new_energy=energy
|
|
472
|
+
)
|
|
473
|
+
return gap_poly(energy), phase_poly(energy)
|
|
474
|
+
|
|
475
|
+
def _get_poly(
|
|
476
|
+
self,
|
|
477
|
+
new_energy: float,
|
|
478
|
+
lookup_table: dict[str | None, dict[str, dict[str, Any]]],
|
|
479
|
+
) -> np.poly1d:
|
|
480
|
+
"""
|
|
481
|
+
Get the correct polynomial for a given energy form lookuptable
|
|
482
|
+
for any given polarisation.
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
if (
|
|
486
|
+
new_energy < lookup_table[self.pol]["Limit"]["Minimum"]
|
|
487
|
+
or new_energy > lookup_table[self.pol]["Limit"]["Maximum"]
|
|
488
|
+
):
|
|
489
|
+
raise ValueError(
|
|
490
|
+
"Demanding energy must lie between {} and {} eV!".format(
|
|
491
|
+
lookup_table[self.pol]["Limit"]["Minimum"],
|
|
492
|
+
lookup_table[self.pol]["Limit"]["Maximum"],
|
|
493
|
+
)
|
|
494
|
+
)
|
|
495
|
+
else:
|
|
496
|
+
for energy_range in lookup_table[self.pol]["Energies"].values():
|
|
497
|
+
if (
|
|
498
|
+
new_energy >= energy_range["Low"]
|
|
499
|
+
and new_energy < energy_range["High"]
|
|
500
|
+
):
|
|
501
|
+
return energy_range["Poly"]
|
|
502
|
+
|
|
503
|
+
raise ValueError(
|
|
504
|
+
"""Cannot find polynomial coefficients for your requested energy.
|
|
505
|
+
There might be gap in the calibration lookup table."""
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
@abc.abstractmethod
|
|
509
|
+
def update_lookuptable(self) -> None:
|
|
510
|
+
"""
|
|
511
|
+
Abstract method to update the stored lookup tabled from file.
|
|
512
|
+
This function should include check to ensure the lookuptable is in the correct format:
|
|
513
|
+
# ensure the importing lookup table is the correct format
|
|
514
|
+
Lookuptable.model_validate(<loockuptable>)
|
|
515
|
+
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
async def determinePhaseFromHardware(self) -> tuple[str | None, float]:
|
|
519
|
+
"""
|
|
520
|
+
Try to determine polarisation and phase value using row phase motor position pattern.
|
|
521
|
+
However there is no way to return lh3 polarisation or higher harmonic setting.
|
|
522
|
+
(May be for future one can use the inverse poly to work out the energy and try to match it with the current energy
|
|
523
|
+
to workout the polarisation but during my test the inverse poly is too unstable for general use.)
|
|
524
|
+
"""
|
|
525
|
+
cur_loc = await self.read()
|
|
526
|
+
top_outer = cur_loc[self.phase.top_outer.user_setpoint_readback.name]["value"]
|
|
527
|
+
top_inner = cur_loc[self.phase.top_inner.user_setpoint_readback.name]["value"]
|
|
528
|
+
btm_inner = cur_loc[self.phase.btm_inner.user_setpoint_readback.name]["value"]
|
|
529
|
+
btm_outer = cur_loc[self.phase.btm_outer.user_setpoint_readback.name]["value"]
|
|
530
|
+
gap = cur_loc[self.gap.user_readback.name]["value"]
|
|
531
|
+
if gap > MAXIMUM_GAP_MOTOR_POSITION:
|
|
532
|
+
raise RuntimeError(
|
|
533
|
+
f"{self.name} is not in use, close gap or set polarisation to use this ID"
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
if all(
|
|
537
|
+
motor_position_equal(x, 0.0)
|
|
538
|
+
for x in [top_outer, top_inner, btm_inner, btm_outer]
|
|
539
|
+
):
|
|
540
|
+
# Linear Horizontal
|
|
541
|
+
polarisation = "lh"
|
|
542
|
+
phase = 0.0
|
|
543
|
+
return polarisation, phase
|
|
544
|
+
if (
|
|
545
|
+
motor_position_equal(top_outer, MAXIMUM_ROW_PHASE_MOTOR_POSITION)
|
|
546
|
+
and motor_position_equal(top_inner, 0.0)
|
|
547
|
+
and motor_position_equal(btm_inner, MAXIMUM_ROW_PHASE_MOTOR_POSITION)
|
|
548
|
+
and motor_position_equal(btm_outer, 0.0)
|
|
549
|
+
):
|
|
550
|
+
# Linear Vertical
|
|
551
|
+
polarisation = "lv"
|
|
552
|
+
phase = MAXIMUM_ROW_PHASE_MOTOR_POSITION
|
|
553
|
+
return polarisation, phase
|
|
554
|
+
if (
|
|
555
|
+
motor_position_equal(top_outer, btm_inner)
|
|
556
|
+
and top_outer > 0.0
|
|
557
|
+
and motor_position_equal(top_inner, 0.0)
|
|
558
|
+
and motor_position_equal(btm_outer, 0.0)
|
|
559
|
+
):
|
|
560
|
+
# Positive Circular
|
|
561
|
+
polarisation = "pc"
|
|
562
|
+
phase = top_outer
|
|
563
|
+
return polarisation, phase
|
|
564
|
+
if (
|
|
565
|
+
motor_position_equal(top_outer, btm_inner)
|
|
566
|
+
and top_outer < 0.0
|
|
567
|
+
and motor_position_equal(top_inner, 0.0)
|
|
568
|
+
and motor_position_equal(btm_outer, 0.0)
|
|
569
|
+
):
|
|
570
|
+
# Negative Circular
|
|
571
|
+
polarisation = "nc"
|
|
572
|
+
phase = top_outer
|
|
573
|
+
return polarisation, phase
|
|
574
|
+
if (
|
|
575
|
+
motor_position_equal(top_outer, -btm_inner)
|
|
576
|
+
and motor_position_equal(top_inner, 0.0)
|
|
577
|
+
and motor_position_equal(btm_outer, 0.0)
|
|
578
|
+
):
|
|
579
|
+
# Positive Linear Arbitrary
|
|
580
|
+
polarisation = "la"
|
|
581
|
+
phase = top_outer
|
|
582
|
+
return polarisation, phase
|
|
583
|
+
if (
|
|
584
|
+
motor_position_equal(top_inner, -btm_outer)
|
|
585
|
+
and motor_position_equal(top_outer, 0.0)
|
|
586
|
+
and motor_position_equal(btm_inner, 0.0)
|
|
587
|
+
):
|
|
588
|
+
# Negative Linear Arbitrary
|
|
589
|
+
polarisation = "la"
|
|
590
|
+
phase = top_inner
|
|
591
|
+
return polarisation, phase
|
|
592
|
+
# UNKNOWN default
|
|
593
|
+
polarisation = None
|
|
594
|
+
phase = 0.0
|
|
595
|
+
return (polarisation, phase)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def motor_position_equal(a, b) -> bool:
|
|
599
|
+
"""
|
|
600
|
+
Check motor is within tolerance.
|
|
601
|
+
"""
|
|
602
|
+
return abs(a - b) < ROW_PHASE_MOTOR_TOLERANCE
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import StandardReadable
|
|
4
|
+
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ColorMode(str, Enum):
|
|
8
|
+
"""
|
|
9
|
+
Enum to store the various color modes of the camera. We use RGB1.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
MONO = "Mono"
|
|
13
|
+
BAYER = "Bayer"
|
|
14
|
+
RGB1 = "RGB1"
|
|
15
|
+
RGB2 = "RGB2"
|
|
16
|
+
RGB3 = "RGB3"
|
|
17
|
+
YUV444 = "YUV444"
|
|
18
|
+
YUV422 = "YUV422"
|
|
19
|
+
YUV421 = "YUV421"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Cam(StandardReadable):
|
|
23
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
24
|
+
self.color_mode = epics_signal_rw(ColorMode, prefix + "ColorMode")
|
|
25
|
+
self.acquire_period = epics_signal_rw(float, prefix + "AcquirePeriod")
|
|
26
|
+
self.acquire_time = epics_signal_rw(float, prefix + "AcquireTime")
|
|
27
|
+
self.gain = epics_signal_rw(float, prefix + "Gain")
|
|
28
|
+
|
|
29
|
+
self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV")
|
|
30
|
+
self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV")
|
|
31
|
+
super().__init__(name)
|