dls-dodal 1.47.0__py3-none-any.whl → 1.49.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.47.0.dist-info → dls_dodal-1.49.0.dist-info}/METADATA +3 -2
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/RECORD +59 -49
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/aithre.py +21 -0
- dodal/beamlines/b01_1.py +1 -1
- dodal/beamlines/b16.py +65 -0
- dodal/beamlines/b18.py +38 -0
- dodal/beamlines/i03.py +21 -6
- dodal/beamlines/i04.py +17 -10
- dodal/beamlines/i10.py +41 -233
- dodal/beamlines/i18.py +1 -1
- dodal/beamlines/i19_1.py +9 -6
- dodal/beamlines/i24.py +5 -5
- dodal/beamlines/k11.py +35 -0
- dodal/common/beamlines/beamline_parameters.py +2 -28
- dodal/common/beamlines/device_helpers.py +1 -0
- dodal/devices/aithre_lasershaping/goniometer.py +36 -2
- dodal/devices/aithre_lasershaping/laser_robot.py +27 -0
- dodal/devices/apple2_undulator.py +257 -136
- dodal/devices/b16/__init__.py +0 -0
- dodal/devices/b16/detector.py +34 -0
- dodal/devices/bimorph_mirror.py +29 -36
- dodal/devices/electron_analyser/__init__.py +21 -1
- dodal/devices/electron_analyser/abstract/__init__.py +0 -6
- dodal/devices/electron_analyser/abstract/base_detector.py +16 -128
- dodal/devices/electron_analyser/abstract/base_driver_io.py +122 -8
- dodal/devices/electron_analyser/abstract/base_region.py +7 -3
- dodal/devices/electron_analyser/detector.py +141 -0
- dodal/devices/electron_analyser/enums.py +6 -0
- dodal/devices/electron_analyser/specs/__init__.py +3 -2
- dodal/devices/electron_analyser/specs/detector.py +6 -22
- dodal/devices/electron_analyser/specs/driver_io.py +27 -3
- dodal/devices/electron_analyser/specs/enums.py +8 -0
- dodal/devices/electron_analyser/specs/region.py +3 -2
- dodal/devices/electron_analyser/types.py +30 -4
- dodal/devices/electron_analyser/util.py +1 -1
- dodal/devices/electron_analyser/vgscienta/__init__.py +3 -2
- dodal/devices/electron_analyser/vgscienta/detector.py +9 -23
- dodal/devices/electron_analyser/vgscienta/driver_io.py +33 -4
- dodal/devices/electron_analyser/vgscienta/enums.py +19 -0
- dodal/devices/electron_analyser/vgscienta/region.py +7 -23
- dodal/devices/fast_grid_scan.py +1 -1
- dodal/devices/i04/murko_results.py +93 -96
- dodal/devices/i10/__init__.py +0 -0
- dodal/devices/i10/i10_apple2.py +181 -126
- dodal/devices/i18/diode.py +37 -4
- dodal/devices/i22/nxsas.py +1 -1
- dodal/devices/mx_phase1/beamstop.py +23 -6
- dodal/devices/oav/oav_detector.py +101 -25
- dodal/devices/oav/oav_parameters.py +46 -16
- dodal/devices/oav/oav_to_redis_forwarder.py +2 -2
- dodal/devices/robot.py +20 -1
- dodal/devices/smargon.py +43 -4
- dodal/devices/zebra/zebra.py +8 -0
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +167 -0
- dodal/plan_stubs/electron_analyser/__init__.py +0 -3
- dodal/plan_stubs/electron_analyser/configure_driver.py +0 -92
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/top_level.txt +0 -0
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import asyncio
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from math import isclose
|
|
4
5
|
from typing import Any, Generic, TypeVar
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
from bluesky.protocols import Movable
|
|
8
9
|
from ophyd_async.core import (
|
|
9
10
|
AsyncStatus,
|
|
10
|
-
Reference,
|
|
11
11
|
SignalR,
|
|
12
12
|
SignalW,
|
|
13
13
|
StandardReadable,
|
|
14
14
|
StandardReadableFormat,
|
|
15
15
|
StrictEnum,
|
|
16
|
+
derived_signal_rw,
|
|
16
17
|
soft_signal_r_and_setter,
|
|
17
18
|
wait_for_value,
|
|
18
19
|
)
|
|
@@ -23,6 +24,8 @@ from dodal.log import LOGGER
|
|
|
23
24
|
|
|
24
25
|
T = TypeVar("T")
|
|
25
26
|
|
|
27
|
+
DEFAULT_MOTOR_MIN_TIMEOUT = 10
|
|
28
|
+
|
|
26
29
|
|
|
27
30
|
class UndulatorGateStatus(StrictEnum):
|
|
28
31
|
OPEN = "Open"
|
|
@@ -68,8 +71,7 @@ class LookupTableEntries(BaseModel):
|
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
class Lookuptable(RootModel):
|
|
71
|
-
"""
|
|
72
|
-
BaseModel class for the lookup table.
|
|
74
|
+
"""BaseModel class for the lookup table.
|
|
73
75
|
Apple2 lookup table should be in this format.
|
|
74
76
|
|
|
75
77
|
{mode: {'Energies': {Any: {'Low': float,
|
|
@@ -87,6 +89,16 @@ class Lookuptable(RootModel):
|
|
|
87
89
|
root: dict[str, LookupTableEntries]
|
|
88
90
|
|
|
89
91
|
|
|
92
|
+
class Pol(StrictEnum):
|
|
93
|
+
NONE = "None"
|
|
94
|
+
LH = "lh"
|
|
95
|
+
LV = "lv"
|
|
96
|
+
PC = "pc"
|
|
97
|
+
NC = "nc"
|
|
98
|
+
LA = "la"
|
|
99
|
+
LH3 = "lh3"
|
|
100
|
+
|
|
101
|
+
|
|
90
102
|
ROW_PHASE_MOTOR_TOLERANCE = 0.004
|
|
91
103
|
MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
|
|
92
104
|
MAXIMUM_GAP_MOTOR_POSITION = 100
|
|
@@ -98,7 +110,7 @@ async def estimate_motor_timeout(
|
|
|
98
110
|
vel = await velocity.get_value()
|
|
99
111
|
cur_pos = await curr_pos.get_value()
|
|
100
112
|
target_pos = float(await setpoint.get_value())
|
|
101
|
-
return abs((target_pos - cur_pos) * 2.0 / vel) +
|
|
113
|
+
return abs((target_pos - cur_pos) * 2.0 / vel) + DEFAULT_MOTOR_MIN_TIMEOUT
|
|
102
114
|
|
|
103
115
|
|
|
104
116
|
class SafeUndulatorMover(StandardReadable, Movable[T], Generic[T]):
|
|
@@ -216,11 +228,10 @@ class UndulatorPhaseMotor(StandardReadable):
|
|
|
216
228
|
"""
|
|
217
229
|
fullPV = f"{prefix}BL{infix}"
|
|
218
230
|
self.user_setpoint = epics_signal_w(str, fullPV + "SET")
|
|
219
|
-
self.
|
|
220
|
-
|
|
231
|
+
self.user_setpoint_readback = epics_signal_r(float, fullPV + "DMD")
|
|
221
232
|
fullPV = fullPV + "MTR"
|
|
222
233
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
223
|
-
self.
|
|
234
|
+
self.user_readback = epics_signal_r(float, fullPV + ".RBV")
|
|
224
235
|
|
|
225
236
|
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
226
237
|
self.motor_egu = epics_signal_r(str, fullPV + ".EGU")
|
|
@@ -282,20 +293,24 @@ class UndulatorPhaseAxes(SafeUndulatorMover[Apple2PhasesVal]):
|
|
|
282
293
|
timeouts = await asyncio.gather(
|
|
283
294
|
*[
|
|
284
295
|
estimate_motor_timeout(
|
|
285
|
-
axis.user_setpoint_demand_readback,
|
|
286
296
|
axis.user_setpoint_readback,
|
|
297
|
+
axis.user_readback,
|
|
287
298
|
axis.velocity,
|
|
288
299
|
)
|
|
289
300
|
for axis in axes
|
|
290
301
|
]
|
|
291
302
|
)
|
|
292
|
-
|
|
303
|
+
"""A 2.0 multiplier is required to prevent premature motor timeouts in phase
|
|
304
|
+
axes as it is a master-slave system, where the slave's movement,
|
|
305
|
+
being dependent on the master, can take up to twice as long to complete.
|
|
306
|
+
"""
|
|
307
|
+
return np.max(timeouts) * 2.0
|
|
293
308
|
|
|
294
309
|
|
|
295
310
|
class UndulatorJawPhase(SafeUndulatorMover[float]):
|
|
296
311
|
"""
|
|
297
312
|
A JawPhase movable, this is use for moving the jaw phase which is use to control the
|
|
298
|
-
linear arbitrary polarisation but only
|
|
313
|
+
linear arbitrary polarisation but only on some of the beamline.
|
|
299
314
|
"""
|
|
300
315
|
|
|
301
316
|
def __init__(
|
|
@@ -321,23 +336,70 @@ class UndulatorJawPhase(SafeUndulatorMover[float]):
|
|
|
321
336
|
Get motor speed, current position and target position to calculate required timeout.
|
|
322
337
|
"""
|
|
323
338
|
return await estimate_motor_timeout(
|
|
324
|
-
self.jaw_phase.user_setpoint_demand_readback,
|
|
325
339
|
self.jaw_phase.user_setpoint_readback,
|
|
340
|
+
self.jaw_phase.user_readback,
|
|
326
341
|
self.jaw_phase.velocity,
|
|
327
342
|
)
|
|
328
343
|
|
|
329
344
|
|
|
330
|
-
class Apple2(StandardReadable, Movable):
|
|
345
|
+
class Apple2(abc.ABC, StandardReadable, Movable):
|
|
331
346
|
"""
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
347
|
+
Apple2 Undulator Device
|
|
348
|
+
|
|
349
|
+
The `Apple2` class represents an Apple 2 insertion device (undulator) used in synchrotron beamlines.
|
|
350
|
+
This device provides additional degrees of freedom compared to standard undulators, allowing independent
|
|
351
|
+
movement of magnet banks to produce X-rays with various polarisations and energies.
|
|
352
|
+
|
|
353
|
+
The class is designed to manage the undulator's gap, phase motors, and polarisation settings, while
|
|
354
|
+
abstracting hardware interactions and providing a high-level interface for beamline operations.
|
|
355
|
+
|
|
336
356
|
|
|
337
357
|
A pair of look up tables are needed to provide the conversion between motor position
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
358
|
+
and energy.
|
|
359
|
+
|
|
360
|
+
Attributes
|
|
361
|
+
----------
|
|
362
|
+
gap : UndulatorGap
|
|
363
|
+
The gap control device for the undulator.
|
|
364
|
+
phase : UndulatorPhaseAxes
|
|
365
|
+
The phase control device, consisting of four phase motors.
|
|
366
|
+
energy : SignalR
|
|
367
|
+
A soft signal for the current energy readback.
|
|
368
|
+
polarisation_setpoint : SignalR
|
|
369
|
+
A soft signal for the polarisation setpoint.
|
|
370
|
+
polarisation : SignalRW
|
|
371
|
+
A hardware-backed signal for polarisation readback and control.
|
|
372
|
+
lookup_tables : dict
|
|
373
|
+
A dictionary storing lookup tables for gap and phase motor positions, used for energy and polarisation conversion.
|
|
374
|
+
_available_pol : list
|
|
375
|
+
A list of available polarisations supported by the device.
|
|
376
|
+
|
|
377
|
+
Abstract Methods
|
|
378
|
+
----------------
|
|
379
|
+
set(value: float) -> None
|
|
380
|
+
Abstract method to set motor positions for a given energy and polarisation.
|
|
381
|
+
update_lookuptable() -> None
|
|
382
|
+
Abstract method to load and validate lookup tables from external sources.
|
|
383
|
+
|
|
384
|
+
Methods
|
|
385
|
+
-------
|
|
386
|
+
_set_pol_setpoint(pol: Pol) -> None
|
|
387
|
+
Sets the polarisation setpoint without moving hardware.
|
|
388
|
+
determine_phase_from_hardware(...) -> tuple[Pol, float]
|
|
389
|
+
Determines the polarisation and phase value based on motor positions.
|
|
390
|
+
|
|
391
|
+
Notes
|
|
392
|
+
-----
|
|
393
|
+
- This class requires beamline-specific implementations of the abstract methods.
|
|
394
|
+
- The lookup tables must follow the `Lookuptable` format and be validated before use.
|
|
395
|
+
- The device supports multiple polarisation modes, including linear horizontal (LH), linear vertical (LV),
|
|
396
|
+
positive circular (PC), negative circular (NC), and linear arbitrary (LA).
|
|
397
|
+
|
|
398
|
+
For more detail see
|
|
399
|
+
`UML </_images/apple2_design.png>`__ for detail.
|
|
400
|
+
|
|
401
|
+
.. figure:: /explanations/umls/apple2_design.png
|
|
402
|
+
|
|
341
403
|
"""
|
|
342
404
|
|
|
343
405
|
def __init__(
|
|
@@ -348,129 +410,198 @@ class Apple2(StandardReadable, Movable):
|
|
|
348
410
|
name: str = "",
|
|
349
411
|
) -> None:
|
|
350
412
|
"""
|
|
413
|
+
|
|
351
414
|
Parameters
|
|
352
415
|
----------
|
|
353
|
-
id_gap:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
prefix:
|
|
358
|
-
Not in use but needed for device_instantiation.
|
|
359
|
-
name:
|
|
360
|
-
Name of the device.
|
|
416
|
+
id_gap: An UndulatorGap device.
|
|
417
|
+
id_phase: An UndulatorPhaseAxes device.
|
|
418
|
+
prefix: Not in use but needed for device_instantiation.
|
|
419
|
+
name: Name of the device.
|
|
361
420
|
"""
|
|
362
421
|
super().__init__(name)
|
|
363
422
|
|
|
364
423
|
# Attributes are set after super call so they are not renamed to
|
|
365
424
|
# <name>-undulator, etc.
|
|
366
|
-
self.gap =
|
|
367
|
-
self.phase =
|
|
425
|
+
self.gap = id_gap
|
|
426
|
+
self.phase = id_phase
|
|
368
427
|
|
|
369
428
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
370
|
-
# Store the polarisation for readback.
|
|
371
|
-
self.polarisation, self._polarisation_set = soft_signal_r_and_setter(
|
|
372
|
-
str, initial_value=None
|
|
373
|
-
)
|
|
374
429
|
# Store the set energy for readback.
|
|
375
|
-
self.energy, self.
|
|
430
|
+
self.energy, self._set_energy_rbv = soft_signal_r_and_setter(
|
|
376
431
|
float, initial_value=None
|
|
377
432
|
)
|
|
433
|
+
|
|
434
|
+
# Store the polarisation for setpoint. And provide readback for LH3.
|
|
435
|
+
# LH3 is a special case as it is indistinguishable from LH in the hardware.
|
|
436
|
+
self.polarisation_setpoint, self._polarisation_setpoint_set = (
|
|
437
|
+
soft_signal_r_and_setter(Pol)
|
|
438
|
+
)
|
|
378
439
|
# This store two lookup tables, Gap and Phase in the Lookuptable format
|
|
379
440
|
self.lookup_tables: dict[str, dict[str | None, dict[str, dict[str, Any]]]] = {
|
|
380
441
|
"Gap": {},
|
|
381
442
|
"Phase": {},
|
|
382
443
|
}
|
|
383
|
-
#
|
|
444
|
+
# Hardware backed read/write for polarisation.
|
|
445
|
+
self.polarisation = derived_signal_rw(
|
|
446
|
+
raw_to_derived=self._read_pol,
|
|
447
|
+
set_derived=self._set_pol,
|
|
448
|
+
pol=self.polarisation_setpoint,
|
|
449
|
+
top_outer=self.phase.top_outer.user_readback,
|
|
450
|
+
top_inner=self.phase.top_inner.user_readback,
|
|
451
|
+
btm_inner=self.phase.btm_inner.user_readback,
|
|
452
|
+
btm_outer=self.phase.btm_outer.user_readback,
|
|
453
|
+
gap=id_gap.user_readback,
|
|
454
|
+
)
|
|
455
|
+
|
|
384
456
|
self._available_pol = []
|
|
385
|
-
# The polarisation state of the id that are use for internal checking before setting.
|
|
386
|
-
self._pol = None
|
|
387
457
|
"""
|
|
388
458
|
Abstract method that run at start up to load lookup tables into self.lookup_tables
|
|
389
|
-
|
|
459
|
+
and set available_pol.
|
|
390
460
|
"""
|
|
391
461
|
self.update_lookuptable()
|
|
392
462
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
463
|
+
def _set_pol_setpoint(self, pol: Pol) -> None:
|
|
464
|
+
"""Set the polarisation setpoint without moving hardware. The polarisation
|
|
465
|
+
setpoint is used to determine the gap and phase motor positions when
|
|
466
|
+
setting the energy/polarisation of the undulator."""
|
|
467
|
+
self._polarisation_setpoint_set(pol)
|
|
396
468
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
469
|
+
async def _set_pol(
|
|
470
|
+
self,
|
|
471
|
+
value: Pol,
|
|
472
|
+
) -> None:
|
|
473
|
+
# This changes the pol setpoint and then changes polarisation via set energy.
|
|
474
|
+
self._set_pol_setpoint(value)
|
|
475
|
+
await self.set(await self.energy.get_value())
|
|
476
|
+
|
|
477
|
+
@abc.abstractmethod
|
|
478
|
+
@AsyncStatus.wrap
|
|
479
|
+
async def set(self, value: float) -> None:
|
|
480
|
+
"""
|
|
481
|
+
Set should be in energy units, this will set the energy of the ID by setting the
|
|
482
|
+
gap and phase motors to the correct position for the given energy
|
|
483
|
+
and polarisation.
|
|
484
|
+
This method should be implemented by the beamline specific ID class as the
|
|
485
|
+
motor positions will be different for each beamline depending on the
|
|
486
|
+
undulator design and the lookup table used.
|
|
487
|
+
_set can be used to set the motor positions for the given energy and
|
|
488
|
+
polarisation provided that all motors can be moved at the same time.
|
|
489
|
+
|
|
490
|
+
Examples
|
|
491
|
+
--------
|
|
492
|
+
>>> RE( id.set(888.0)) # This will set the ID to 888 eV
|
|
493
|
+
>>> RE(scan([detector], id,600,700,100)) # This will scan the ID from 600 to 700 eV in 100 steps.
|
|
494
|
+
"""
|
|
495
|
+
|
|
496
|
+
def _read_pol(
|
|
497
|
+
self,
|
|
498
|
+
pol: Pol,
|
|
499
|
+
top_outer: float,
|
|
500
|
+
top_inner: float,
|
|
501
|
+
btm_inner: float,
|
|
502
|
+
btm_outer: float,
|
|
503
|
+
gap: float,
|
|
504
|
+
) -> Pol:
|
|
505
|
+
LOGGER.info(
|
|
506
|
+
f"Reading polarisation setpoint from hardware: "
|
|
507
|
+
f"top_outer={top_outer}, top_inner={top_inner}, "
|
|
508
|
+
f"btm_inner={btm_inner}, btm_outer={btm_outer}, gap={gap}."
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
read_pol, _ = self.determine_phase_from_hardware(
|
|
512
|
+
top_outer, top_inner, btm_inner, btm_outer, gap
|
|
513
|
+
)
|
|
514
|
+
# LH3 is indistinguishable from LH see determine_phase_from_hardware's docString
|
|
515
|
+
# so we return LH3 if the setpoint is LH3 and the readback is LH.
|
|
516
|
+
if pol == Pol.LH3 and read_pol == Pol.LH:
|
|
517
|
+
LOGGER.info(
|
|
518
|
+
"The hardware cannot distinguish between LH and LH3."
|
|
519
|
+
" Returning the last commanded polarisation value"
|
|
406
520
|
)
|
|
521
|
+
return Pol.LH3
|
|
522
|
+
|
|
523
|
+
return read_pol
|
|
407
524
|
|
|
408
525
|
async def _set(self, value: Apple2Val, energy: float) -> None:
|
|
409
526
|
"""
|
|
410
|
-
Check ID is in a movable state and set all the demand value before moving
|
|
411
|
-
|
|
527
|
+
Check ID is in a movable state and set all the demand value before moving them
|
|
528
|
+
all at the same time. This should be modified by the beamline specific ID class
|
|
529
|
+
, if the ID motors has to move in a specific order.
|
|
412
530
|
"""
|
|
413
531
|
|
|
414
532
|
# Only need to check gap as the phase motors share both fault and gate with gap.
|
|
415
|
-
await self.gap
|
|
533
|
+
await self.gap.raise_if_cannot_move()
|
|
416
534
|
await asyncio.gather(
|
|
417
|
-
self.phase
|
|
418
|
-
self.phase
|
|
419
|
-
self.phase
|
|
420
|
-
self.phase
|
|
421
|
-
self.gap
|
|
535
|
+
self.phase.top_outer.user_setpoint.set(value=value.top_outer),
|
|
536
|
+
self.phase.top_inner.user_setpoint.set(value=value.top_inner),
|
|
537
|
+
self.phase.btm_inner.user_setpoint.set(value=value.btm_inner),
|
|
538
|
+
self.phase.btm_outer.user_setpoint.set(value=value.btm_outer),
|
|
539
|
+
self.gap.user_setpoint.set(value=value.gap),
|
|
422
540
|
)
|
|
423
541
|
timeout = np.max(
|
|
424
|
-
await asyncio.gather(self.gap
|
|
542
|
+
await asyncio.gather(self.gap.get_timeout(), self.phase.get_timeout())
|
|
425
543
|
)
|
|
426
544
|
LOGGER.info(
|
|
427
|
-
f"Moving f{self.name} energy and polorisation to {energy}, {self.
|
|
545
|
+
f"Moving f{self.name} energy and polorisation to {energy}, {await self.polarisation.get_value()}"
|
|
428
546
|
+ f"with motor position {value}, timeout = {timeout}"
|
|
429
547
|
)
|
|
430
|
-
|
|
431
548
|
await asyncio.gather(
|
|
432
|
-
self.gap
|
|
433
|
-
self.phase
|
|
434
|
-
)
|
|
435
|
-
await wait_for_value(
|
|
436
|
-
self.gap().gate, UndulatorGateStatus.CLOSE, timeout=timeout
|
|
549
|
+
self.gap.set_move.set(value=1, wait=False, timeout=timeout),
|
|
550
|
+
self.phase.set_move.set(value=1, wait=False, timeout=timeout),
|
|
437
551
|
)
|
|
438
|
-
self.
|
|
552
|
+
await wait_for_value(self.gap.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
|
|
553
|
+
self._set_energy_rbv(energy) # Update energy after move for readback.
|
|
439
554
|
|
|
440
|
-
def _get_id_gap_phase(self, energy: float) -> tuple[float, float]:
|
|
555
|
+
async def _get_id_gap_phase(self, energy: float) -> tuple[float, float]:
|
|
441
556
|
"""
|
|
442
557
|
Converts energy and polarisation to gap and phase.
|
|
443
558
|
"""
|
|
444
|
-
gap_poly = self._get_poly(
|
|
559
|
+
gap_poly = await self._get_poly(
|
|
445
560
|
lookup_table=self.lookup_tables["Gap"], new_energy=energy
|
|
446
561
|
)
|
|
447
|
-
phase_poly = self._get_poly(
|
|
562
|
+
phase_poly = await self._get_poly(
|
|
448
563
|
lookup_table=self.lookup_tables["Phase"], new_energy=energy
|
|
449
564
|
)
|
|
450
565
|
return gap_poly(energy), phase_poly(energy)
|
|
451
566
|
|
|
452
|
-
def _get_poly(
|
|
567
|
+
async def _get_poly(
|
|
453
568
|
self,
|
|
454
569
|
new_energy: float,
|
|
455
570
|
lookup_table: dict[str | None, dict[str, dict[str, Any]]],
|
|
456
571
|
) -> np.poly1d:
|
|
457
572
|
"""
|
|
458
573
|
Get the correct polynomial for a given energy form lookuptable
|
|
459
|
-
|
|
574
|
+
for the current polarisation setpoint.
|
|
575
|
+
Parameters
|
|
576
|
+
----------
|
|
577
|
+
new_energy : float
|
|
578
|
+
The energy in eV for which the polynomial is requested.
|
|
579
|
+
lookup_table : dict[str | None, dict[str, dict[str, Any]]]
|
|
580
|
+
The lookup table containing polynomial coefficients for different energies
|
|
581
|
+
and polarisations.
|
|
582
|
+
Returns
|
|
583
|
+
-------
|
|
584
|
+
np.poly1d
|
|
585
|
+
The polynomial coefficients for the requested energy and polarisation.
|
|
586
|
+
Raises
|
|
587
|
+
------
|
|
588
|
+
ValueError
|
|
589
|
+
If the requested energy is outside the limits defined in the lookup table
|
|
590
|
+
or if no polynomial coefficients are found for the requested energy.
|
|
460
591
|
"""
|
|
461
|
-
|
|
592
|
+
pol = await self.polarisation_setpoint.get_value()
|
|
462
593
|
if (
|
|
463
|
-
new_energy < lookup_table[
|
|
464
|
-
or new_energy > lookup_table[
|
|
594
|
+
new_energy < lookup_table[pol]["Limit"]["Minimum"]
|
|
595
|
+
or new_energy > lookup_table[pol]["Limit"]["Maximum"]
|
|
465
596
|
):
|
|
466
597
|
raise ValueError(
|
|
467
598
|
"Demanding energy must lie between {} and {} eV!".format(
|
|
468
|
-
lookup_table[
|
|
469
|
-
lookup_table[
|
|
599
|
+
lookup_table[pol]["Limit"]["Minimum"],
|
|
600
|
+
lookup_table[pol]["Limit"]["Maximum"],
|
|
470
601
|
)
|
|
471
602
|
)
|
|
472
603
|
else:
|
|
473
|
-
for energy_range in lookup_table[
|
|
604
|
+
for energy_range in lookup_table[pol]["Energies"].values():
|
|
474
605
|
if (
|
|
475
606
|
new_energy >= energy_range["Low"]
|
|
476
607
|
and new_energy < energy_range["High"]
|
|
@@ -492,87 +623,77 @@ class Apple2(StandardReadable, Movable):
|
|
|
492
623
|
|
|
493
624
|
"""
|
|
494
625
|
|
|
495
|
-
|
|
626
|
+
def determine_phase_from_hardware(
|
|
627
|
+
self,
|
|
628
|
+
top_outer: float,
|
|
629
|
+
top_inner: float,
|
|
630
|
+
btm_inner: float,
|
|
631
|
+
btm_outer: float,
|
|
632
|
+
gap: float,
|
|
633
|
+
) -> tuple[Pol, float]:
|
|
496
634
|
"""
|
|
497
|
-
|
|
635
|
+
Determine polarisation and phase value using motor position patterns.
|
|
498
636
|
However there is no way to return lh3 polarisation or higher harmonic setting.
|
|
499
637
|
(May be for future one can use the inverse poly to work out the energy and try to match it with the current energy
|
|
500
638
|
to workout the polarisation but during my test the inverse poly is too unstable for general use.)
|
|
501
639
|
"""
|
|
502
|
-
top_outer = await self.phase().top_outer.user_setpoint_readback.get_value()
|
|
503
|
-
top_inner = await self.phase().top_inner.user_setpoint_readback.get_value()
|
|
504
|
-
btm_inner = await self.phase().btm_inner.user_setpoint_readback.get_value()
|
|
505
|
-
btm_outer = await self.phase().btm_outer.user_setpoint_readback.get_value()
|
|
506
|
-
gap = await self.gap().user_readback.get_value()
|
|
507
640
|
if gap > MAXIMUM_GAP_MOTOR_POSITION:
|
|
508
641
|
raise RuntimeError(
|
|
509
642
|
f"{self.name} is not in use, close gap or set polarisation to use this ID"
|
|
510
643
|
)
|
|
511
644
|
|
|
512
645
|
if all(
|
|
513
|
-
|
|
646
|
+
isclose(x, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
514
647
|
for x in [top_outer, top_inner, btm_inner, btm_outer]
|
|
515
648
|
):
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
phase = 0.0
|
|
519
|
-
return polarisation, phase
|
|
649
|
+
LOGGER.info("Determined polarisation: LH (Linear Horizontal).")
|
|
650
|
+
return Pol.LH, 0.0
|
|
520
651
|
if (
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
652
|
+
isclose(
|
|
653
|
+
top_outer,
|
|
654
|
+
MAXIMUM_ROW_PHASE_MOTOR_POSITION,
|
|
655
|
+
abs_tol=ROW_PHASE_MOTOR_TOLERANCE,
|
|
656
|
+
)
|
|
657
|
+
and isclose(top_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
658
|
+
and isclose(
|
|
659
|
+
btm_inner,
|
|
660
|
+
MAXIMUM_ROW_PHASE_MOTOR_POSITION,
|
|
661
|
+
abs_tol=ROW_PHASE_MOTOR_TOLERANCE,
|
|
662
|
+
)
|
|
663
|
+
and isclose(btm_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
525
664
|
):
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
phase = MAXIMUM_ROW_PHASE_MOTOR_POSITION
|
|
529
|
-
return polarisation, phase
|
|
665
|
+
LOGGER.info("Determined polarisation: LV (Linear Vertical).")
|
|
666
|
+
return Pol.LV, MAXIMUM_ROW_PHASE_MOTOR_POSITION
|
|
530
667
|
if (
|
|
531
|
-
|
|
668
|
+
isclose(top_outer, btm_inner, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
532
669
|
and top_outer > 0.0
|
|
533
|
-
and
|
|
534
|
-
and
|
|
670
|
+
and isclose(top_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
671
|
+
and isclose(btm_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
535
672
|
):
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
phase = top_outer
|
|
539
|
-
return polarisation, phase
|
|
673
|
+
LOGGER.info("Determined polarisation: PC (Positive Circular).")
|
|
674
|
+
return Pol.PC, top_outer
|
|
540
675
|
if (
|
|
541
|
-
|
|
676
|
+
isclose(top_outer, btm_inner, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
542
677
|
and top_outer < 0.0
|
|
543
|
-
and
|
|
544
|
-
and
|
|
678
|
+
and isclose(top_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
679
|
+
and isclose(btm_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
545
680
|
):
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
phase = top_outer
|
|
549
|
-
return polarisation, phase
|
|
681
|
+
LOGGER.info("Determined polarisation: NC (Negative Circular).")
|
|
682
|
+
return Pol.NC, top_outer
|
|
550
683
|
if (
|
|
551
|
-
|
|
552
|
-
and
|
|
553
|
-
and
|
|
684
|
+
isclose(top_outer, -btm_inner, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
685
|
+
and isclose(top_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
686
|
+
and isclose(btm_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
554
687
|
):
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
phase = top_outer
|
|
558
|
-
return polarisation, phase
|
|
688
|
+
LOGGER.info("Determined polarisation: LA (Positive Linear Arbitrary).")
|
|
689
|
+
return Pol.LA, top_outer
|
|
559
690
|
if (
|
|
560
|
-
|
|
561
|
-
and
|
|
562
|
-
and
|
|
691
|
+
isclose(top_inner, -btm_outer, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
692
|
+
and isclose(top_outer, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
693
|
+
and isclose(btm_inner, 0.0, abs_tol=ROW_PHASE_MOTOR_TOLERANCE)
|
|
563
694
|
):
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
phase = top_inner
|
|
567
|
-
return polarisation, phase
|
|
568
|
-
# UNKNOWN default
|
|
569
|
-
polarisation = None
|
|
570
|
-
phase = 0.0
|
|
571
|
-
return (polarisation, phase)
|
|
572
|
-
|
|
695
|
+
LOGGER.info("Determined polarisation: LA (Negative Linear Arbitrary).")
|
|
696
|
+
return Pol.LA, top_inner
|
|
573
697
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
Check motor is within tolerance.
|
|
577
|
-
"""
|
|
578
|
-
return abs(a - b) < ROW_PHASE_MOTOR_TOLERANCE
|
|
698
|
+
LOGGER.warning("Unable to determine polarisation. Defaulting to NONE.")
|
|
699
|
+
return Pol.NONE, 0.0
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from ophyd_async.epics.adcore import (
|
|
2
|
+
ADBaseController,
|
|
3
|
+
ADBaseIO,
|
|
4
|
+
ADTIFFWriter,
|
|
5
|
+
AreaDetector,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from dodal.common.beamlines.beamline_utils import get_path_provider
|
|
9
|
+
from dodal.common.beamlines.device_helpers import CAM_SUFFIX, TIFF_SUFFIX
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConstantDeadTimeController(ADBaseController):
|
|
13
|
+
def __init__(self, driver: ADBaseIO, deadtime: float):
|
|
14
|
+
super().__init__(driver)
|
|
15
|
+
self.deadtime = deadtime
|
|
16
|
+
|
|
17
|
+
def get_deadtime(self, exposure: float | None) -> float:
|
|
18
|
+
return self.deadtime
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def software_triggered_tiff_area_detector(prefix: str, deadtime: float = 0.0):
|
|
22
|
+
"""
|
|
23
|
+
Wrapper for AreaDetector with fixed dead time (defaulted to 0)
|
|
24
|
+
and a TIFF file writer.
|
|
25
|
+
Most detectors in B16 could be configured like this
|
|
26
|
+
"""
|
|
27
|
+
return AreaDetector(
|
|
28
|
+
writer=ADTIFFWriter.with_io(
|
|
29
|
+
prefix=prefix, path_provider=get_path_provider(), fileio_suffix=TIFF_SUFFIX
|
|
30
|
+
),
|
|
31
|
+
controller=ConstantDeadTimeController(
|
|
32
|
+
driver=ADBaseIO(prefix + CAM_SUFFIX), deadtime=deadtime
|
|
33
|
+
),
|
|
34
|
+
)
|