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.
Files changed (55) hide show
  1. {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/METADATA +3 -3
  2. {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/RECORD +53 -43
  3. {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/WHEEL +1 -1
  4. dodal/__init__.py +8 -0
  5. dodal/_version.py +2 -2
  6. dodal/beamline_specific_utils/i03.py +6 -2
  7. dodal/beamlines/__init__.py +2 -3
  8. dodal/beamlines/b01_1.py +77 -0
  9. dodal/beamlines/i03.py +41 -9
  10. dodal/beamlines/i04.py +26 -4
  11. dodal/beamlines/i10.py +257 -0
  12. dodal/beamlines/i22.py +1 -2
  13. dodal/beamlines/i24.py +7 -7
  14. dodal/beamlines/p38.py +1 -2
  15. dodal/common/signal_utils.py +53 -0
  16. dodal/common/types.py +2 -7
  17. dodal/devices/aperturescatterguard.py +12 -15
  18. dodal/devices/apple2_undulator.py +602 -0
  19. dodal/devices/areadetector/plugins/CAM.py +31 -0
  20. dodal/devices/areadetector/plugins/MJPG.py +51 -106
  21. dodal/devices/backlight.py +7 -6
  22. dodal/devices/diamond_filter.py +47 -0
  23. dodal/devices/eiger.py +6 -2
  24. dodal/devices/eiger_odin.py +48 -39
  25. dodal/devices/focusing_mirror.py +14 -8
  26. dodal/devices/i10/i10_apple2.py +398 -0
  27. dodal/devices/i10/i10_setting_data.py +7 -0
  28. dodal/devices/i22/dcm.py +7 -8
  29. dodal/devices/i24/dual_backlight.py +5 -5
  30. dodal/devices/oav/oav_calculations.py +22 -0
  31. dodal/devices/oav/oav_detector.py +118 -97
  32. dodal/devices/oav/oav_parameters.py +50 -104
  33. dodal/devices/oav/oav_to_redis_forwarder.py +75 -34
  34. dodal/devices/oav/{grid_overlay.py → snapshots/grid_overlay.py} +0 -43
  35. dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +64 -0
  36. dodal/devices/oav/snapshots/snapshot_with_grid.py +57 -0
  37. dodal/devices/oav/utils.py +26 -25
  38. dodal/devices/pgm.py +41 -0
  39. dodal/devices/qbpm.py +18 -0
  40. dodal/devices/robot.py +2 -2
  41. dodal/devices/smargon.py +2 -2
  42. dodal/devices/tetramm.py +2 -2
  43. dodal/devices/undulator.py +2 -1
  44. dodal/devices/util/adjuster_plans.py +1 -1
  45. dodal/devices/util/lookup_tables.py +4 -5
  46. dodal/devices/zebra.py +5 -2
  47. dodal/devices/zocalo/zocalo_results.py +13 -10
  48. dodal/plans/data_session_metadata.py +2 -2
  49. dodal/plans/motor_util_plans.py +11 -9
  50. dodal/utils.py +7 -0
  51. dodal/beamlines/i04_1.py +0 -140
  52. dodal/devices/oav/oav_errors.py +0 -35
  53. {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/LICENSE +0 -0
  54. {dls_dodal-1.32.0.dist-info → dls_dodal-1.34.1.dist-info}/entry_points.txt +0 -0
  55. {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)