dls-dodal 1.33.0__py3-none-any.whl → 1.35.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 (89) hide show
  1. {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/METADATA +3 -3
  2. dls_dodal-1.35.0.dist-info/RECORD +147 -0
  3. {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.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/i03.py +41 -9
  9. dodal/beamlines/i04.py +26 -4
  10. dodal/beamlines/i10.py +257 -0
  11. dodal/beamlines/i22.py +25 -13
  12. dodal/beamlines/i24.py +11 -11
  13. dodal/beamlines/p38.py +24 -13
  14. dodal/common/beamlines/beamline_utils.py +1 -2
  15. dodal/common/crystal_metadata.py +61 -0
  16. dodal/common/signal_utils.py +10 -14
  17. dodal/common/types.py +2 -7
  18. dodal/devices/CTAB.py +1 -1
  19. dodal/devices/aperture.py +1 -1
  20. dodal/devices/aperturescatterguard.py +20 -8
  21. dodal/devices/apple2_undulator.py +603 -0
  22. dodal/devices/areadetector/plugins/CAM.py +29 -0
  23. dodal/devices/areadetector/plugins/MJPG.py +51 -106
  24. dodal/devices/attenuator.py +1 -1
  25. dodal/devices/backlight.py +11 -11
  26. dodal/devices/cryostream.py +3 -5
  27. dodal/devices/dcm.py +26 -2
  28. dodal/devices/detector/detector_motion.py +3 -5
  29. dodal/devices/diamond_filter.py +46 -0
  30. dodal/devices/eiger.py +6 -2
  31. dodal/devices/eiger_odin.py +48 -39
  32. dodal/devices/fast_grid_scan.py +1 -1
  33. dodal/devices/fluorescence_detector_motion.py +5 -7
  34. dodal/devices/focusing_mirror.py +26 -19
  35. dodal/devices/hutch_shutter.py +4 -5
  36. dodal/devices/i10/i10_apple2.py +399 -0
  37. dodal/devices/i10/i10_setting_data.py +7 -0
  38. dodal/devices/i22/dcm.py +50 -83
  39. dodal/devices/i22/fswitch.py +5 -5
  40. dodal/devices/i24/aperture.py +3 -5
  41. dodal/devices/i24/beamstop.py +3 -5
  42. dodal/devices/i24/dcm.py +1 -1
  43. dodal/devices/i24/dual_backlight.py +9 -11
  44. dodal/devices/i24/pmac.py +35 -46
  45. dodal/devices/i24/vgonio.py +16 -0
  46. dodal/devices/ipin.py +5 -3
  47. dodal/devices/linkam3.py +7 -7
  48. dodal/devices/oav/oav_calculations.py +22 -0
  49. dodal/devices/oav/oav_detector.py +118 -83
  50. dodal/devices/oav/oav_parameters.py +50 -104
  51. dodal/devices/oav/oav_to_redis_forwarder.py +77 -35
  52. dodal/devices/oav/pin_image_recognition/__init__.py +9 -7
  53. dodal/devices/oav/{grid_overlay.py → snapshots/grid_overlay.py} +16 -59
  54. dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +64 -0
  55. dodal/devices/oav/snapshots/snapshot_with_grid.py +57 -0
  56. dodal/devices/oav/utils.py +28 -27
  57. dodal/devices/p99/sample_stage.py +3 -5
  58. dodal/devices/pgm.py +40 -0
  59. dodal/devices/qbpm.py +18 -0
  60. dodal/devices/robot.py +5 -5
  61. dodal/devices/smargon.py +3 -3
  62. dodal/devices/synchrotron.py +9 -4
  63. dodal/devices/tetramm.py +9 -9
  64. dodal/devices/thawer.py +13 -7
  65. dodal/devices/undulator.py +7 -6
  66. dodal/devices/util/adjuster_plans.py +1 -1
  67. dodal/devices/util/epics_util.py +1 -1
  68. dodal/devices/util/lookup_tables.py +4 -5
  69. dodal/devices/watsonmarlow323_pump.py +45 -0
  70. dodal/devices/webcam.py +9 -2
  71. dodal/devices/xbpm_feedback.py +3 -5
  72. dodal/devices/xspress3/xspress3.py +8 -9
  73. dodal/devices/xspress3/xspress3_channel.py +3 -5
  74. dodal/devices/zebra.py +12 -8
  75. dodal/devices/zebra_controlled_shutter.py +5 -6
  76. dodal/devices/zocalo/__init__.py +2 -2
  77. dodal/devices/zocalo/zocalo_constants.py +3 -0
  78. dodal/devices/zocalo/zocalo_interaction.py +2 -1
  79. dodal/devices/zocalo/zocalo_results.py +105 -89
  80. dodal/plans/data_session_metadata.py +2 -2
  81. dodal/plans/motor_util_plans.py +11 -9
  82. dodal/utils.py +11 -0
  83. dls_dodal-1.33.0.dist-info/RECORD +0 -136
  84. dodal/beamlines/i04_1.py +0 -140
  85. dodal/devices/i24/i24_vgonio.py +0 -17
  86. dodal/devices/oav/oav_errors.py +0 -35
  87. {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/LICENSE +0 -0
  88. {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/entry_points.txt +0 -0
  89. {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,603 @@
1
+ import abc
2
+ import asyncio
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ import numpy as np
7
+ from bluesky.protocols import Movable
8
+ from ophyd_async.core import (
9
+ AsyncStatus,
10
+ Reference,
11
+ StandardReadable,
12
+ StandardReadableFormat,
13
+ StrictEnum,
14
+ soft_signal_r_and_setter,
15
+ wait_for_value,
16
+ )
17
+ from ophyd_async.epics.core 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(StrictEnum):
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(StandardReadableFormat.CONFIG_SIGNAL):
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(StandardReadableFormat.HINTED_SIGNAL):
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(StandardReadableFormat.HINTED_SIGNAL):
191
+ self.user_setpoint_readback = epics_signal_r(float, fullPV + ".RBV")
192
+
193
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
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
+ self.gap = Reference(id_gap)
392
+ self.phase = Reference(id_phase)
393
+
394
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
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(
461
+ self.gap().gate, UndulatorGateStatus.close, timeout=timeout
462
+ )
463
+ self._energy_set(energy) # Update energy for after move for readback.
464
+
465
+ def _get_id_gap_phase(self, energy: float) -> tuple[float, float]:
466
+ """
467
+ Converts energy and polarisation to gap and phase.
468
+ """
469
+ gap_poly = self._get_poly(
470
+ lookup_table=self.lookup_tables["Gap"], new_energy=energy
471
+ )
472
+ phase_poly = self._get_poly(
473
+ lookup_table=self.lookup_tables["Phase"], new_energy=energy
474
+ )
475
+ return gap_poly(energy), phase_poly(energy)
476
+
477
+ def _get_poly(
478
+ self,
479
+ new_energy: float,
480
+ lookup_table: dict[str | None, dict[str, dict[str, Any]]],
481
+ ) -> np.poly1d:
482
+ """
483
+ Get the correct polynomial for a given energy form lookuptable
484
+ for any given polarisation.
485
+ """
486
+
487
+ if (
488
+ new_energy < lookup_table[self.pol]["Limit"]["Minimum"]
489
+ or new_energy > lookup_table[self.pol]["Limit"]["Maximum"]
490
+ ):
491
+ raise ValueError(
492
+ "Demanding energy must lie between {} and {} eV!".format(
493
+ lookup_table[self.pol]["Limit"]["Minimum"],
494
+ lookup_table[self.pol]["Limit"]["Maximum"],
495
+ )
496
+ )
497
+ else:
498
+ for energy_range in lookup_table[self.pol]["Energies"].values():
499
+ if (
500
+ new_energy >= energy_range["Low"]
501
+ and new_energy < energy_range["High"]
502
+ ):
503
+ return energy_range["Poly"]
504
+
505
+ raise ValueError(
506
+ """Cannot find polynomial coefficients for your requested energy.
507
+ There might be gap in the calibration lookup table."""
508
+ )
509
+
510
+ @abc.abstractmethod
511
+ def update_lookuptable(self) -> None:
512
+ """
513
+ Abstract method to update the stored lookup tabled from file.
514
+ This function should include check to ensure the lookuptable is in the correct format:
515
+ # ensure the importing lookup table is the correct format
516
+ Lookuptable.model_validate(<loockuptable>)
517
+
518
+ """
519
+
520
+ async def determinePhaseFromHardware(self) -> tuple[str | None, float]:
521
+ """
522
+ Try to determine polarisation and phase value using row phase motor position pattern.
523
+ However there is no way to return lh3 polarisation or higher harmonic setting.
524
+ (May be for future one can use the inverse poly to work out the energy and try to match it with the current energy
525
+ to workout the polarisation but during my test the inverse poly is too unstable for general use.)
526
+ """
527
+ top_outer = await self.phase().top_outer.user_setpoint_readback.get_value()
528
+ top_inner = await self.phase().top_inner.user_setpoint_readback.get_value()
529
+ btm_inner = await self.phase().btm_inner.user_setpoint_readback.get_value()
530
+ btm_outer = await self.phase().btm_outer.user_setpoint_readback.get_value()
531
+ gap = await self.gap().user_readback.get_value()
532
+ if gap > MAXIMUM_GAP_MOTOR_POSITION:
533
+ raise RuntimeError(
534
+ f"{self.name} is not in use, close gap or set polarisation to use this ID"
535
+ )
536
+
537
+ if all(
538
+ motor_position_equal(x, 0.0)
539
+ for x in [top_outer, top_inner, btm_inner, btm_outer]
540
+ ):
541
+ # Linear Horizontal
542
+ polarisation = "lh"
543
+ phase = 0.0
544
+ return polarisation, phase
545
+ if (
546
+ motor_position_equal(top_outer, MAXIMUM_ROW_PHASE_MOTOR_POSITION)
547
+ and motor_position_equal(top_inner, 0.0)
548
+ and motor_position_equal(btm_inner, MAXIMUM_ROW_PHASE_MOTOR_POSITION)
549
+ and motor_position_equal(btm_outer, 0.0)
550
+ ):
551
+ # Linear Vertical
552
+ polarisation = "lv"
553
+ phase = MAXIMUM_ROW_PHASE_MOTOR_POSITION
554
+ return polarisation, phase
555
+ if (
556
+ motor_position_equal(top_outer, btm_inner)
557
+ and top_outer > 0.0
558
+ and motor_position_equal(top_inner, 0.0)
559
+ and motor_position_equal(btm_outer, 0.0)
560
+ ):
561
+ # Positive Circular
562
+ polarisation = "pc"
563
+ phase = top_outer
564
+ return polarisation, phase
565
+ if (
566
+ motor_position_equal(top_outer, btm_inner)
567
+ and top_outer < 0.0
568
+ and motor_position_equal(top_inner, 0.0)
569
+ and motor_position_equal(btm_outer, 0.0)
570
+ ):
571
+ # Negative Circular
572
+ polarisation = "nc"
573
+ phase = top_outer
574
+ return polarisation, phase
575
+ if (
576
+ motor_position_equal(top_outer, -btm_inner)
577
+ and motor_position_equal(top_inner, 0.0)
578
+ and motor_position_equal(btm_outer, 0.0)
579
+ ):
580
+ # Positive Linear Arbitrary
581
+ polarisation = "la"
582
+ phase = top_outer
583
+ return polarisation, phase
584
+ if (
585
+ motor_position_equal(top_inner, -btm_outer)
586
+ and motor_position_equal(top_outer, 0.0)
587
+ and motor_position_equal(btm_inner, 0.0)
588
+ ):
589
+ # Negative Linear Arbitrary
590
+ polarisation = "la"
591
+ phase = top_inner
592
+ return polarisation, phase
593
+ # UNKNOWN default
594
+ polarisation = None
595
+ phase = 0.0
596
+ return (polarisation, phase)
597
+
598
+
599
+ def motor_position_equal(a, b) -> bool:
600
+ """
601
+ Check motor is within tolerance.
602
+ """
603
+ return abs(a - b) < ROW_PHASE_MOTOR_TOLERANCE
@@ -0,0 +1,29 @@
1
+ from ophyd_async.core import StandardReadable, StrictEnum
2
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
3
+
4
+
5
+ class ColorMode(StrictEnum):
6
+ """
7
+ Enum to store the various color modes of the camera. We use RGB1.
8
+ """
9
+
10
+ MONO = "Mono"
11
+ BAYER = "Bayer"
12
+ RGB1 = "RGB1"
13
+ RGB2 = "RGB2"
14
+ RGB3 = "RGB3"
15
+ YUV444 = "YUV444"
16
+ YUV422 = "YUV422"
17
+ YUV421 = "YUV421"
18
+
19
+
20
+ class Cam(StandardReadable):
21
+ def __init__(self, prefix: str, name: str = "") -> None:
22
+ self.color_mode = epics_signal_rw(ColorMode, prefix + "ColorMode")
23
+ self.acquire_period = epics_signal_rw(float, prefix + "AcquirePeriod")
24
+ self.acquire_time = epics_signal_rw(float, prefix + "AcquireTime")
25
+ self.gain = epics_signal_rw(float, prefix + "Gain")
26
+
27
+ self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV")
28
+ self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV")
29
+ super().__init__(name)