dls-dodal 1.36.2__py3-none-any.whl → 1.37.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.36.2.dist-info → dls_dodal-1.37.0.dist-info}/METADATA +4 -4
- {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/RECORD +54 -38
- {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/i02_1.py +37 -0
- dodal/beamlines/i03.py +20 -3
- dodal/beamlines/i04.py +3 -3
- dodal/beamlines/i10.py +179 -7
- dodal/beamlines/i22.py +15 -0
- dodal/beamlines/i24.py +2 -2
- dodal/beamlines/p99.py +6 -2
- dodal/common/crystal_metadata.py +3 -3
- dodal/common/udc_directory_provider.py +3 -1
- dodal/devices/aperturescatterguard.py +3 -0
- dodal/devices/apple2_undulator.py +9 -9
- dodal/devices/{attenuator.py → attenuator/attenuator.py} +29 -1
- dodal/devices/attenuator/filter.py +11 -0
- dodal/devices/attenuator/filter_selections.py +72 -0
- dodal/devices/bimorph_mirror.py +151 -0
- dodal/devices/current_amplifiers/__init__.py +34 -0
- dodal/devices/current_amplifiers/current_amplifier.py +103 -0
- dodal/devices/current_amplifiers/current_amplifier_detector.py +109 -0
- dodal/devices/current_amplifiers/femto.py +143 -0
- dodal/devices/current_amplifiers/sr570.py +214 -0
- dodal/devices/current_amplifiers/struck_scaler_counter.py +79 -0
- dodal/devices/detector/det_dim_constants.py +15 -0
- dodal/devices/eiger_odin.py +3 -3
- dodal/devices/fast_grid_scan.py +8 -3
- dodal/devices/i03/beamstop.py +85 -0
- dodal/devices/i04/transfocator.py +67 -53
- dodal/devices/i10/i10_setting_data.py +3 -3
- dodal/devices/i10/mirrors.py +24 -0
- dodal/devices/i10/rasor/rasor_current_amp.py +72 -0
- dodal/devices/i10/rasor/rasor_motors.py +62 -0
- dodal/devices/i10/rasor/rasor_scaler_cards.py +12 -0
- dodal/devices/i10/slits.py +37 -0
- dodal/devices/i24/dual_backlight.py +1 -0
- dodal/devices/i24/focus_mirrors.py +12 -12
- dodal/devices/linkam3.py +2 -2
- dodal/devices/p99/sample_stage.py +2 -28
- dodal/devices/robot.py +2 -2
- dodal/devices/slits.py +29 -7
- dodal/devices/tetramm.py +16 -16
- dodal/devices/undulator_dcm.py +9 -11
- dodal/devices/util/test_utils.py +2 -2
- dodal/devices/xspress3/xspress3.py +3 -3
- dodal/devices/zebra.py +19 -14
- dodal/devices/zocalo/zocalo_interaction.py +2 -1
- dodal/devices/zocalo/zocalo_results.py +22 -2
- dodal/log.py +2 -2
- dodal/plans/wrapped.py +3 -3
- {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/top_level.txt +0 -0
dodal/beamlines/p99.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from dodal.common.beamlines.beamline_utils import device_factory, set_beamline
|
|
2
|
+
from dodal.devices.attenuator.filter import FilterMotor
|
|
3
|
+
from dodal.devices.attenuator.filter_selections import P99FilterSelections
|
|
2
4
|
from dodal.devices.motors import XYZPositioner
|
|
3
|
-
from dodal.devices.p99.sample_stage import
|
|
5
|
+
from dodal.devices.p99.sample_stage import SampleAngleStage
|
|
4
6
|
from dodal.log import set_beamline as set_log_beamline
|
|
5
7
|
from dodal.utils import BeamlinePrefix, get_beamline_name
|
|
6
8
|
|
|
@@ -17,7 +19,9 @@ def angle_stage() -> SampleAngleStage:
|
|
|
17
19
|
|
|
18
20
|
@device_factory()
|
|
19
21
|
def filter() -> FilterMotor:
|
|
20
|
-
return FilterMotor(
|
|
22
|
+
return FilterMotor(
|
|
23
|
+
f"{PREFIX.beamline_prefix}-MO-STAGE-02:MP:SELECT", P99FilterSelections
|
|
24
|
+
)
|
|
21
25
|
|
|
22
26
|
|
|
23
27
|
@device_factory()
|
dodal/common/crystal_metadata.py
CHANGED
|
@@ -55,7 +55,7 @@ def make_crystal_metadata_from_material(
|
|
|
55
55
|
d_spacing = d_spacing_param or CrystalMetadata.calculate_default_d_spacing(
|
|
56
56
|
material.value.lattice_parameter, reflection_plane
|
|
57
57
|
)
|
|
58
|
-
assert all(
|
|
59
|
-
|
|
60
|
-
)
|
|
58
|
+
assert all(isinstance(i, int) and i > 0 for i in reflection_plane), (
|
|
59
|
+
"Reflection plane indices must be positive integers"
|
|
60
|
+
)
|
|
61
61
|
return CrystalMetadata(usage, material.value.name, reflection_plane, d_spacing)
|
|
@@ -46,7 +46,9 @@ class PandASubpathProvider(UpdatingPathProvider):
|
|
|
46
46
|
self._filename_provider.suffix = suffix
|
|
47
47
|
|
|
48
48
|
def __call__(self, device_name: str | None = None) -> PathInfo:
|
|
49
|
-
assert self._output_directory,
|
|
49
|
+
assert self._output_directory, (
|
|
50
|
+
"Directory unknown for PandA to write into, update() needs to be called at least once"
|
|
51
|
+
)
|
|
50
52
|
return PathInfo(
|
|
51
53
|
directory_path=self._output_directory,
|
|
52
54
|
filename=self._filename_provider(device_name),
|
|
@@ -21,8 +21,8 @@ from dodal.log import LOGGER
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class UndulatorGateStatus(StrictEnum):
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
OPEN = "Open"
|
|
25
|
+
CLOSE = "Closed"
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
@dataclass
|
|
@@ -146,7 +146,7 @@ class UndulatorGap(StandardReadable, Movable):
|
|
|
146
146
|
timeout = await self._cal_timeout()
|
|
147
147
|
LOGGER.info(f"Moving {self.name} to {value} with timeout = {timeout}")
|
|
148
148
|
await self.set_move.set(value=1, timeout=timeout)
|
|
149
|
-
await wait_for_value(self.gate, UndulatorGateStatus.
|
|
149
|
+
await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
|
|
150
150
|
|
|
151
151
|
async def _cal_timeout(self) -> float:
|
|
152
152
|
vel = await self.velocity.get_value()
|
|
@@ -157,7 +157,7 @@ class UndulatorGap(StandardReadable, Movable):
|
|
|
157
157
|
async def check_id_status(self) -> None:
|
|
158
158
|
if await self.fault.get_value() != 0:
|
|
159
159
|
raise RuntimeError(f"{self.name} is in fault state")
|
|
160
|
-
if await self.gate.get_value() == UndulatorGateStatus.
|
|
160
|
+
if await self.gate.get_value() == UndulatorGateStatus.OPEN:
|
|
161
161
|
raise RuntimeError(f"{self.name} is already in motion.")
|
|
162
162
|
|
|
163
163
|
async def get_timeout(self) -> float:
|
|
@@ -251,7 +251,7 @@ class UndulatorPhaseAxes(StandardReadable, Movable):
|
|
|
251
251
|
)
|
|
252
252
|
timeout = await self._cal_timeout()
|
|
253
253
|
await self.set_move.set(value=1, timeout=timeout)
|
|
254
|
-
await wait_for_value(self.gate, UndulatorGateStatus.
|
|
254
|
+
await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
|
|
255
255
|
|
|
256
256
|
async def _cal_timeout(self) -> float:
|
|
257
257
|
"""
|
|
@@ -283,7 +283,7 @@ class UndulatorPhaseAxes(StandardReadable, Movable):
|
|
|
283
283
|
async def check_id_status(self) -> None:
|
|
284
284
|
if await self.fault.get_value() != 0:
|
|
285
285
|
raise RuntimeError(f"{self.name} is in fault state")
|
|
286
|
-
if await self.gate.get_value() == UndulatorGateStatus.
|
|
286
|
+
if await self.gate.get_value() == UndulatorGateStatus.OPEN:
|
|
287
287
|
raise RuntimeError(f"{self.name} is already in motion.")
|
|
288
288
|
|
|
289
289
|
async def get_timeout(self) -> float:
|
|
@@ -325,7 +325,7 @@ class UndulatorJawPhase(StandardReadable, Movable):
|
|
|
325
325
|
)
|
|
326
326
|
timeout = await self._cal_timeout()
|
|
327
327
|
await self.set_move.set(value=1, timeout=timeout)
|
|
328
|
-
await wait_for_value(self.gate, UndulatorGateStatus.
|
|
328
|
+
await wait_for_value(self.gate, UndulatorGateStatus.CLOSE, timeout=timeout)
|
|
329
329
|
|
|
330
330
|
async def _cal_timeout(self) -> float:
|
|
331
331
|
"""
|
|
@@ -345,7 +345,7 @@ class UndulatorJawPhase(StandardReadable, Movable):
|
|
|
345
345
|
async def check_id_status(self) -> None:
|
|
346
346
|
if await self.fault.get_value() != 0:
|
|
347
347
|
raise RuntimeError(f"{self.name} is in fault state")
|
|
348
|
-
if await self.gate.get_value() == UndulatorGateStatus.
|
|
348
|
+
if await self.gate.get_value() == UndulatorGateStatus.OPEN:
|
|
349
349
|
raise RuntimeError(f"{self.name} is already in motion.")
|
|
350
350
|
|
|
351
351
|
async def get_timeout(self) -> float:
|
|
@@ -458,7 +458,7 @@ class Apple2(StandardReadable, Movable):
|
|
|
458
458
|
self.phase().set_move.set(value=1, timeout=timeout),
|
|
459
459
|
)
|
|
460
460
|
await wait_for_value(
|
|
461
|
-
self.gap().gate, UndulatorGateStatus.
|
|
461
|
+
self.gap().gate, UndulatorGateStatus.CLOSE, timeout=timeout
|
|
462
462
|
)
|
|
463
463
|
self._energy_set(energy) # Update energy for after move for readback.
|
|
464
464
|
|
|
@@ -7,10 +7,12 @@ from ophyd_async.core import (
|
|
|
7
7
|
DeviceVector,
|
|
8
8
|
SignalR,
|
|
9
9
|
StandardReadable,
|
|
10
|
+
SubsetEnum,
|
|
10
11
|
wait_for_value,
|
|
11
12
|
)
|
|
12
13
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
13
14
|
|
|
15
|
+
from dodal.devices.attenuator.filter import FilterMotor
|
|
14
16
|
from dodal.log import LOGGER
|
|
15
17
|
|
|
16
18
|
|
|
@@ -27,8 +29,9 @@ class ReadOnlyAttenuator(StandardReadable):
|
|
|
27
29
|
super().__init__(name)
|
|
28
30
|
|
|
29
31
|
|
|
30
|
-
class
|
|
32
|
+
class BinaryFilterAttenuator(ReadOnlyAttenuator, Movable):
|
|
31
33
|
"""The attenuator will insert filters into the beam to reduce its transmission.
|
|
34
|
+
In this attenuator, each filter can be in one of two states: IN or OUT
|
|
32
35
|
|
|
33
36
|
This device should be set with:
|
|
34
37
|
yield from bps.set(attenuator, desired_transmission)
|
|
@@ -83,3 +86,28 @@ class Attenuator(ReadOnlyAttenuator, Movable):
|
|
|
83
86
|
for i in range(16)
|
|
84
87
|
]
|
|
85
88
|
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class EnumFilterAttenuator(ReadOnlyAttenuator):
|
|
92
|
+
"""The attenuator will insert filters into the beam to reduce its transmission.
|
|
93
|
+
|
|
94
|
+
This device is currently working, but feature incomplete. See https://github.com/DiamondLightSource/dodal/issues/972
|
|
95
|
+
|
|
96
|
+
In this attenuator, the state of a filter corresponds to the selected material,
|
|
97
|
+
e.g Ag50, in contrast to being either 'IN' or 'OUT'; see BinaryFilterAttenuator.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
prefix: str,
|
|
103
|
+
filter_selection: tuple[type[SubsetEnum], ...],
|
|
104
|
+
name: str = "",
|
|
105
|
+
):
|
|
106
|
+
with self.add_children_as_readables():
|
|
107
|
+
self.filters: DeviceVector[FilterMotor] = DeviceVector(
|
|
108
|
+
{
|
|
109
|
+
index: FilterMotor(f"{prefix}MP{index + 1}:", filter, name)
|
|
110
|
+
for index, filter in enumerate(filter_selection)
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
super().__init__(prefix, name=name)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from ophyd_async.core import StandardReadable, SubsetEnum
|
|
2
|
+
from ophyd_async.epics.core import epics_signal_rw
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FilterMotor(StandardReadable):
|
|
6
|
+
def __init__(
|
|
7
|
+
self, prefix: str, filter_selections: type[SubsetEnum], name: str = ""
|
|
8
|
+
):
|
|
9
|
+
with self.add_children_as_readables():
|
|
10
|
+
self.user_setpoint = epics_signal_rw(filter_selections, f"{prefix}SELECT")
|
|
11
|
+
super().__init__(name=name)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from ophyd_async.core import SubsetEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class P99FilterSelections(SubsetEnum):
|
|
5
|
+
EMPTY = "Empty"
|
|
6
|
+
MN5UM = "Mn 5um"
|
|
7
|
+
FE = "Fe (empty)"
|
|
8
|
+
CO5UM = "Co 5um"
|
|
9
|
+
NI5UM = "Ni 5um"
|
|
10
|
+
CU5UM = "Cu 5um"
|
|
11
|
+
ZN5UM = "Zn 5um"
|
|
12
|
+
ZR = "Zr (empty)"
|
|
13
|
+
MO = "Mo (empty)"
|
|
14
|
+
RH = "Rh (empty)"
|
|
15
|
+
PD = "Pd (empty)"
|
|
16
|
+
AG = "Ag (empty)"
|
|
17
|
+
CD25UM = "Cd 25um"
|
|
18
|
+
W = "W (empty)"
|
|
19
|
+
PT = "Pt (empty)"
|
|
20
|
+
USER = "User"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class I02_1FilterOneSelections(SubsetEnum):
|
|
24
|
+
EMPTY = "Empty"
|
|
25
|
+
AL8 = "Al8"
|
|
26
|
+
AL15 = "Al15"
|
|
27
|
+
AL25 = "Al25"
|
|
28
|
+
AL1000 = "Al1000"
|
|
29
|
+
TI50 = "Ti50"
|
|
30
|
+
TI100 = "Ti100"
|
|
31
|
+
TI200 = "Ti200"
|
|
32
|
+
TI400 = "Ti400"
|
|
33
|
+
TWO_TIMES_TI500 = "2xTi500"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class I02_1FilterTwoSelections(SubsetEnum):
|
|
37
|
+
EMPTY = "Empty"
|
|
38
|
+
AL50 = "Al50"
|
|
39
|
+
AL100 = "Al100"
|
|
40
|
+
AL125 = "Al125"
|
|
41
|
+
AL250 = "Al250"
|
|
42
|
+
AL500 = "Al500"
|
|
43
|
+
AL1000 = "Al1000"
|
|
44
|
+
TI50 = "Ti50"
|
|
45
|
+
TI100 = "Ti100"
|
|
46
|
+
TWO_TIMES_TI500 = "2xTi500"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class I02_1FilterThreeSelections(SubsetEnum):
|
|
50
|
+
EMPTY = "Empty"
|
|
51
|
+
AL15 = "Al15"
|
|
52
|
+
AL25 = "Al25"
|
|
53
|
+
AL50 = "Al50"
|
|
54
|
+
AL100 = "Al100"
|
|
55
|
+
AL250 = "Al250"
|
|
56
|
+
AL1000 = "Al1000"
|
|
57
|
+
TI50 = "Ti50"
|
|
58
|
+
TI100 = "Ti100"
|
|
59
|
+
TI200 = "Ti200"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class I02_1FilterFourSelections(SubsetEnum):
|
|
63
|
+
EMPTY = "Empty"
|
|
64
|
+
AL15 = "Al15"
|
|
65
|
+
AL25 = "Al25"
|
|
66
|
+
AL50 = "Al50"
|
|
67
|
+
AL100 = "Al100"
|
|
68
|
+
AL250 = "Al250"
|
|
69
|
+
AL500 = "Al500"
|
|
70
|
+
TI300 = "Ti300"
|
|
71
|
+
TI400 = "Ti400"
|
|
72
|
+
TI500 = "Ti500"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from typing import Annotated as A
|
|
4
|
+
|
|
5
|
+
from bluesky.protocols import Movable
|
|
6
|
+
from ophyd_async.core import (
|
|
7
|
+
DEFAULT_TIMEOUT,
|
|
8
|
+
AsyncStatus,
|
|
9
|
+
DeviceVector,
|
|
10
|
+
SignalR,
|
|
11
|
+
SignalRW,
|
|
12
|
+
SignalW,
|
|
13
|
+
StandardReadable,
|
|
14
|
+
StrictEnum,
|
|
15
|
+
wait_for_value,
|
|
16
|
+
)
|
|
17
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
18
|
+
from ophyd_async.epics.core import (
|
|
19
|
+
EpicsDevice,
|
|
20
|
+
PvSuffix,
|
|
21
|
+
epics_signal_r,
|
|
22
|
+
epics_signal_w,
|
|
23
|
+
epics_signal_x,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BimorphMirrorOnOff(StrictEnum):
|
|
28
|
+
ON = "ON"
|
|
29
|
+
OFF = "OFF"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BimorphMirrorMode(StrictEnum):
|
|
33
|
+
HI = "HI"
|
|
34
|
+
NORMAL = "NORMAL"
|
|
35
|
+
FAST = "FAST"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BimorphMirrorStatus(StrictEnum):
|
|
39
|
+
IDLE = "Idle"
|
|
40
|
+
BUSY = "Busy"
|
|
41
|
+
ERROR = "Error"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BimorphMirrorChannel(StandardReadable, Movable, EpicsDevice):
|
|
45
|
+
"""Collection of PVs comprising a single bimorph channel.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
target_voltage: Float RW_RBV for target voltage, which can be set using parent mirror's all target proc
|
|
49
|
+
output_voltage: Float RW_RBV for current voltage on bimorph
|
|
50
|
+
status: BimorphMirrorOnOff readable for ON/OFF status of channel
|
|
51
|
+
shift: Float writeable shifting channel voltage
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
target_voltage: A[SignalRW[float], PvSuffix.rbv("VTRGT"), Format.CONFIG_SIGNAL]
|
|
55
|
+
output_voltage: A[SignalRW[float], PvSuffix.rbv("VOUT"), Format.HINTED_SIGNAL]
|
|
56
|
+
status: A[SignalR[BimorphMirrorOnOff], PvSuffix("STATUS"), Format.CONFIG_SIGNAL]
|
|
57
|
+
shift: A[SignalW[float], PvSuffix("SHIFT")]
|
|
58
|
+
|
|
59
|
+
@AsyncStatus.wrap
|
|
60
|
+
async def set(self, value: float):
|
|
61
|
+
"""Sets channel's VOUT to given value.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
value: float to set VOUT to
|
|
65
|
+
"""
|
|
66
|
+
await self.output_voltage.set(value)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BimorphMirror(StandardReadable, Movable):
|
|
70
|
+
"""Class to represent CAENels Bimorph Mirrors.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
channels: DeviceVector of BimorphMirrorChannel, indexed from 1, for each channel
|
|
74
|
+
enabled: Writeable BimorphOnOff
|
|
75
|
+
commit_target_voltages: Procable signal that writes values in each channel's VTRGT to VOUT
|
|
76
|
+
status: Readable BimorphMirrorStatus Busy/Idle status
|
|
77
|
+
err: Alarm status"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, prefix: str, number_of_channels: int, name=""):
|
|
80
|
+
"""
|
|
81
|
+
Args:
|
|
82
|
+
prefix: str PV prefix
|
|
83
|
+
number_of_channels: int number of channels on bimorph mirror (can be zero)
|
|
84
|
+
name: str name of device
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ValueError: number_of_channels is less than zero"""
|
|
88
|
+
|
|
89
|
+
if number_of_channels < 0:
|
|
90
|
+
raise ValueError(f"Number of channels is below zero: {number_of_channels}")
|
|
91
|
+
|
|
92
|
+
with self.add_children_as_readables():
|
|
93
|
+
self.channels = DeviceVector(
|
|
94
|
+
{
|
|
95
|
+
i: BimorphMirrorChannel(f"{prefix}C{i}:")
|
|
96
|
+
for i in range(1, number_of_channels + 1)
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
self.enabled = epics_signal_w(BimorphMirrorOnOff, f"{prefix}ONOFF")
|
|
100
|
+
self.commit_target_voltages = epics_signal_x(f"{prefix}ALLTRGT.PROC")
|
|
101
|
+
self.status = epics_signal_r(BimorphMirrorStatus, f"{prefix}STATUS")
|
|
102
|
+
self.err = epics_signal_r(str, f"{prefix}ERR")
|
|
103
|
+
super().__init__(name=name)
|
|
104
|
+
|
|
105
|
+
@AsyncStatus.wrap
|
|
106
|
+
async def set(self, value: Mapping[int, float], tolerance: float = 0.0001) -> None:
|
|
107
|
+
"""Sets bimorph voltages in parrallel via target voltage and all proc.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
value: Dict of channel numbers to target voltages
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
ValueError: On set to non-existent channel"""
|
|
114
|
+
|
|
115
|
+
if any(key not in self.channels for key in value):
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"Attempting to put to non-existent channels: {[key for key in value if (key not in self.channels)]}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Write target voltages:
|
|
121
|
+
await asyncio.gather(
|
|
122
|
+
*[
|
|
123
|
+
self.channels[i].target_voltage.set(target, wait=True)
|
|
124
|
+
for i, target in value.items()
|
|
125
|
+
]
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Trigger set target voltages:
|
|
129
|
+
await self.commit_target_voltages.trigger()
|
|
130
|
+
|
|
131
|
+
# Wait for values to propogate to voltage out rbv:
|
|
132
|
+
await asyncio.gather(
|
|
133
|
+
*[
|
|
134
|
+
wait_for_value(
|
|
135
|
+
self.channels[i].output_voltage,
|
|
136
|
+
tolerance_func_builder(tolerance, target),
|
|
137
|
+
timeout=DEFAULT_TIMEOUT,
|
|
138
|
+
)
|
|
139
|
+
for i, target in value.items()
|
|
140
|
+
],
|
|
141
|
+
wait_for_value(
|
|
142
|
+
self.status, BimorphMirrorStatus.IDLE, timeout=DEFAULT_TIMEOUT
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def tolerance_func_builder(tolerance: float, target_value: float):
|
|
148
|
+
def is_within_value(x):
|
|
149
|
+
return abs(x - target_value) <= tolerance
|
|
150
|
+
|
|
151
|
+
return is_within_value
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from .current_amplifier import CurrentAmp
|
|
2
|
+
from .current_amplifier_detector import CurrentAmpCounter, CurrentAmpDet
|
|
3
|
+
from .femto import (
|
|
4
|
+
Femto3xxGainTable,
|
|
5
|
+
Femto3xxGainToCurrentTable,
|
|
6
|
+
Femto3xxRaiseTime,
|
|
7
|
+
FemtoDDPCA,
|
|
8
|
+
)
|
|
9
|
+
from .sr570 import (
|
|
10
|
+
SR570,
|
|
11
|
+
SR570FineGainTable,
|
|
12
|
+
SR570FullGainTable,
|
|
13
|
+
SR570GainTable,
|
|
14
|
+
SR570GainToCurrentTable,
|
|
15
|
+
SR570RaiseTimeTable,
|
|
16
|
+
)
|
|
17
|
+
from .struck_scaler_counter import StruckScaler
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"FemtoDDPCA",
|
|
21
|
+
"Femto3xxGainTable",
|
|
22
|
+
"Femto3xxRaiseTime",
|
|
23
|
+
"CurrentAmp",
|
|
24
|
+
"Femto3xxGainToCurrentTable",
|
|
25
|
+
"CurrentAmpCounter",
|
|
26
|
+
"CurrentAmpDet",
|
|
27
|
+
"SR570",
|
|
28
|
+
"SR570GainTable",
|
|
29
|
+
"SR570FineGainTable",
|
|
30
|
+
"SR570FullGainTable",
|
|
31
|
+
"SR570GainToCurrentTable",
|
|
32
|
+
"SR570RaiseTimeTable",
|
|
33
|
+
"StruckScaler",
|
|
34
|
+
]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from bluesky.protocols import (
|
|
5
|
+
Movable,
|
|
6
|
+
Preparable,
|
|
7
|
+
)
|
|
8
|
+
from ophyd_async.core import AsyncStatus, StandardReadable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CurrentAmp(ABC, StandardReadable, Movable):
|
|
12
|
+
"""
|
|
13
|
+
Base class for current amplifier, it contains the minimal functionality
|
|
14
|
+
a current amplifier needs:
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
gain_conversion_table (Enum): The conversion table between current
|
|
18
|
+
and gain setting.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, gain_conversion_table: type[Enum], name: str = "") -> None:
|
|
22
|
+
self.gain_conversion_table = gain_conversion_table
|
|
23
|
+
super().__init__(name)
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
@AsyncStatus.wrap
|
|
27
|
+
async def increase_gain(self, value: int = 1) -> None:
|
|
28
|
+
"""Increase gain, increment by 1 by default.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
bool: True if success.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@AsyncStatus.wrap
|
|
35
|
+
@abstractmethod
|
|
36
|
+
async def decrease_gain(self, value: int = 1) -> None:
|
|
37
|
+
"""Decrease gain, decrement by 1 by default.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
bool: True if success.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@AsyncStatus.wrap
|
|
44
|
+
@abstractmethod
|
|
45
|
+
async def get_gain(self) -> type[Enum]:
|
|
46
|
+
"""Get the current gain setting
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Enum: The member name of the current gain setting in gain_conversion_table.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
@AsyncStatus.wrap
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def get_upperlimit(self) -> float:
|
|
55
|
+
"""Get the upper limit of the current amplifier"""
|
|
56
|
+
|
|
57
|
+
@AsyncStatus.wrap
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def get_lowerlimit(self) -> float:
|
|
60
|
+
"""Get the lower limit of the current amplifier"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CurrentAmpCounter(ABC, StandardReadable, Preparable):
|
|
64
|
+
"""
|
|
65
|
+
Base class for current amplifier counter, it contain the minimal implementations
|
|
66
|
+
required for a counter/detector to function with CurrentAmpDet:
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
count_per_volt (float): The conversion factor between counter output and voltage.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, count_per_volt: float, name: str = ""):
|
|
73
|
+
self.count_per_volt = count_per_volt
|
|
74
|
+
super().__init__(name)
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
async def get_count(self) -> float:
|
|
78
|
+
""" "Get count
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
float: Current count
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
async def get_count_per_sec(self) -> float:
|
|
86
|
+
"""Get count per second
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
float: Current count per second
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
async def get_voltage_per_sec(self) -> float:
|
|
94
|
+
"""Get count per second in voltage
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
float: Current count in volt per second
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
@AsyncStatus.wrap
|
|
102
|
+
async def prepare(self, value: float) -> None:
|
|
103
|
+
"""Prepare method for setting up the counter"""
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from bluesky.protocols import Preparable, Reading
|
|
4
|
+
from ophyd_async.core import (
|
|
5
|
+
AsyncStatus,
|
|
6
|
+
Reference,
|
|
7
|
+
StandardReadable,
|
|
8
|
+
StandardReadableFormat,
|
|
9
|
+
soft_signal_r_and_setter,
|
|
10
|
+
soft_signal_rw,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from dodal.devices.current_amplifiers.current_amplifier import (
|
|
14
|
+
CurrentAmp,
|
|
15
|
+
CurrentAmpCounter,
|
|
16
|
+
)
|
|
17
|
+
from dodal.log import LOGGER
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CurrentAmpDet(StandardReadable, Preparable):
|
|
21
|
+
"""
|
|
22
|
+
CurrentAmpDet composed of a CurrentAmp and a CurrentAmpCounter. It provides
|
|
23
|
+
the option for automatically changing the CurrentAmp gain to within the optimal
|
|
24
|
+
range. It also converts the currentAmp/counter output back into the detector
|
|
25
|
+
current output in Amp.
|
|
26
|
+
Attributes:
|
|
27
|
+
current_amp (currentAmp): Current amplifier type device.
|
|
28
|
+
counter (CurrentAmpCounter): Counter that capture the current amplifier output.
|
|
29
|
+
current (SignalRW([float]): Soft signal to store the corrected current.
|
|
30
|
+
auto_mode (signalR([bool])): Soft signal to store the flag for auto gain.
|
|
31
|
+
name (str): Name of the device.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
current_amp: CurrentAmp,
|
|
37
|
+
counter: CurrentAmpCounter,
|
|
38
|
+
name: str = "",
|
|
39
|
+
) -> None:
|
|
40
|
+
self.current_amp = Reference(current_amp)
|
|
41
|
+
self.counter = Reference(counter)
|
|
42
|
+
with self.add_children_as_readables():
|
|
43
|
+
self.current, self._set_current = soft_signal_r_and_setter(
|
|
44
|
+
float, initial_value=None, units="Amp"
|
|
45
|
+
)
|
|
46
|
+
with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
|
|
47
|
+
self.auto_mode = soft_signal_rw(bool, initial_value=True)
|
|
48
|
+
super().__init__(name)
|
|
49
|
+
|
|
50
|
+
async def read(self) -> dict[str, Reading]:
|
|
51
|
+
"""
|
|
52
|
+
Read is modified so that if auto_mode is true it will optimise gain before
|
|
53
|
+
taking the final reading
|
|
54
|
+
"""
|
|
55
|
+
if await self.auto_mode.get_value():
|
|
56
|
+
LOGGER.info(f"{self.name}-Attempting auto-gain")
|
|
57
|
+
status = self.auto_gain()
|
|
58
|
+
try:
|
|
59
|
+
await status
|
|
60
|
+
LOGGER.info(
|
|
61
|
+
f"{self.name} new gain = {await self.current_amp().get_gain()}."
|
|
62
|
+
)
|
|
63
|
+
except ValueError as ex:
|
|
64
|
+
LOGGER.warning(f"{self.name} gain went outside limits")
|
|
65
|
+
# Further details are provided to the user in the logged exception
|
|
66
|
+
LOGGER.exception(ex)
|
|
67
|
+
current = await self.get_corrected_current()
|
|
68
|
+
self._set_current(current)
|
|
69
|
+
return await super().read()
|
|
70
|
+
|
|
71
|
+
@AsyncStatus.wrap
|
|
72
|
+
async def auto_gain(self) -> None:
|
|
73
|
+
within_limits = False
|
|
74
|
+
while not within_limits:
|
|
75
|
+
reading = abs(await self.counter().get_voltage_per_sec())
|
|
76
|
+
upper_limit, lower_limit = await asyncio.gather(
|
|
77
|
+
self.current_amp().get_upperlimit(),
|
|
78
|
+
self.current_amp().get_lowerlimit(),
|
|
79
|
+
)
|
|
80
|
+
if reading > upper_limit:
|
|
81
|
+
await self.current_amp().decrease_gain()
|
|
82
|
+
elif reading < lower_limit:
|
|
83
|
+
await self.current_amp().increase_gain()
|
|
84
|
+
else:
|
|
85
|
+
within_limits = True
|
|
86
|
+
|
|
87
|
+
async def get_corrected_current(self) -> float:
|
|
88
|
+
"""
|
|
89
|
+
Convert the output(count and gain) back into the read detector output in Amp.
|
|
90
|
+
"""
|
|
91
|
+
current_gain, voltage_per_sec = await asyncio.gather(
|
|
92
|
+
self.current_amp().get_gain(),
|
|
93
|
+
self.counter().get_voltage_per_sec(),
|
|
94
|
+
)
|
|
95
|
+
correction_factor = current_gain.value
|
|
96
|
+
corrected_current = voltage_per_sec / correction_factor
|
|
97
|
+
return corrected_current
|
|
98
|
+
|
|
99
|
+
@AsyncStatus.wrap
|
|
100
|
+
async def stage(self) -> None:
|
|
101
|
+
await self.counter().stage()
|
|
102
|
+
|
|
103
|
+
@AsyncStatus.wrap
|
|
104
|
+
async def unstage(self) -> None:
|
|
105
|
+
await self.counter().unstage()
|
|
106
|
+
|
|
107
|
+
@AsyncStatus.wrap
|
|
108
|
+
async def prepare(self, value) -> None:
|
|
109
|
+
await self.counter().prepare(value=value)
|