dls-dodal 1.61.0__py3-none-any.whl → 1.63.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.61.0.dist-info → dls_dodal-1.63.0.dist-info}/METADATA +1 -1
- {dls_dodal-1.61.0.dist-info → dls_dodal-1.63.0.dist-info}/RECORD +81 -73
- dls_dodal-1.63.0.dist-info/entry_points.txt +3 -0
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +1 -0
- dodal/beamlines/adsim.py +5 -3
- dodal/beamlines/b21.py +3 -1
- dodal/beamlines/i02_2.py +32 -0
- dodal/beamlines/i03.py +9 -0
- dodal/beamlines/i04.py +1 -1
- dodal/beamlines/i09.py +10 -3
- dodal/beamlines/i09_1.py +9 -3
- dodal/beamlines/i10.py +7 -69
- dodal/beamlines/i10_1.py +35 -0
- dodal/beamlines/i10_optics.py +205 -0
- dodal/beamlines/i15_1.py +5 -5
- dodal/beamlines/i17.py +50 -1
- dodal/beamlines/i18.py +15 -9
- dodal/beamlines/i19_1.py +3 -3
- dodal/beamlines/i19_2.py +12 -2
- dodal/beamlines/i19_optics.py +4 -1
- dodal/beamlines/i24.py +3 -3
- dodal/cli.py +4 -4
- dodal/common/visit.py +4 -4
- dodal/devices/aperturescatterguard.py +6 -4
- dodal/devices/apple2_undulator.py +211 -114
- dodal/devices/attenuator/filter_selections.py +6 -6
- dodal/devices/common_dcm.py +62 -15
- dodal/devices/controllers.py +8 -6
- dodal/devices/current_amplifiers/femto.py +4 -4
- dodal/devices/current_amplifiers/sr570.py +3 -3
- dodal/devices/fast_grid_scan.py +97 -21
- dodal/devices/fast_shutter.py +69 -0
- dodal/devices/i02_1/fast_grid_scan.py +1 -1
- dodal/devices/i02_2/__init__.py +0 -0
- dodal/devices/i03/dcm.py +4 -2
- dodal/devices/i04/murko_results.py +35 -14
- dodal/devices/i09/__init__.py +1 -2
- dodal/devices/i10/__init__.py +29 -0
- dodal/devices/i10/diagnostics.py +37 -5
- dodal/devices/i10/i10_apple2.py +125 -229
- dodal/devices/i10/slits.py +38 -6
- dodal/devices/i15/dcm.py +6 -44
- dodal/devices/i17/__init__.py +0 -0
- dodal/devices/i17/i17_apple2.py +51 -0
- dodal/devices/i19/access_controlled/__init__.py +0 -0
- dodal/devices/i19/{shutter.py → access_controlled/shutter.py} +7 -4
- dodal/devices/i19/mapt_configuration.py +38 -0
- dodal/devices/i19/pin_col_stages.py +170 -0
- dodal/devices/i22/dcm.py +2 -2
- dodal/devices/i24/dcm.py +2 -2
- dodal/devices/oav/oav_detector.py +1 -1
- dodal/devices/oav/oav_parameters.py +4 -4
- dodal/devices/oav/oav_to_redis_forwarder.py +4 -4
- dodal/devices/oav/pin_image_recognition/__init__.py +3 -3
- dodal/devices/oav/pin_image_recognition/utils.py +1 -1
- dodal/devices/oav/snapshots/snapshot.py +1 -1
- dodal/devices/oav/snapshots/snapshot_image_processing.py +12 -12
- dodal/devices/oav/snapshots/snapshot_with_grid.py +1 -1
- dodal/devices/oav/utils.py +2 -2
- dodal/devices/pgm.py +3 -3
- dodal/devices/robot.py +5 -5
- dodal/devices/tetramm.py +9 -5
- dodal/devices/thawer.py +0 -4
- dodal/devices/v2f.py +2 -2
- dodal/devices/zebra/zebra_constants_mapping.py +2 -2
- dodal/devices/zocalo/__init__.py +4 -4
- dodal/devices/zocalo/zocalo_results.py +4 -4
- dodal/log.py +9 -9
- dodal/plan_stubs/motor_utils.py +4 -4
- dodal/plans/configure_arm_trigger_and_disarm_detector.py +29 -7
- dodal/plans/save_panda.py +7 -7
- dodal/plans/verify_undulator_gap.py +2 -2
- dls_dodal-1.61.0.dist-info/entry_points.txt +0 -3
- dodal/beamlines/i10-1.py +0 -25
- dodal/devices/i09/dcm.py +0 -26
- {dls_dodal-1.61.0.dist-info → dls_dodal-1.63.0.dist-info}/WHEEL +0 -0
- {dls_dodal-1.61.0.dist-info → dls_dodal-1.63.0.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.61.0.dist-info → dls_dodal-1.63.0.dist-info}/top_level.txt +0 -0
- /dodal/devices/areadetector/plugins/{CAM.py → cam.py} +0 -0
- /dodal/devices/areadetector/plugins/{MJPG.py → mjpg.py} +0 -0
- /dodal/devices/i18/{KBMirror.py → kb_mirror.py} +0 -0
- /dodal/devices/i19/{blueapi_device.py → access_controlled/blueapi_device.py} +0 -0
- /dodal/devices/i19/{hutch_access.py → access_controlled/hutch_access.py} +0 -0
|
@@ -5,19 +5,23 @@ from math import isclose
|
|
|
5
5
|
from typing import Generic, Protocol, TypeVar
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
|
-
from bluesky.protocols import Movable
|
|
8
|
+
from bluesky.protocols import Locatable, Location, Movable
|
|
9
9
|
from ophyd_async.core import (
|
|
10
10
|
AsyncStatus,
|
|
11
|
+
Reference,
|
|
11
12
|
SignalR,
|
|
13
|
+
SignalRW,
|
|
12
14
|
SignalW,
|
|
13
15
|
StandardReadable,
|
|
14
16
|
StandardReadableFormat,
|
|
15
17
|
StrictEnum,
|
|
16
18
|
derived_signal_rw,
|
|
17
19
|
soft_signal_r_and_setter,
|
|
20
|
+
soft_signal_rw,
|
|
18
21
|
wait_for_value,
|
|
19
22
|
)
|
|
20
23
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
|
|
24
|
+
from ophyd_async.epics.motor import Motor
|
|
21
25
|
|
|
22
26
|
from dodal.log import LOGGER
|
|
23
27
|
|
|
@@ -40,12 +44,8 @@ class Apple2PhasesVal:
|
|
|
40
44
|
|
|
41
45
|
|
|
42
46
|
@dataclass
|
|
43
|
-
class Apple2Val:
|
|
47
|
+
class Apple2Val(Apple2PhasesVal):
|
|
44
48
|
gap: str
|
|
45
|
-
top_outer: str
|
|
46
|
-
top_inner: str
|
|
47
|
-
btm_inner: str
|
|
48
|
-
btm_outer: str
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
class Pol(StrictEnum):
|
|
@@ -185,24 +185,24 @@ class UndulatorPhaseMotor(StandardReadable):
|
|
|
185
185
|
name : str
|
|
186
186
|
Name of the Id phase device
|
|
187
187
|
"""
|
|
188
|
-
|
|
189
|
-
self.user_setpoint = epics_signal_w(str,
|
|
190
|
-
self.user_setpoint_readback = epics_signal_r(float,
|
|
191
|
-
|
|
188
|
+
full_pv = f"{prefix}BL{infix}"
|
|
189
|
+
self.user_setpoint = epics_signal_w(str, full_pv + "SET")
|
|
190
|
+
self.user_setpoint_readback = epics_signal_r(float, full_pv + "DMD")
|
|
191
|
+
full_pv = full_pv + "MTR"
|
|
192
192
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
193
|
-
self.user_readback = epics_signal_r(float,
|
|
193
|
+
self.user_readback = epics_signal_r(float, full_pv + ".RBV")
|
|
194
194
|
|
|
195
195
|
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
196
|
-
self.motor_egu = epics_signal_r(str,
|
|
197
|
-
self.velocity = epics_signal_rw(float,
|
|
198
|
-
|
|
199
|
-
self.max_velocity = epics_signal_r(float,
|
|
200
|
-
self.acceleration_time = epics_signal_rw(float,
|
|
201
|
-
self.precision = epics_signal_r(int,
|
|
202
|
-
self.deadband = epics_signal_r(float,
|
|
203
|
-
self.motor_done_move = epics_signal_r(int,
|
|
204
|
-
self.low_limit_travel = epics_signal_rw(float,
|
|
205
|
-
self.high_limit_travel = epics_signal_rw(float,
|
|
196
|
+
self.motor_egu = epics_signal_r(str, full_pv + ".EGU")
|
|
197
|
+
self.velocity = epics_signal_rw(float, full_pv + ".VELO")
|
|
198
|
+
|
|
199
|
+
self.max_velocity = epics_signal_r(float, full_pv + ".VMAX")
|
|
200
|
+
self.acceleration_time = epics_signal_rw(float, full_pv + ".ACCL")
|
|
201
|
+
self.precision = epics_signal_r(int, full_pv + ".PREC")
|
|
202
|
+
self.deadband = epics_signal_r(float, full_pv + ".RDBD")
|
|
203
|
+
self.motor_done_move = epics_signal_r(int, full_pv + ".DMOV")
|
|
204
|
+
self.low_limit_travel = epics_signal_rw(float, full_pv + ".LLM")
|
|
205
|
+
self.high_limit_travel = epics_signal_rw(float, full_pv + ".HLM")
|
|
206
206
|
super().__init__(name=name)
|
|
207
207
|
|
|
208
208
|
|
|
@@ -301,7 +301,7 @@ class UndulatorJawPhase(SafeUndulatorMover[float]):
|
|
|
301
301
|
)
|
|
302
302
|
|
|
303
303
|
|
|
304
|
-
class
|
|
304
|
+
class Apple2(StandardReadable, Movable):
|
|
305
305
|
"""
|
|
306
306
|
Device representing the combined motor controls for an Apple2 undulator.
|
|
307
307
|
|
|
@@ -334,8 +334,7 @@ class Apple2Motors(StandardReadable, Movable):
|
|
|
334
334
|
async def set(self, id_motor_values: Apple2Val) -> None:
|
|
335
335
|
"""
|
|
336
336
|
Check ID is in a movable state and set all the demand value before moving them
|
|
337
|
-
all at the same time.
|
|
338
|
-
class, if the ID motors has to move in a specific order.
|
|
337
|
+
all at the same time.
|
|
339
338
|
"""
|
|
340
339
|
|
|
341
340
|
# Only need to check gap as the phase motors share both fault and gate with gap.
|
|
@@ -366,80 +365,76 @@ class EnergyMotorConvertor(Protocol):
|
|
|
366
365
|
...
|
|
367
366
|
|
|
368
367
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
Apple2 Undulator Device
|
|
368
|
+
Apple2Type = TypeVar("Apple2Type", bound="Apple2")
|
|
369
|
+
|
|
372
370
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
movement of magnet banks to produce X-rays with various polarisations and energies.
|
|
371
|
+
class Apple2Controller(abc.ABC, StandardReadable, Generic[Apple2Type]):
|
|
372
|
+
"""
|
|
376
373
|
|
|
377
|
-
|
|
378
|
-
abstracting hardware interactions and providing a high-level interface for beamline operations.
|
|
374
|
+
Abstract base class for controlling an Apple2 undulator device.
|
|
379
375
|
|
|
380
|
-
|
|
381
|
-
|
|
376
|
+
This class manages the undulator's gap and phase motors, and provides an interface
|
|
377
|
+
for controlling polarisation and energy settings. It exposes derived signals for
|
|
378
|
+
energy and polarisation, and handles conversion between energy/polarisation and
|
|
379
|
+
motor positions via a user-supplied conversion callable.
|
|
382
380
|
|
|
383
381
|
Attributes
|
|
384
382
|
----------
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
energy :
|
|
388
|
-
|
|
383
|
+
apple2 : Reference[Apple2Type]
|
|
384
|
+
Reference to the Apple2 device containing gap and phase motors.
|
|
385
|
+
energy : derived_signal_rw
|
|
386
|
+
Derived signal for moving and reading back energy.
|
|
389
387
|
polarisation_setpoint : SignalR
|
|
390
|
-
|
|
391
|
-
polarisation :
|
|
392
|
-
|
|
393
|
-
lookup_tables : dict
|
|
394
|
-
A dictionary storing lookup tables for gap and phase motor positions, used for energy and polarisation conversion.
|
|
388
|
+
Soft signal for the polarisation setpoint.
|
|
389
|
+
polarisation : derived_signal_rw
|
|
390
|
+
Hardware-backed signal for polarisation readback and control.
|
|
395
391
|
energy_to_motor : EnergyMotorConvertor
|
|
396
|
-
|
|
392
|
+
Callable that converts energy and polarisation to motor positions.
|
|
397
393
|
|
|
398
394
|
Abstract Methods
|
|
399
395
|
----------------
|
|
400
|
-
|
|
396
|
+
_set_motors_from_energy(value: float) -> None
|
|
401
397
|
Abstract method to set motor positions for a given energy and polarisation.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
-------
|
|
405
|
-
determine_phase_from_hardware(...) -> tuple[Pol, float]
|
|
406
|
-
Determines the polarisation and phase value based on motor positions.
|
|
398
|
+
energy_to_motor : EnergyMotorConvertor
|
|
399
|
+
A callable that converts energy and polarisation to motor positions.
|
|
407
400
|
|
|
408
401
|
Notes
|
|
409
402
|
-----
|
|
410
|
-
-
|
|
411
|
-
-
|
|
403
|
+
- Subclasses must implement `_set_motors_from_energy` for beamline-specific logic.
|
|
404
|
+
- LH3 polarisation is indistinguishable from LH in hardware; special handling is provided.
|
|
405
|
+
- Supports multiple polarisation modes, including linear horizontal (LH), linear vertical (LV),
|
|
412
406
|
positive circular (PC), negative circular (NC), and linear arbitrary (LA).
|
|
413
407
|
|
|
414
|
-
For more detail see
|
|
415
|
-
`UML </_images/apple2_design.png>`__ for detail.
|
|
416
|
-
|
|
417
|
-
.. figure:: /explanations/umls/apple2_design.png
|
|
418
|
-
|
|
419
408
|
"""
|
|
420
409
|
|
|
421
410
|
def __init__(
|
|
422
411
|
self,
|
|
423
|
-
|
|
424
|
-
|
|
412
|
+
apple2: Apple2Type,
|
|
413
|
+
energy_to_motor_converter: EnergyMotorConvertor,
|
|
425
414
|
name: str = "",
|
|
426
415
|
) -> None:
|
|
427
416
|
"""
|
|
428
417
|
|
|
429
418
|
Parameters
|
|
430
419
|
----------
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
420
|
+
apple2: Apple2
|
|
421
|
+
An Apple2 device.
|
|
422
|
+
name: str
|
|
423
|
+
Name of the device.
|
|
435
424
|
"""
|
|
425
|
+
self.energy_to_motor = energy_to_motor_converter
|
|
426
|
+
self.apple2 = Reference(apple2)
|
|
436
427
|
|
|
437
|
-
|
|
438
|
-
self.
|
|
428
|
+
# Store the set energy for readback.
|
|
429
|
+
self._energy, self._energy_set = soft_signal_r_and_setter(
|
|
430
|
+
float, initial_value=None, units="eV"
|
|
431
|
+
)
|
|
439
432
|
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
433
|
+
self.energy = derived_signal_rw(
|
|
434
|
+
raw_to_derived=self._read_energy,
|
|
435
|
+
set_derived=self._set_energy,
|
|
436
|
+
energy=self._energy,
|
|
437
|
+
derived_units="eV",
|
|
443
438
|
)
|
|
444
439
|
|
|
445
440
|
# Store the polarisation for setpoint. And provide readback for LH3.
|
|
@@ -447,61 +442,65 @@ class Apple2(abc.ABC, StandardReadable, Movable):
|
|
|
447
442
|
self.polarisation_setpoint, self._polarisation_setpoint_set = (
|
|
448
443
|
soft_signal_r_and_setter(Pol)
|
|
449
444
|
)
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
445
|
+
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
446
|
+
# Hardware backed read/write for polarisation.
|
|
447
|
+
self.polarisation = derived_signal_rw(
|
|
448
|
+
raw_to_derived=self._read_pol,
|
|
449
|
+
set_derived=self._set_pol,
|
|
450
|
+
pol=self.polarisation_setpoint,
|
|
451
|
+
top_outer=self.apple2().phase.top_outer.user_readback,
|
|
452
|
+
top_inner=self.apple2().phase.top_inner.user_readback,
|
|
453
|
+
btm_inner=self.apple2().phase.btm_inner.user_readback,
|
|
454
|
+
btm_outer=self.apple2().phase.btm_outer.user_readback,
|
|
455
|
+
gap=self.apple2().gap.user_readback,
|
|
456
|
+
)
|
|
462
457
|
super().__init__(name)
|
|
463
458
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
async def _set_pol(
|
|
471
|
-
self,
|
|
472
|
-
value: Pol,
|
|
473
|
-
) -> None:
|
|
474
|
-
# This changes the pol setpoint and then changes polarisation via set energy.
|
|
475
|
-
self._set_pol_setpoint(value)
|
|
476
|
-
await self.set(await self.energy.get_value())
|
|
477
|
-
|
|
478
|
-
@AsyncStatus.wrap
|
|
479
|
-
async def set(self, value: float) -> None:
|
|
459
|
+
@abc.abstractmethod
|
|
460
|
+
async def _set_motors_from_energy(self, value: float) -> None:
|
|
461
|
+
"""
|
|
462
|
+
This method should be implemented by the beamline specific ID class as the
|
|
463
|
+
motor positions will be different for each beamline depending on the
|
|
464
|
+
undulator design and the lookup table used.
|
|
480
465
|
"""
|
|
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
466
|
|
|
467
|
+
async def _set_energy(self, energy: float) -> None:
|
|
468
|
+
await self._set_motors_from_energy(energy)
|
|
469
|
+
self._energy_set(energy)
|
|
485
470
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
RE(scan([detector], id,600,700,100)) # This will scan the ID from 600 to 700 eV in 100 steps.
|
|
490
|
-
"""
|
|
491
|
-
await self._set(value)
|
|
492
|
-
self._set_energy_rbv(value) # Update energy after move for readback.
|
|
493
|
-
LOGGER.info(f"Energy set to {value} eV successfully.")
|
|
471
|
+
def _read_energy(self, energy: float) -> float:
|
|
472
|
+
"""Readback for energy is just the set value."""
|
|
473
|
+
return energy
|
|
494
474
|
|
|
495
|
-
|
|
496
|
-
async def _set(self, value: float) -> None:
|
|
475
|
+
async def _check_and_get_pol_setpoint(self) -> Pol:
|
|
497
476
|
"""
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
undulator design and the lookup table used. The set method can be
|
|
501
|
-
used to set the motor positions for the given energy and polarisation
|
|
502
|
-
provided that all motors can be moved at the same time.
|
|
477
|
+
Check the polarisation setpoint and if it is NONE try to read it from
|
|
478
|
+
hardware.
|
|
503
479
|
"""
|
|
504
480
|
|
|
481
|
+
pol = await self.polarisation_setpoint.get_value()
|
|
482
|
+
|
|
483
|
+
if pol == Pol.NONE:
|
|
484
|
+
LOGGER.warning(
|
|
485
|
+
"Found no setpoint for polarisation. Attempting to"
|
|
486
|
+
" determine polarisation from hardware..."
|
|
487
|
+
)
|
|
488
|
+
pol = await self.polarisation.get_value()
|
|
489
|
+
if pol == Pol.NONE:
|
|
490
|
+
raise ValueError(
|
|
491
|
+
f"Polarisation cannot be determined from hardware for {self.name}"
|
|
492
|
+
)
|
|
493
|
+
self._polarisation_setpoint_set(pol)
|
|
494
|
+
return pol
|
|
495
|
+
|
|
496
|
+
async def _set_pol(
|
|
497
|
+
self,
|
|
498
|
+
value: Pol,
|
|
499
|
+
) -> None:
|
|
500
|
+
# This changes the pol setpoint and then changes polarisation via set energy.
|
|
501
|
+
self._polarisation_setpoint_set(value)
|
|
502
|
+
await self.energy.set(await self.energy.get_value())
|
|
503
|
+
|
|
505
504
|
def _read_pol(
|
|
506
505
|
self,
|
|
507
506
|
pol: Pol,
|
|
@@ -605,3 +604,101 @@ class Apple2(abc.ABC, StandardReadable, Movable):
|
|
|
605
604
|
|
|
606
605
|
LOGGER.warning("Unable to determine polarisation. Defaulting to NONE.")
|
|
607
606
|
return Pol.NONE, 0.0
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
class InsertionDeviceEnergyBase(abc.ABC, StandardReadable, Movable):
|
|
610
|
+
"""Base class for ID energy movable device."""
|
|
611
|
+
|
|
612
|
+
def __init__(self, name: str = "") -> None:
|
|
613
|
+
self.energy: Reference[SignalRW[float]]
|
|
614
|
+
super().__init__(name=name)
|
|
615
|
+
|
|
616
|
+
@abc.abstractmethod
|
|
617
|
+
@AsyncStatus.wrap
|
|
618
|
+
async def set(self, energy: float) -> None: ...
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
class BeamEnergy(StandardReadable, Movable[float]):
|
|
622
|
+
"""
|
|
623
|
+
Compound device to set both ID and energy motor at the same time with an option to add an offset.
|
|
624
|
+
"""
|
|
625
|
+
|
|
626
|
+
def __init__(
|
|
627
|
+
self, id_energy: InsertionDeviceEnergyBase, mono: Motor, name: str = ""
|
|
628
|
+
) -> None:
|
|
629
|
+
"""
|
|
630
|
+
Parameters
|
|
631
|
+
----------
|
|
632
|
+
|
|
633
|
+
id_energy: InsertionDeviceEnergy
|
|
634
|
+
An InsertionDeviceEnergy device.
|
|
635
|
+
mono: Motor
|
|
636
|
+
A Motor(energy) device.
|
|
637
|
+
name:
|
|
638
|
+
New device name.
|
|
639
|
+
"""
|
|
640
|
+
super().__init__(name=name)
|
|
641
|
+
self._id_energy = Reference(id_energy)
|
|
642
|
+
self._mono_energy = Reference(mono)
|
|
643
|
+
|
|
644
|
+
self.add_readables(
|
|
645
|
+
[
|
|
646
|
+
self._id_energy().energy(),
|
|
647
|
+
self._mono_energy().user_readback,
|
|
648
|
+
],
|
|
649
|
+
StandardReadableFormat.HINTED_SIGNAL,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
653
|
+
self.id_energy_offset = soft_signal_rw(float, initial_value=0)
|
|
654
|
+
|
|
655
|
+
@AsyncStatus.wrap
|
|
656
|
+
async def set(self, energy: float) -> None:
|
|
657
|
+
LOGGER.info(f"Moving f{self.name} energy to {energy}.")
|
|
658
|
+
await asyncio.gather(
|
|
659
|
+
self._id_energy().set(
|
|
660
|
+
energy=energy + await self.id_energy_offset.get_value()
|
|
661
|
+
),
|
|
662
|
+
self._mono_energy().set(energy),
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
class InsertionDeviceEnergy(InsertionDeviceEnergyBase):
|
|
667
|
+
"""Apple2 ID energy movable device."""
|
|
668
|
+
|
|
669
|
+
def __init__(self, id_controller: Apple2Controller, name: str = "") -> None:
|
|
670
|
+
self.energy = Reference(id_controller.energy)
|
|
671
|
+
super().__init__(name=name)
|
|
672
|
+
|
|
673
|
+
self.add_readables(
|
|
674
|
+
[
|
|
675
|
+
self.energy(),
|
|
676
|
+
],
|
|
677
|
+
StandardReadableFormat.HINTED_SIGNAL,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
@AsyncStatus.wrap
|
|
681
|
+
async def set(self, energy: float) -> None:
|
|
682
|
+
await self.energy().set(energy)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
class InsertionDevicePolarisation(StandardReadable, Locatable[Pol]):
|
|
686
|
+
"""Apple2 ID polarisation movable device."""
|
|
687
|
+
|
|
688
|
+
def __init__(self, id_controller: Apple2Controller, name: str = "") -> None:
|
|
689
|
+
self.polarisation = Reference(id_controller.polarisation)
|
|
690
|
+
self.polarisation_setpoint = Reference(id_controller.polarisation_setpoint)
|
|
691
|
+
super().__init__(name=name)
|
|
692
|
+
|
|
693
|
+
self.add_readables([self.polarisation()], StandardReadableFormat.HINTED_SIGNAL)
|
|
694
|
+
|
|
695
|
+
@AsyncStatus.wrap
|
|
696
|
+
async def set(self, pol: Pol) -> None:
|
|
697
|
+
await self.polarisation().set(pol)
|
|
698
|
+
|
|
699
|
+
async def locate(self) -> Location[Pol]:
|
|
700
|
+
"""Return the current polarisation"""
|
|
701
|
+
setpoint, readback = await asyncio.gather(
|
|
702
|
+
self.polarisation_setpoint().get_value(), self.polarisation().get_value()
|
|
703
|
+
)
|
|
704
|
+
return Location(setpoint=setpoint, readback=readback)
|
|
@@ -20,7 +20,7 @@ class P99FilterSelections(SubsetEnum):
|
|
|
20
20
|
USER = "User"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
class I02_1FilterOneSelections(SubsetEnum):
|
|
23
|
+
class I02_1FilterOneSelections(SubsetEnum): # noqa: N801
|
|
24
24
|
EMPTY = "Empty"
|
|
25
25
|
AL8 = "Al8"
|
|
26
26
|
AL15 = "Al15"
|
|
@@ -33,7 +33,7 @@ class I02_1FilterOneSelections(SubsetEnum):
|
|
|
33
33
|
TWO_TIMES_TI500 = "2xTi500"
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
class I02_1FilterTwoSelections(SubsetEnum):
|
|
36
|
+
class I02_1FilterTwoSelections(SubsetEnum): # noqa: N801
|
|
37
37
|
EMPTY = "Empty"
|
|
38
38
|
AL50 = "Al50"
|
|
39
39
|
AL100 = "Al100"
|
|
@@ -46,7 +46,7 @@ class I02_1FilterTwoSelections(SubsetEnum):
|
|
|
46
46
|
TWO_TIMES_TI500 = "2xTi500"
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
class I02_1FilterThreeSelections(SubsetEnum):
|
|
49
|
+
class I02_1FilterThreeSelections(SubsetEnum): # noqa: N801
|
|
50
50
|
EMPTY = "Empty"
|
|
51
51
|
AL15 = "Al15"
|
|
52
52
|
AL25 = "Al25"
|
|
@@ -59,7 +59,7 @@ class I02_1FilterThreeSelections(SubsetEnum):
|
|
|
59
59
|
TI200 = "Ti200"
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
class I02_1FilterFourSelections(SubsetEnum):
|
|
62
|
+
class I02_1FilterFourSelections(SubsetEnum): # noqa: N801
|
|
63
63
|
EMPTY = "Empty"
|
|
64
64
|
AL15 = "Al15"
|
|
65
65
|
AL25 = "Al25"
|
|
@@ -72,7 +72,7 @@ class I02_1FilterFourSelections(SubsetEnum):
|
|
|
72
72
|
TI500 = "Ti500"
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
class
|
|
75
|
+
class I24FilterOneSelections(SubsetEnum):
|
|
76
76
|
EMPTY = "Empty"
|
|
77
77
|
AL12_5 = "Al12.5"
|
|
78
78
|
AL25 = "Al25"
|
|
@@ -85,7 +85,7 @@ class I24_FilterOneSelections(SubsetEnum):
|
|
|
85
85
|
TI500 = "Ti500"
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
class
|
|
88
|
+
class I24FilterTwoSelections(SubsetEnum):
|
|
89
89
|
EMPTY = "Empty"
|
|
90
90
|
AL100 = "Al100"
|
|
91
91
|
AL200 = "Al200"
|
dodal/devices/common_dcm.py
CHANGED
|
@@ -2,6 +2,7 @@ from typing import Generic, TypeVar
|
|
|
2
2
|
|
|
3
3
|
from ophyd_async.core import (
|
|
4
4
|
StandardReadable,
|
|
5
|
+
derived_signal_r,
|
|
5
6
|
)
|
|
6
7
|
from ophyd_async.epics.core import epics_signal_r
|
|
7
8
|
from ophyd_async.epics.motor import Motor
|
|
@@ -31,42 +32,39 @@ Xtal_1 = TypeVar("Xtal_1", bound=StationaryCrystal)
|
|
|
31
32
|
Xtal_2 = TypeVar("Xtal_2", bound=StationaryCrystal)
|
|
32
33
|
|
|
33
34
|
|
|
34
|
-
class
|
|
35
|
+
class DoubleCrystalMonochromatorBase(StandardReadable, Generic[Xtal_1, Xtal_2]):
|
|
35
36
|
"""
|
|
36
|
-
|
|
37
|
+
Base device for the double crystal monochromator (DCM), used to select the energy of the beam.
|
|
37
38
|
|
|
38
39
|
Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
|
|
39
40
|
each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
|
|
40
41
|
This base device accounts for all combinations of this.
|
|
41
42
|
|
|
42
|
-
This device should act as a parent for beamline-specific DCM's
|
|
43
|
+
This device should act as a parent for beamline-specific DCM's which do not match the standard EPICS interface, it provides
|
|
44
|
+
only energy and the crystal configuration. Most beamlines should use DoubleCrystalMonochromator instead
|
|
43
45
|
|
|
44
46
|
Bluesky plans using DCM's should be typed to specify which types of crystals are required. For example, a plan
|
|
45
|
-
which only requires one crystal which can roll should be typed
|
|
47
|
+
which only requires one crystal which can roll should be typed
|
|
48
|
+
'def my_plan(dcm: DoubleCrystalMonochromatorBase[RollCrystal, StationaryCrystal])`
|
|
46
49
|
"""
|
|
47
50
|
|
|
48
51
|
def __init__(
|
|
49
52
|
self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
|
|
50
53
|
) -> None:
|
|
51
54
|
with self.add_children_as_readables():
|
|
52
|
-
# Virtual motor PV's which set the physical motors so that the DCM produces requested
|
|
53
|
-
# wavelength/energy
|
|
55
|
+
# Virtual motor PV's which set the physical motors so that the DCM produces requested energy
|
|
54
56
|
self.energy_in_kev = Motor(prefix + "ENERGY")
|
|
55
|
-
self.
|
|
56
|
-
|
|
57
|
-
# Real motors
|
|
58
|
-
self.bragg_in_degrees = Motor(prefix + "BRAGG")
|
|
59
|
-
# Offset ensures that the beam exits the DCM at the same point, regardless of energy.
|
|
60
|
-
self.offset_in_mm = Motor(prefix + "OFFSET")
|
|
61
|
-
|
|
62
|
-
self.crystal_metadata_d_spacing_a = epics_signal_r(
|
|
63
|
-
float, prefix + "DSPACING:RBV"
|
|
57
|
+
self.energy_in_ev = derived_signal_r(
|
|
58
|
+
self._convert_keV_to_eV, energy_signal=self.energy_in_kev.user_readback
|
|
64
59
|
)
|
|
65
60
|
|
|
66
61
|
self._make_crystals(prefix, xtal_1, xtal_2)
|
|
67
62
|
|
|
68
63
|
super().__init__(name)
|
|
69
64
|
|
|
65
|
+
def _convert_keV_to_eV(self, energy_signal: float) -> float: # noqa: N802
|
|
66
|
+
return energy_signal * 1000
|
|
67
|
+
|
|
70
68
|
# Prefix convention is different depending on whether there are one or two controllable crystals
|
|
71
69
|
def _make_crystals(self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2]):
|
|
72
70
|
if StationaryCrystal not in [xtal_1, xtal_2]:
|
|
@@ -75,3 +73,52 @@ class BaseDCM(StandardReadable, Generic[Xtal_1, Xtal_2]):
|
|
|
75
73
|
else:
|
|
76
74
|
self.xtal_1 = xtal_1(prefix)
|
|
77
75
|
self.xtal_2 = xtal_2(prefix)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DoubleCrystalMonochromator(
|
|
79
|
+
DoubleCrystalMonochromatorBase, Generic[Xtal_1, Xtal_2]
|
|
80
|
+
):
|
|
81
|
+
"""
|
|
82
|
+
Common device for the double crystal monochromator (DCM), used to select the energy of the beam.
|
|
83
|
+
|
|
84
|
+
Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
|
|
85
|
+
each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
|
|
86
|
+
This base device accounts for all combinations of this.
|
|
87
|
+
|
|
88
|
+
This device should act as a parent for beamline-specific DCM's, in which any other missing signals can be added.
|
|
89
|
+
|
|
90
|
+
Bluesky plans using DCM's should be typed to specify which types of crystals are required. For example, a plan which only
|
|
91
|
+
requires one crystal which can roll should be typed 'def my_plan(dcm: DoubleCrystalMonochromator[RollCrystal, StationaryCrystal])`
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
|
|
96
|
+
) -> None:
|
|
97
|
+
super().__init__(prefix, xtal_1, xtal_2, name)
|
|
98
|
+
with self.add_children_as_readables():
|
|
99
|
+
# Virtual motor PV's which set the physical motors so that the DCM produces requested
|
|
100
|
+
# wavelength
|
|
101
|
+
self.wavelength_in_a = Motor(prefix + "WAVELENGTH")
|
|
102
|
+
|
|
103
|
+
# Real motors
|
|
104
|
+
self.bragg_in_degrees = Motor(prefix + "BRAGG")
|
|
105
|
+
# Offset ensures that the beam exits the DCM at the same point, regardless of energy.
|
|
106
|
+
self.offset_in_mm = Motor(prefix + "OFFSET")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class DoubleCrystalMonochromatorWithDSpacing(
|
|
110
|
+
DoubleCrystalMonochromator, Generic[Xtal_1, Xtal_2]
|
|
111
|
+
):
|
|
112
|
+
"""
|
|
113
|
+
Adds crystal D-spacing metadata to the DoubleCrystalMonochromator class. This should be used in preference to the
|
|
114
|
+
DoubleCrystalMonochromator on beamlines which have a "DSPACING:RBV" pv on their DCM.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
|
|
119
|
+
) -> None:
|
|
120
|
+
super().__init__(prefix, xtal_1, xtal_2, name)
|
|
121
|
+
with self.add_children_as_readables():
|
|
122
|
+
self.crystal_metadata_d_spacing_a = epics_signal_r(
|
|
123
|
+
float, prefix + "DSPACING:RBV"
|
|
124
|
+
)
|
dodal/devices/controllers.py
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
from typing import TypeVar
|
|
2
2
|
|
|
3
|
-
from ophyd_async.epics.adcore import
|
|
4
|
-
ADBaseController,
|
|
5
|
-
ADBaseIO,
|
|
6
|
-
)
|
|
3
|
+
from ophyd_async.epics.adcore import ADBaseController, ADBaseIO, ADImageMode
|
|
7
4
|
|
|
8
5
|
ADBaseIOT = TypeVar("ADBaseIOT", bound=ADBaseIO)
|
|
9
6
|
|
|
@@ -13,8 +10,13 @@ class ConstantDeadTimeController(ADBaseController[ADBaseIOT]):
|
|
|
13
10
|
ADBaseController with a configured constant deadtime for a driver of type ADBaseIO.
|
|
14
11
|
"""
|
|
15
12
|
|
|
16
|
-
def __init__(
|
|
17
|
-
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
driver: ADBaseIOT,
|
|
16
|
+
deadtime: float,
|
|
17
|
+
image_mode: ADImageMode = ADImageMode.MULTIPLE,
|
|
18
|
+
):
|
|
19
|
+
super().__init__(driver, image_mode=image_mode)
|
|
18
20
|
self.deadtime = deadtime
|
|
19
21
|
|
|
20
22
|
def get_deadtime(self, exposure: float | None) -> float:
|
|
@@ -104,15 +104,15 @@ class FemtoDDPCA(CurrentAmp):
|
|
|
104
104
|
+ "\n Available gain:"
|
|
105
105
|
+ f" {[f'{c.value:.0e}' for c in self.gain_conversion_table]}"
|
|
106
106
|
)
|
|
107
|
-
|
|
108
|
-
LOGGER.info(f"{self.name} gain change to {
|
|
107
|
+
sensitivity_setting = self.gain_conversion_table(value).name
|
|
108
|
+
LOGGER.info(f"{self.name} gain change to {sensitivity_setting}:{value}")
|
|
109
109
|
|
|
110
110
|
await self.gain.set(
|
|
111
|
-
value=self.gain_table[
|
|
111
|
+
value=self.gain_table[sensitivity_setting],
|
|
112
112
|
timeout=self.timeout,
|
|
113
113
|
)
|
|
114
114
|
# wait for current amplifier's bandpass filter to settle.
|
|
115
|
-
await asyncio.sleep(self.raise_timetable[
|
|
115
|
+
await asyncio.sleep(self.raise_timetable[sensitivity_setting].value)
|
|
116
116
|
|
|
117
117
|
async def increase_gain(self, value: int = 1) -> None:
|
|
118
118
|
current_gain = int((await self.get_gain()).name.split("_")[-1])
|
|
@@ -79,7 +79,7 @@ class SR570FullGainTable(Enum):
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
class SR570GainToCurrentTable(float, Enum):
|
|
82
|
-
"""Conversion table for gain(
|
|
82
|
+
"""Conversion table for gain (sensitivity) to current"""
|
|
83
83
|
|
|
84
84
|
SEN_1 = 1e3
|
|
85
85
|
SEN_2 = 2e3
|
|
@@ -168,10 +168,10 @@ class SR570(CurrentAmp):
|
|
|
168
168
|
+ "\n Available gain:"
|
|
169
169
|
+ f" {[f'{c.value:.0e}' for c in self.gain_conversion_table]}"
|
|
170
170
|
)
|
|
171
|
-
|
|
171
|
+
sensitivity_setting = self.gain_conversion_table(value).name
|
|
172
172
|
LOGGER.info(f"{self.name} gain change to {value}")
|
|
173
173
|
|
|
174
|
-
coarse_gain, fine_gain = self.combined_table[
|
|
174
|
+
coarse_gain, fine_gain = self.combined_table[sensitivity_setting].value
|
|
175
175
|
await asyncio.gather(
|
|
176
176
|
self.fine_gain.set(value=fine_gain, timeout=self.timeout),
|
|
177
177
|
self.coarse_gain.set(value=coarse_gain, timeout=self.timeout),
|