dls-dodal 1.58.0__py3-none-any.whl → 1.59.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/METADATA +2 -1
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/RECORD +58 -43
- dodal/_version.py +2 -2
- dodal/beamlines/b07.py +10 -5
- dodal/beamlines/b07_1.py +10 -5
- dodal/beamlines/b21.py +22 -0
- dodal/beamlines/i02_1.py +80 -0
- dodal/beamlines/i03.py +5 -3
- dodal/beamlines/i04.py +5 -3
- dodal/beamlines/i09.py +10 -9
- dodal/beamlines/i09_1.py +10 -5
- dodal/beamlines/i10-1.py +25 -0
- dodal/beamlines/i10.py +17 -1
- dodal/beamlines/i11.py +0 -17
- dodal/beamlines/i19_2.py +11 -0
- dodal/beamlines/i21.py +27 -0
- dodal/beamlines/i22.py +12 -2
- dodal/beamlines/i24.py +32 -3
- dodal/beamlines/k07.py +31 -0
- dodal/beamlines/p60.py +10 -9
- dodal/common/watcher_utils.py +1 -1
- dodal/devices/apple2_undulator.py +18 -142
- dodal/devices/attenuator/attenuator.py +48 -2
- dodal/devices/attenuator/filter.py +3 -0
- dodal/devices/attenuator/filter_selections.py +26 -0
- dodal/devices/eiger.py +2 -1
- dodal/devices/electron_analyser/__init__.py +4 -0
- dodal/devices/electron_analyser/abstract/base_driver_io.py +30 -18
- dodal/devices/electron_analyser/energy_sources.py +101 -0
- dodal/devices/electron_analyser/specs/detector.py +6 -6
- dodal/devices/electron_analyser/specs/driver_io.py +7 -15
- dodal/devices/electron_analyser/vgscienta/detector.py +6 -6
- dodal/devices/electron_analyser/vgscienta/driver_io.py +7 -14
- dodal/devices/fast_grid_scan.py +130 -64
- dodal/devices/focusing_mirror.py +30 -0
- dodal/devices/i02_1/__init__.py +0 -0
- dodal/devices/i02_1/fast_grid_scan.py +61 -0
- dodal/devices/i02_1/sample_motors.py +19 -0
- dodal/devices/i04/murko_results.py +69 -23
- dodal/devices/i10/i10_apple2.py +282 -140
- dodal/devices/i21/__init__.py +3 -0
- dodal/devices/i21/enums.py +8 -0
- dodal/devices/i22/nxsas.py +2 -0
- dodal/devices/i24/commissioning_jungfrau.py +114 -0
- dodal/devices/smargon.py +0 -56
- dodal/devices/temperture_controller/__init__.py +3 -0
- dodal/devices/temperture_controller/lakeshore/__init__.py +0 -0
- dodal/devices/temperture_controller/lakeshore/lakeshore.py +204 -0
- dodal/devices/temperture_controller/lakeshore/lakeshore_io.py +112 -0
- dodal/devices/tetramm.py +38 -16
- dodal/devices/v2f.py +39 -0
- dodal/devices/zebra/zebra.py +1 -0
- dodal/devices/zebra/zebra_constants_mapping.py +1 -1
- dodal/parameters/experiment_parameter_base.py +1 -5
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/WHEEL +0 -0
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/licenses/LICENSE +0 -0
- {dls_dodal-1.58.0.dist-info → dls_dodal-1.59.1.dist-info}/top_level.txt +0 -0
dodal/devices/smargon.py
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from collections.abc import Collection, Generator
|
|
3
|
-
from dataclasses import dataclass
|
|
4
2
|
from enum import Enum
|
|
5
3
|
from math import isclose
|
|
6
4
|
from typing import TypedDict, cast
|
|
7
5
|
|
|
8
|
-
from bluesky import plan_stubs as bps
|
|
9
6
|
from bluesky.protocols import Movable
|
|
10
|
-
from bluesky.utils import Msg
|
|
11
7
|
from ophyd_async.core import (
|
|
12
8
|
AsyncStatus,
|
|
13
9
|
Device,
|
|
@@ -66,40 +62,6 @@ class StubOffsets(Device):
|
|
|
66
62
|
await self.to_robot_load.set(1)
|
|
67
63
|
|
|
68
64
|
|
|
69
|
-
@dataclass
|
|
70
|
-
class AxisLimit:
|
|
71
|
-
"""Represents the minimum and maximum allowable values on an axis"""
|
|
72
|
-
|
|
73
|
-
min_value: float
|
|
74
|
-
max_value: float
|
|
75
|
-
|
|
76
|
-
def contains(self, pos: float):
|
|
77
|
-
"""Determine if the specified value is within limits.
|
|
78
|
-
|
|
79
|
-
Args:
|
|
80
|
-
pos: the value to check
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
True if the value does not exceed the limits
|
|
84
|
-
"""
|
|
85
|
-
return self.min_value <= pos <= self.max_value
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@dataclass
|
|
89
|
-
class XYZLimits:
|
|
90
|
-
"""The limits of the smargon x, y, z axes."""
|
|
91
|
-
|
|
92
|
-
x: AxisLimit
|
|
93
|
-
y: AxisLimit
|
|
94
|
-
z: AxisLimit
|
|
95
|
-
|
|
96
|
-
def position_valid(self, pos: Collection[float]) -> bool:
|
|
97
|
-
return all(
|
|
98
|
-
axis_limits.contains(value)
|
|
99
|
-
for axis_limits, value in zip([self.x, self.y, self.z], pos, strict=False)
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
65
|
class DeferMoves(StrictEnum):
|
|
104
66
|
ON = "Defer On"
|
|
105
67
|
OFF = "Defer Off"
|
|
@@ -144,24 +106,6 @@ class Smargon(XYZStage, Movable):
|
|
|
144
106
|
|
|
145
107
|
super().__init__(prefix, name)
|
|
146
108
|
|
|
147
|
-
def get_xyz_limits(self) -> Generator[Msg, None, XYZLimits]:
|
|
148
|
-
"""Obtain a plan stub that returns the smargon XYZ axis limits
|
|
149
|
-
|
|
150
|
-
Yields:
|
|
151
|
-
Bluesky messages
|
|
152
|
-
|
|
153
|
-
Returns:
|
|
154
|
-
the axis limits
|
|
155
|
-
"""
|
|
156
|
-
limits = {}
|
|
157
|
-
for name, pv in [
|
|
158
|
-
(attr_name, getattr(self, attr_name)) for attr_name in ["x", "y", "z"]
|
|
159
|
-
]:
|
|
160
|
-
min_value = yield from bps.rd(pv.low_limit_travel)
|
|
161
|
-
max_value = yield from bps.rd(pv.high_limit_travel)
|
|
162
|
-
limits[name] = AxisLimit(min_value, max_value)
|
|
163
|
-
return XYZLimits(**limits)
|
|
164
|
-
|
|
165
109
|
@AsyncStatus.wrap
|
|
166
110
|
async def set(self, value: CombinedMove):
|
|
167
111
|
"""This will move all motion together in a deferred move.
|
|
File without changes
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from asyncio import gather
|
|
2
|
+
|
|
3
|
+
from bluesky.protocols import Movable
|
|
4
|
+
from ophyd_async.core import (
|
|
5
|
+
AsyncStatus,
|
|
6
|
+
SignalDatatypeT,
|
|
7
|
+
StandardReadable,
|
|
8
|
+
StandardReadableFormat,
|
|
9
|
+
StrictEnum,
|
|
10
|
+
derived_signal_rw,
|
|
11
|
+
soft_signal_rw,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .lakeshore_io import (
|
|
15
|
+
LakeshoreBaseIO,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Heater336Settings(StrictEnum):
|
|
20
|
+
OFF = "Off"
|
|
21
|
+
LOW = "Low"
|
|
22
|
+
MEDIUM = "Medium"
|
|
23
|
+
HIGH = "High"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Lakeshore(LakeshoreBaseIO, StandardReadable, Movable[float]):
|
|
27
|
+
"""
|
|
28
|
+
Device for controlling and reading from a Lakeshore temperature controller.
|
|
29
|
+
It supports multiple channels and PID control.
|
|
30
|
+
|
|
31
|
+
Attributes
|
|
32
|
+
----------
|
|
33
|
+
temperature : LakeshoreBaseIO
|
|
34
|
+
Temperature IO interface.
|
|
35
|
+
PID : PIDBaseIO
|
|
36
|
+
PID IO interface.
|
|
37
|
+
control_channel : derived_signal_rw
|
|
38
|
+
Signal for selecting the control channel,
|
|
39
|
+
optional readback as hinted signal
|
|
40
|
+
(default readback channel is the same as control channel).
|
|
41
|
+
|
|
42
|
+
temperature_high_limit: soft_signal_rw
|
|
43
|
+
Signal to store the soft high temperature limit.
|
|
44
|
+
temperature_low_limit: soft_signal_rw
|
|
45
|
+
Signal to store the soft low temperature limit.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
Methods
|
|
49
|
+
-------
|
|
50
|
+
set(value: float)
|
|
51
|
+
Set the temperature setpoint for the selected control channel.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
prefix: str,
|
|
57
|
+
num_readback_channel: int,
|
|
58
|
+
heater_setting: type[SignalDatatypeT],
|
|
59
|
+
control_channel: int = 1,
|
|
60
|
+
single_control_channel: bool = False,
|
|
61
|
+
name: str = "",
|
|
62
|
+
):
|
|
63
|
+
"""
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
prefix : str
|
|
67
|
+
The EPICS prefix for the device.
|
|
68
|
+
no_channels : int
|
|
69
|
+
Number of temperature channels.
|
|
70
|
+
heater_setting : type[SignalDatatypeT]
|
|
71
|
+
Enum type for heater settings.
|
|
72
|
+
control_channel : int, optional
|
|
73
|
+
The initial control channel (default is 1).
|
|
74
|
+
single_control_channel : bool, optional
|
|
75
|
+
Whether to use a single control channel (default is False).
|
|
76
|
+
name : str, optional
|
|
77
|
+
Name of the device.
|
|
78
|
+
"""
|
|
79
|
+
self._control_channel = soft_signal_rw(int, initial_value=control_channel)
|
|
80
|
+
self.temperature_high_limit = soft_signal_rw(float, initial_value=400)
|
|
81
|
+
self.temperature_low_limit = soft_signal_rw(float, initial_value=0)
|
|
82
|
+
|
|
83
|
+
self.control_channel = derived_signal_rw(
|
|
84
|
+
raw_to_derived=self._get_control_channel,
|
|
85
|
+
set_derived=self._set_control_channel,
|
|
86
|
+
current_channel=self._control_channel,
|
|
87
|
+
)
|
|
88
|
+
super().__init__(
|
|
89
|
+
prefix=prefix,
|
|
90
|
+
num_readback_channel=num_readback_channel,
|
|
91
|
+
heater_setting=heater_setting,
|
|
92
|
+
name=name,
|
|
93
|
+
single_control_channel=single_control_channel,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
self.add_readables(
|
|
97
|
+
[setpoint.user_setpoint for setpoint in self.control_channels.values()]
|
|
98
|
+
)
|
|
99
|
+
self.add_readables(
|
|
100
|
+
list(self.readback.values()), format=StandardReadableFormat.HINTED_SIGNAL
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
self.add_readables(
|
|
104
|
+
[
|
|
105
|
+
self._control_channel,
|
|
106
|
+
self.control_channels[control_channel].p,
|
|
107
|
+
self.control_channels[control_channel].i,
|
|
108
|
+
self.control_channels[control_channel].d,
|
|
109
|
+
self.control_channels[control_channel].heater_output_range,
|
|
110
|
+
],
|
|
111
|
+
StandardReadableFormat.CONFIG_SIGNAL,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@AsyncStatus.wrap
|
|
115
|
+
async def set(self, value: float) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Set the temperature setpoint for the active control channel.
|
|
118
|
+
"""
|
|
119
|
+
high, low = await gather(
|
|
120
|
+
self.temperature_high_limit.get_value(),
|
|
121
|
+
self.temperature_low_limit.get_value(),
|
|
122
|
+
)
|
|
123
|
+
if high >= value >= low:
|
|
124
|
+
await self.control_channels[
|
|
125
|
+
await self.control_channel.get_value()
|
|
126
|
+
].user_setpoint.set(value)
|
|
127
|
+
else:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"{self.name} requested temperature {value} is outside limits: {low}, {high}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _get_control_channel(self, current_channel: int) -> int:
|
|
133
|
+
return current_channel
|
|
134
|
+
|
|
135
|
+
async def _set_control_channel(self, value: int) -> None:
|
|
136
|
+
if value < 1 or value > len(self.control_channels):
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"Control channel must be between 1 and {len(self.control_channels)}."
|
|
139
|
+
)
|
|
140
|
+
await self._control_channel.set(value)
|
|
141
|
+
self._read_config_funcs = (
|
|
142
|
+
self._control_channel.read,
|
|
143
|
+
self.control_channels[value].user_setpoint.read,
|
|
144
|
+
self.control_channels[value].p.read,
|
|
145
|
+
self.control_channels[value].i.read,
|
|
146
|
+
self.control_channels[value].d.read,
|
|
147
|
+
self.control_channels[value].heater_output_range.read,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class Lakeshore336(Lakeshore):
|
|
152
|
+
def __init__(
|
|
153
|
+
self,
|
|
154
|
+
prefix: str,
|
|
155
|
+
control_channel: int = 1,
|
|
156
|
+
name: str = "",
|
|
157
|
+
):
|
|
158
|
+
"""
|
|
159
|
+
Lakeshore 336 temperature controller. With 4 readback and control channels.
|
|
160
|
+
Heater settings are: Off, Low, Medium, High.
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
prefix : str
|
|
164
|
+
The EPICS prefix for the device.
|
|
165
|
+
control_channel : int, optional
|
|
166
|
+
The initial control channel (default is 1).
|
|
167
|
+
"""
|
|
168
|
+
super().__init__(
|
|
169
|
+
prefix=prefix,
|
|
170
|
+
num_readback_channel=4,
|
|
171
|
+
heater_setting=Heater336Settings,
|
|
172
|
+
control_channel=control_channel,
|
|
173
|
+
single_control_channel=False,
|
|
174
|
+
name=name,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class Lakeshore340(Lakeshore):
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
prefix: str,
|
|
182
|
+
control_channel: int = 1,
|
|
183
|
+
name: str = "",
|
|
184
|
+
):
|
|
185
|
+
"""Lakeshore 340 temperature controller. With 4 readback channels and a single
|
|
186
|
+
control channel.
|
|
187
|
+
Heater settings are in power from 0 to 5. 0 is 0 watt, 5 is 50 watt.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
prefix : str
|
|
192
|
+
The EPICS prefix for the device.
|
|
193
|
+
control_channel : int, optional
|
|
194
|
+
The initial control channel (default is 1).
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
super().__init__(
|
|
198
|
+
prefix=prefix,
|
|
199
|
+
num_readback_channel=4,
|
|
200
|
+
heater_setting=float,
|
|
201
|
+
control_channel=control_channel,
|
|
202
|
+
single_control_channel=True,
|
|
203
|
+
name=name,
|
|
204
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from ophyd_async.core import Device, DeviceVector, SignalDatatypeT
|
|
2
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LakeshoreControlChannel(Device):
|
|
6
|
+
"""
|
|
7
|
+
Single control channel for a Lakeshore temperature controller.
|
|
8
|
+
|
|
9
|
+
Provides access to setpoint, ramp rate, ramp enable, heater output, heater output range,
|
|
10
|
+
PID parameters (P, I, D), and manual output for the channel.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
prefix: str,
|
|
16
|
+
suffix: str,
|
|
17
|
+
heater_type: type[SignalDatatypeT],
|
|
18
|
+
name: str = "",
|
|
19
|
+
):
|
|
20
|
+
"""Initialize the LakeshoreControlChannel device.
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
prefix: str
|
|
24
|
+
The EPICS prefix for the Lakeshore device.
|
|
25
|
+
suffix: str
|
|
26
|
+
Suffix for the channel, used to differentiate multiple channels.
|
|
27
|
+
heater_type: SignalDatatypeT
|
|
28
|
+
Type of the heater output range.
|
|
29
|
+
name: str
|
|
30
|
+
Optional name for the device.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def channel_rw(channel_type, pv_name):
|
|
34
|
+
return epics_signal_rw(
|
|
35
|
+
channel_type,
|
|
36
|
+
f"{prefix}{pv_name}{suffix}",
|
|
37
|
+
f"{prefix}{pv_name}_S{suffix}",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
self.user_setpoint = channel_rw(channel_type=float, pv_name="SETP")
|
|
41
|
+
self.ramp_rate = channel_rw(channel_type=float, pv_name="RAMP")
|
|
42
|
+
self.ramp_enable = channel_rw(channel_type=int, pv_name="RAMPST")
|
|
43
|
+
self.heater_output_range = channel_rw(channel_type=heater_type, pv_name="RANGE")
|
|
44
|
+
self.p = channel_rw(channel_type=float, pv_name="P")
|
|
45
|
+
self.i = channel_rw(channel_type=float, pv_name="I")
|
|
46
|
+
self.d = channel_rw(channel_type=float, pv_name="D")
|
|
47
|
+
self.manual_output = channel_rw(channel_type=float, pv_name="MOUT")
|
|
48
|
+
self.heater_output = epics_signal_r(float, f"{prefix}{'HTR'}{suffix}")
|
|
49
|
+
|
|
50
|
+
super().__init__(name=name)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class LakeshoreBaseIO(Device):
|
|
54
|
+
"""Base class for Lakeshore temperature controller IO.
|
|
55
|
+
|
|
56
|
+
Provides access to control channels and readback channels for setpoint, ramp rate, heater output,
|
|
57
|
+
and PID parameters. Supports both single and multiple control channel configurations.
|
|
58
|
+
Note:
|
|
59
|
+
Almost all models have a controller for each readback channel but some models
|
|
60
|
+
only has a single controller for multiple readback channels.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
prefix: str,
|
|
66
|
+
num_readback_channel: int,
|
|
67
|
+
heater_setting: type[SignalDatatypeT],
|
|
68
|
+
name: str = "",
|
|
69
|
+
single_control_channel: bool = False,
|
|
70
|
+
):
|
|
71
|
+
"""Initialize the LakeshoreBaseIO device.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
-----------
|
|
75
|
+
prefix: str
|
|
76
|
+
The EPICS prefix for the Lakeshore device.
|
|
77
|
+
num_readback_channel: int
|
|
78
|
+
Number of readback channels to create.
|
|
79
|
+
heater_setting: SignalDatatypeT
|
|
80
|
+
Type of the heater setting.
|
|
81
|
+
name: str
|
|
82
|
+
Optional name for the device.
|
|
83
|
+
single_control_channel: bool
|
|
84
|
+
If True, use a single control channel for all readback.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
suffixes = (
|
|
88
|
+
[""]
|
|
89
|
+
if single_control_channel
|
|
90
|
+
else map(str, range(1, num_readback_channel + 1))
|
|
91
|
+
)
|
|
92
|
+
self.control_channels = DeviceVector(
|
|
93
|
+
{
|
|
94
|
+
i: LakeshoreControlChannel(
|
|
95
|
+
prefix=prefix, suffix=suffix, heater_type=heater_setting
|
|
96
|
+
)
|
|
97
|
+
for i, suffix in enumerate(suffixes, start=1)
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self.readback = DeviceVector(
|
|
102
|
+
{
|
|
103
|
+
i: epics_signal_r(
|
|
104
|
+
float,
|
|
105
|
+
read_pv=f"{prefix}KRDG{i - 1}",
|
|
106
|
+
)
|
|
107
|
+
for i in range(1, num_readback_channel + 1)
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
super().__init__(
|
|
111
|
+
name=name,
|
|
112
|
+
)
|
dodal/devices/tetramm.py
CHANGED
|
@@ -16,6 +16,7 @@ from ophyd_async.core import (
|
|
|
16
16
|
TriggerInfo,
|
|
17
17
|
set_and_wait_for_value,
|
|
18
18
|
soft_signal_r_and_setter,
|
|
19
|
+
wait_for_value,
|
|
19
20
|
)
|
|
20
21
|
from ophyd_async.epics.adcore import (
|
|
21
22
|
ADHDFWriter,
|
|
@@ -23,7 +24,7 @@ from ophyd_async.epics.adcore import (
|
|
|
23
24
|
NDFileHDFIO,
|
|
24
25
|
NDPluginBaseIO,
|
|
25
26
|
)
|
|
26
|
-
from ophyd_async.epics.core import PvSuffix,
|
|
27
|
+
from ophyd_async.epics.core import PvSuffix, epics_signal_r
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
class TetrammRange(StrictEnum):
|
|
@@ -77,6 +78,7 @@ class TetrammController(DetectorController):
|
|
|
77
78
|
_supported_trigger_types = {
|
|
78
79
|
DetectorTrigger.EDGE_TRIGGER: TetrammTrigger.EXT_TRIGGER,
|
|
79
80
|
DetectorTrigger.CONSTANT_GATE: TetrammTrigger.EXT_TRIGGER,
|
|
81
|
+
DetectorTrigger.VARIABLE_GATE: TetrammTrigger.EXT_TRIGGER,
|
|
80
82
|
}
|
|
81
83
|
""""On the TetrAMM ASCII mode requires a minimum value of ValuesPerRead of 500,
|
|
82
84
|
[...] binary mode the minimum value of ValuesPerRead is 5."
|
|
@@ -86,11 +88,9 @@ class TetrammController(DetectorController):
|
|
|
86
88
|
"""The TetrAMM always digitizes at 100 kHz"""
|
|
87
89
|
_base_sample_rate: int = 100_000
|
|
88
90
|
|
|
89
|
-
def __init__(
|
|
90
|
-
self,
|
|
91
|
-
driver: TetrammDriver,
|
|
92
|
-
) -> None:
|
|
91
|
+
def __init__(self, driver: TetrammDriver, file_io: NDFileHDFIO) -> None:
|
|
93
92
|
self.driver = driver
|
|
93
|
+
self._file_io = file_io
|
|
94
94
|
self._arm_status: AsyncStatus | None = None
|
|
95
95
|
|
|
96
96
|
def get_deadtime(self, exposure: float | None) -> float:
|
|
@@ -107,13 +107,19 @@ class TetrammController(DetectorController):
|
|
|
107
107
|
if trigger_info.livetime is None:
|
|
108
108
|
raise ValueError(f"{self.__class__.__name__} requires that livetime is set")
|
|
109
109
|
|
|
110
|
+
current_trig_status = await self.driver.trigger_mode.get_value()
|
|
111
|
+
|
|
112
|
+
if current_trig_status == TetrammTrigger.FREE_RUN: # if freerun turn off first
|
|
113
|
+
await self.disarm()
|
|
114
|
+
|
|
110
115
|
# trigger mode must be set first and on its own!
|
|
111
116
|
await self.driver.trigger_mode.set(
|
|
112
117
|
self._supported_trigger_types[trigger_info.trigger]
|
|
113
118
|
)
|
|
119
|
+
|
|
114
120
|
await asyncio.gather(
|
|
115
|
-
self.driver.averaging_time.set(trigger_info.livetime),
|
|
116
121
|
self.set_exposure(trigger_info.livetime),
|
|
122
|
+
self._file_io.num_capture.set(trigger_info.total_number_of_exposures),
|
|
117
123
|
)
|
|
118
124
|
|
|
119
125
|
# raise an error if asked to trigger faster than the max.
|
|
@@ -133,14 +139,18 @@ class TetrammController(DetectorController):
|
|
|
133
139
|
self._arm_status = await self.start_acquiring_driver_and_ensure_status()
|
|
134
140
|
|
|
135
141
|
async def wait_for_idle(self):
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
self.
|
|
142
|
+
# tetramm never goes idle really, actually it is always acquiring
|
|
143
|
+
# so need to wait for the capture to finish instead
|
|
144
|
+
await wait_for_value(self._file_io.acquire, False, timeout=None)
|
|
145
|
+
|
|
146
|
+
async def unstage(self):
|
|
147
|
+
await self.disarm()
|
|
148
|
+
await self._file_io.acquire.set(False)
|
|
139
149
|
|
|
140
150
|
async def disarm(self):
|
|
141
151
|
# We can't use caput callback as we already used it in arm() and we can't have
|
|
142
152
|
# 2 or they will deadlock
|
|
143
|
-
await
|
|
153
|
+
await set_and_wait_for_value(self.driver.acquire, False, timeout=1)
|
|
144
154
|
|
|
145
155
|
async def set_exposure(self, exposure: float) -> None:
|
|
146
156
|
"""Set the exposure time and acquire period.
|
|
@@ -164,7 +174,9 @@ class TetrammController(DetectorController):
|
|
|
164
174
|
"Tetramm exposure time must be at least "
|
|
165
175
|
f"{minimum_samples * sample_time}s, asked to set it to {exposure}s"
|
|
166
176
|
)
|
|
167
|
-
await self.driver.averaging_time.set(
|
|
177
|
+
await self.driver.averaging_time.set(
|
|
178
|
+
samples_per_reading * sample_time
|
|
179
|
+
) # correct
|
|
168
180
|
|
|
169
181
|
async def start_acquiring_driver_and_ensure_status(self) -> AsyncStatus:
|
|
170
182
|
"""Start acquiring driver, raising ValueError if the detector is in a bad state.
|
|
@@ -202,10 +214,7 @@ class TetrammDatasetDescriber(DatasetDescriber):
|
|
|
202
214
|
async def shape(self) -> tuple[int, int]:
|
|
203
215
|
return (
|
|
204
216
|
int(await self._driver.num_channels.get_value()),
|
|
205
|
-
int(
|
|
206
|
-
await self._driver.averaging_time.get_value()
|
|
207
|
-
/ await self._driver.sample_time.get_value(),
|
|
208
|
-
),
|
|
217
|
+
int(await self._driver.to_average.get_value()),
|
|
209
218
|
)
|
|
210
219
|
|
|
211
220
|
|
|
@@ -223,7 +232,20 @@ class TetrammDetector(StandardDetector):
|
|
|
223
232
|
):
|
|
224
233
|
self.driver = TetrammDriver(prefix + drv_suffix)
|
|
225
234
|
self.file_io = NDFileHDFIO(prefix + fileio_suffix)
|
|
226
|
-
controller = TetrammController(self.driver)
|
|
235
|
+
controller = TetrammController(self.driver, self.file_io)
|
|
236
|
+
|
|
237
|
+
self.current1 = epics_signal_r(float, prefix + "Cur1:MeanValue_RBV")
|
|
238
|
+
self.current2 = epics_signal_r(float, prefix + "Cur2:MeanValue_RBV")
|
|
239
|
+
self.current3 = epics_signal_r(float, prefix + "Cur3:MeanValue_RBV")
|
|
240
|
+
self.current4 = epics_signal_r(float, prefix + "Cur4:MeanValue_RBV")
|
|
241
|
+
|
|
242
|
+
self.sum_x = epics_signal_r(float, prefix + "SumX:MeanValue_RBV")
|
|
243
|
+
self.sum_y = epics_signal_r(float, prefix + "SumY:MeanValue_RBV")
|
|
244
|
+
self.sum_all = epics_signal_r(float, prefix + "SumAll:MeanValue_RBV")
|
|
245
|
+
self.diff_x = epics_signal_r(float, prefix + "DiffX:MeanValue_RBV")
|
|
246
|
+
self.diff_y = epics_signal_r(float, prefix + "DiffY:MeanValue_RBV")
|
|
247
|
+
self.pos_x = epics_signal_r(float, prefix + "PosX:MeanValue_RBV")
|
|
248
|
+
self.pos_y = epics_signal_r(float, prefix + "PosY:MeanValue_RBV")
|
|
227
249
|
|
|
228
250
|
writer = ADHDFWriter(
|
|
229
251
|
fileio=self.file_io,
|
dodal/devices/v2f.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from ophyd_async.core import StandardReadable, StandardReadableFormat, StrictEnum
|
|
2
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class V2FGain(StrictEnum):
|
|
6
|
+
LOW_NOISE3 = "10^3 low noise"
|
|
7
|
+
LOW_NOISE4 = "10^4 low noise"
|
|
8
|
+
LOW_NOISE5 = "10^5 low noise"
|
|
9
|
+
LOW_NOISE6 = "10^6 low noise"
|
|
10
|
+
LOW_NOISE7 = "10^7 low noise"
|
|
11
|
+
LOW_NOISE8 = "10^8 low noise"
|
|
12
|
+
LOW_NOISE9 = "10^9 low noise"
|
|
13
|
+
HIGHSPEED5 = "10^5 high speed"
|
|
14
|
+
HIGHSPEED6 = "10^6 high speed"
|
|
15
|
+
HIGHSPEED7 = "10^7 high speed"
|
|
16
|
+
HIGHSPEED8 = "10^8 high speed"
|
|
17
|
+
HIGHSPEED9 = "10^9 high speed"
|
|
18
|
+
HIGHSPEED10 = "10^10 high spd"
|
|
19
|
+
HIGHSPEED11 = "10^11 high spd"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class QDV2F(StandardReadable):
|
|
23
|
+
"""
|
|
24
|
+
A Quantum Detectors V2F low noise voltage to frequency converter.
|
|
25
|
+
Two channel V2F - 50mHz
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
prefix: str,
|
|
31
|
+
name: str = "",
|
|
32
|
+
I_suffix="I",
|
|
33
|
+
) -> None:
|
|
34
|
+
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
|
|
35
|
+
self.intensity = epics_signal_r(float, f"{prefix}{I_suffix}")
|
|
36
|
+
with self.add_children_as_readables():
|
|
37
|
+
self.gain = epics_signal_rw(V2FGain, f"{prefix}GAIN")
|
|
38
|
+
|
|
39
|
+
super().__init__(name)
|
dodal/devices/zebra/zebra.py
CHANGED
|
@@ -77,7 +77,7 @@ class ZebraMapping(ZebraMappingValidations):
|
|
|
77
77
|
Zebra's hardware configuration and wiring.
|
|
78
78
|
"""
|
|
79
79
|
|
|
80
|
-
# Zebra ophyd signal for
|
|
80
|
+
# Zebra ophyd signal for output can be accessed
|
|
81
81
|
# with, eg, zebra.output.out_pvs[zebra.mapping.outputs.TTL_DETECTOR]
|
|
82
82
|
outputs: ZebraTTLOutputs = ZebraTTLOutputs()
|
|
83
83
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from abc import ABC
|
|
1
|
+
from abc import ABC
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
@@ -9,7 +9,3 @@ class AbstractExperimentParameterBase(BaseModel, ABC):
|
|
|
9
9
|
|
|
10
10
|
class AbstractExperimentWithBeamParams(AbstractExperimentParameterBase):
|
|
11
11
|
transmission_fraction: float
|
|
12
|
-
|
|
13
|
-
@abstractmethod
|
|
14
|
-
def get_num_images(self) -> int:
|
|
15
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|