ophyd-async 0.12.3__py3-none-any.whl → 0.13.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.
- ophyd_async/_version.py +16 -3
- ophyd_async/core/__init__.py +11 -0
- ophyd_async/core/_detector.py +7 -10
- ophyd_async/core/_enums.py +21 -0
- ophyd_async/core/_signal.py +9 -9
- ophyd_async/core/_utils.py +5 -4
- ophyd_async/epics/adandor/_andor.py +1 -2
- ophyd_async/epics/adaravis/__init__.py +1 -2
- ophyd_async/epics/adaravis/_aravis_controller.py +4 -4
- ophyd_async/epics/adaravis/_aravis_io.py +2 -12
- ophyd_async/epics/adcore/__init__.py +4 -2
- ophyd_async/epics/adcore/_core_detector.py +1 -2
- ophyd_async/epics/adcore/_core_io.py +60 -8
- ophyd_async/epics/adcore/_core_logic.py +4 -4
- ophyd_async/epics/adcore/_core_writer.py +10 -7
- ophyd_async/epics/adcore/_hdf_writer.py +12 -7
- ophyd_async/epics/adcore/_utils.py +38 -0
- ophyd_async/epics/adkinetix/_kinetix_io.py +4 -4
- ophyd_async/epics/adpilatus/_pilatus.py +2 -6
- ophyd_async/epics/advimba/__init__.py +0 -2
- ophyd_async/epics/advimba/_vimba_controller.py +6 -9
- ophyd_async/epics/advimba/_vimba_io.py +3 -10
- ophyd_async/epics/core/_aioca.py +6 -2
- ophyd_async/epics/core/_p4p.py +2 -3
- ophyd_async/epics/core/_pvi_connector.py +1 -1
- ophyd_async/epics/pmac/__init__.py +6 -1
- ophyd_async/epics/pmac/_pmac_io.py +1 -0
- ophyd_async/epics/pmac/_utils.py +231 -0
- ophyd_async/epics/testing/_example_ioc.py +1 -2
- ophyd_async/plan_stubs/_nd_attributes.py +11 -37
- ophyd_async/plan_stubs/_settings.py +1 -1
- ophyd_async/tango/core/_tango_transport.py +2 -2
- ophyd_async/testing/_assert.py +6 -6
- ophyd_async/testing/_one_of_everything.py +1 -1
- {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/METADATA +5 -4
- {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/RECORD +39 -37
- {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/WHEEL +0 -0
- {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/licenses/LICENSE +0 -0
- {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/top_level.txt +0 -0
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
|
-
from ophyd_async.core import
|
|
4
|
-
DetectorTrigger,
|
|
5
|
-
TriggerInfo,
|
|
6
|
-
)
|
|
3
|
+
from ophyd_async.core import DetectorTrigger, OnOff, TriggerInfo
|
|
7
4
|
from ophyd_async.epics import adcore
|
|
8
5
|
|
|
9
|
-
from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode,
|
|
6
|
+
from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaTriggerSource
|
|
10
7
|
|
|
11
8
|
TRIGGER_MODE = {
|
|
12
|
-
DetectorTrigger.INTERNAL:
|
|
13
|
-
DetectorTrigger.CONSTANT_GATE:
|
|
14
|
-
DetectorTrigger.VARIABLE_GATE:
|
|
15
|
-
DetectorTrigger.EDGE_TRIGGER:
|
|
9
|
+
DetectorTrigger.INTERNAL: OnOff.OFF,
|
|
10
|
+
DetectorTrigger.CONSTANT_GATE: OnOff.ON,
|
|
11
|
+
DetectorTrigger.VARIABLE_GATE: OnOff.ON,
|
|
12
|
+
DetectorTrigger.EDGE_TRIGGER: OnOff.ON,
|
|
16
13
|
}
|
|
17
14
|
|
|
18
15
|
EXPOSE_OUT_MODE = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Annotated as A
|
|
2
2
|
|
|
3
|
-
from ophyd_async.core import SignalRW, StrictEnum
|
|
3
|
+
from ophyd_async.core import OnOff, SignalRW, StrictEnum
|
|
4
4
|
from ophyd_async.epics import adcore
|
|
5
5
|
from ophyd_async.epics.core import PvSuffix
|
|
6
6
|
|
|
@@ -30,17 +30,10 @@ class VimbaTriggerSource(StrictEnum):
|
|
|
30
30
|
class VimbaOverlap(StrictEnum):
|
|
31
31
|
"""Overlap modes for the Vimba detector."""
|
|
32
32
|
|
|
33
|
-
OFF =
|
|
33
|
+
OFF = OnOff.OFF.value
|
|
34
34
|
PREV_FRAME = "PreviousFrame"
|
|
35
35
|
|
|
36
36
|
|
|
37
|
-
class VimbaOnOff(StrictEnum):
|
|
38
|
-
"""On/Off modes on the Vimba detector."""
|
|
39
|
-
|
|
40
|
-
ON = "On"
|
|
41
|
-
OFF = "Off"
|
|
42
|
-
|
|
43
|
-
|
|
44
37
|
class VimbaExposeOutMode(StrictEnum):
|
|
45
38
|
"""Exposure control modes for Vimba detectors."""
|
|
46
39
|
|
|
@@ -55,6 +48,6 @@ class VimbaDriverIO(adcore.ADBaseIO):
|
|
|
55
48
|
SignalRW[VimbaConvertFormat], PvSuffix("ConvertPixelFormat")
|
|
56
49
|
]
|
|
57
50
|
trigger_source: A[SignalRW[VimbaTriggerSource], PvSuffix("TriggerSource")]
|
|
58
|
-
trigger_mode: A[SignalRW[
|
|
51
|
+
trigger_mode: A[SignalRW[OnOff], PvSuffix("TriggerMode")]
|
|
59
52
|
trigger_overlap: A[SignalRW[VimbaOverlap], PvSuffix("TriggerOverlap")]
|
|
60
53
|
exposure_mode: A[SignalRW[VimbaExposeOutMode], PvSuffix("ExposureMode")]
|
ophyd_async/epics/core/_aioca.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os
|
|
2
3
|
import sys
|
|
3
4
|
import typing
|
|
4
5
|
from collections.abc import Mapping, Sequence
|
|
@@ -40,6 +41,10 @@ from ._util import EpicsSignalBackend, format_datatype, get_supported_values
|
|
|
40
41
|
logger = logging.getLogger("ophyd_async")
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
def _all_updates() -> bool:
|
|
45
|
+
return os.environ.get("OPHYD_ASYNC_EPICS_CA_KEEP_ALL_UPDATES", "True") == "True"
|
|
46
|
+
|
|
47
|
+
|
|
43
48
|
def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
|
|
44
49
|
def get_limits(limit: str) -> LimitsRange | None:
|
|
45
50
|
low = getattr(value, f"lower_{limit}_limit", nan)
|
|
@@ -250,12 +255,11 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
250
255
|
datatype: type[SignalDatatypeT] | None,
|
|
251
256
|
read_pv: str = "",
|
|
252
257
|
write_pv: str = "",
|
|
253
|
-
all_updates: bool = True,
|
|
254
258
|
):
|
|
255
259
|
self.converter: CaConverter = DisconnectedCaConverter(float, dbr.DBR_DOUBLE)
|
|
256
260
|
self.initial_values: dict[str, AugmentedValue] = {}
|
|
257
261
|
self.subscription: Subscription | None = None
|
|
258
|
-
self._all_updates =
|
|
262
|
+
self._all_updates = _all_updates()
|
|
259
263
|
super().__init__(datatype, read_pv, write_pv)
|
|
260
264
|
|
|
261
265
|
def source(self, name: str, read: bool):
|
ophyd_async/epics/core/_p4p.py
CHANGED
|
@@ -22,7 +22,6 @@ from ophyd_async.core import (
|
|
|
22
22
|
SignalDatatype,
|
|
23
23
|
SignalDatatypeT,
|
|
24
24
|
SignalMetadata,
|
|
25
|
-
StrictEnum,
|
|
26
25
|
Table,
|
|
27
26
|
get_enum_cls,
|
|
28
27
|
get_unique,
|
|
@@ -82,7 +81,7 @@ def _metadata_from_value(datatype: type[SignalDatatype], value: Any) -> SignalMe
|
|
|
82
81
|
if (limits := _limits_from_value(value)) and specifier[-1] in _number_specifiers:
|
|
83
82
|
metadata["limits"] = limits
|
|
84
83
|
# Get choices from display or value
|
|
85
|
-
if datatype is str or
|
|
84
|
+
if datatype is str or get_enum_cls(datatype) is not None:
|
|
86
85
|
if hasattr(display_data, "choices"):
|
|
87
86
|
metadata["choices"] = display_data.choices
|
|
88
87
|
elif hasattr(value_data, "choices"):
|
|
@@ -327,7 +326,7 @@ def context() -> Context:
|
|
|
327
326
|
async def pvget_with_timeout(pv: str, timeout: float) -> Any:
|
|
328
327
|
try:
|
|
329
328
|
return await asyncio.wait_for(context().get(pv), timeout=timeout)
|
|
330
|
-
except
|
|
329
|
+
except TimeoutError as exc:
|
|
331
330
|
logger.debug(f"signal pva://{pv} timed out", exc_info=True)
|
|
332
331
|
raise NotConnected(f"pva://{pv}") from exc
|
|
333
332
|
|
|
@@ -6,13 +6,13 @@ from ophyd_async.core import (
|
|
|
6
6
|
Device,
|
|
7
7
|
DeviceConnector,
|
|
8
8
|
DeviceFiller,
|
|
9
|
+
LazyMock,
|
|
9
10
|
Signal,
|
|
10
11
|
SignalR,
|
|
11
12
|
SignalRW,
|
|
12
13
|
SignalW,
|
|
13
14
|
SignalX,
|
|
14
15
|
)
|
|
15
|
-
from ophyd_async.core._utils import LazyMock
|
|
16
16
|
|
|
17
17
|
from ._epics_connector import fill_backend_with_prefix
|
|
18
18
|
from ._signal import PvaSignalBackend, pvget_with_timeout
|
|
@@ -69,6 +69,7 @@ class PmacCoordIO(Device):
|
|
|
69
69
|
|
|
70
70
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
71
71
|
self.defer_moves = epics_signal_rw(bool, f"{prefix}DeferMoves")
|
|
72
|
+
self.cs_port = epics_signal_r(str, f"{prefix}Port")
|
|
72
73
|
self.cs_axis_setpoint = DeviceVector(
|
|
73
74
|
{
|
|
74
75
|
i + 1: epics_signal_rw(np.float64, f"{prefix}M{i + 1}:DirectDemand")
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import numpy.typing as npt
|
|
9
|
+
from scanspec.core import Slice
|
|
10
|
+
|
|
11
|
+
from ophyd_async.core import error_if_none, gather_dict
|
|
12
|
+
from ophyd_async.epics.motor import Motor
|
|
13
|
+
|
|
14
|
+
from ._pmac_io import CS_LETTERS, PmacIO
|
|
15
|
+
|
|
16
|
+
# PMAC durations are in milliseconds
|
|
17
|
+
# We must convert from scanspec durations (seconds) to milliseconds
|
|
18
|
+
# PMAC motion program multiples durations by 0.001
|
|
19
|
+
# (see https://github.com/DiamondLightSource/pmac/blob/afe81f8bb9179c3a20eff351f30bc6cfd1539ad9/pmacApp/pmc/trajectory_scan_code_ppmac.pmc#L241)
|
|
20
|
+
# Therefore, we must divide scanspec durations by 10e-6
|
|
21
|
+
TICK_S = 0.000001
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class _Trajectory:
|
|
26
|
+
positions: dict[Motor, np.ndarray]
|
|
27
|
+
velocities: dict[Motor, np.ndarray]
|
|
28
|
+
user_programs: npt.NDArray[np.int32]
|
|
29
|
+
durations: npt.NDArray[np.float64]
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_slice(cls, slice: Slice[Motor], ramp_up_time: float) -> _Trajectory:
|
|
33
|
+
"""Parse a trajectory with no gaps from a slice.
|
|
34
|
+
|
|
35
|
+
:param slice: Information about a series of scan frames along a number of axes
|
|
36
|
+
:param ramp_up_duration: Time required to ramp up to speed
|
|
37
|
+
:param ramp_down: Booleon representing if we ramp down or not
|
|
38
|
+
:returns Trajectory: Data class representing our parsed trajectory
|
|
39
|
+
:raises RuntimeError: Slice must have no gaps and a duration array
|
|
40
|
+
"""
|
|
41
|
+
slice_duration = error_if_none(slice.duration, "Slice must have a duration")
|
|
42
|
+
|
|
43
|
+
# Check if any gaps other than initial gap.
|
|
44
|
+
if any(slice.gap[1:]):
|
|
45
|
+
raise RuntimeError(
|
|
46
|
+
f"Cannot parse trajectory with gaps. Slice has gaps: {slice.gap}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
scan_size = len(slice)
|
|
50
|
+
motors = slice.axes()
|
|
51
|
+
|
|
52
|
+
positions: dict[Motor, npt.NDArray[np.float64]] = {}
|
|
53
|
+
velocities: dict[Motor, npt.NDArray[np.float64]] = {}
|
|
54
|
+
|
|
55
|
+
# Initialise arrays
|
|
56
|
+
positions = {motor: np.empty(2 * scan_size + 1, float) for motor in motors}
|
|
57
|
+
velocities = {motor: np.empty(2 * scan_size + 1, float) for motor in motors}
|
|
58
|
+
durations: npt.NDArray[np.float64] = np.empty(2 * scan_size + 1, float)
|
|
59
|
+
user_programs: npt.NDArray[np.int32] = np.ones(2 * scan_size + 1, float)
|
|
60
|
+
user_programs[-1] = 8
|
|
61
|
+
|
|
62
|
+
# Ramp up time for start of collection window
|
|
63
|
+
durations[0] = int(ramp_up_time / TICK_S)
|
|
64
|
+
# Half the time per point
|
|
65
|
+
durations[1:] = np.repeat(slice_duration / (2 * TICK_S), 2)
|
|
66
|
+
|
|
67
|
+
# Fill profile assuming no gaps
|
|
68
|
+
# Excluding starting points, we begin at our next frame
|
|
69
|
+
half_durations = slice_duration / 2
|
|
70
|
+
for motor in motors:
|
|
71
|
+
# Set the first position to be lower bound, then
|
|
72
|
+
# alternate mid and upper as the upper and lower
|
|
73
|
+
# bounds of neighbouring points are the same as gap is false
|
|
74
|
+
positions[motor][0] = slice.lower[motor][0]
|
|
75
|
+
positions[motor][1::2] = slice.midpoints[motor]
|
|
76
|
+
positions[motor][2::2] = slice.upper[motor]
|
|
77
|
+
# For velocities we will need the relative distances
|
|
78
|
+
mid_to_upper_velocities = (
|
|
79
|
+
slice.upper[motor] - slice.midpoints[motor]
|
|
80
|
+
) / half_durations
|
|
81
|
+
lower_to_mid_velocities = (
|
|
82
|
+
slice.midpoints[motor] - slice.lower[motor]
|
|
83
|
+
) / half_durations
|
|
84
|
+
# First velocity is the lower -> mid of first point
|
|
85
|
+
velocities[motor][0] = lower_to_mid_velocities[0]
|
|
86
|
+
# For the midpoints, we take the average of the
|
|
87
|
+
# lower -> mid and mid-> upper velocities of the same point
|
|
88
|
+
velocities[motor][1::2] = (
|
|
89
|
+
lower_to_mid_velocities + mid_to_upper_velocities
|
|
90
|
+
) / 2
|
|
91
|
+
# For the upper points, we need to take the average of the
|
|
92
|
+
# mid -> upper velocity of the previous point and
|
|
93
|
+
# lower -> mid velocity of the current point
|
|
94
|
+
velocities[motor][2:-1:2] = (
|
|
95
|
+
mid_to_upper_velocities[:-1] + lower_to_mid_velocities[1:]
|
|
96
|
+
) / 2
|
|
97
|
+
# For the last velocity take the mid to upper velocity
|
|
98
|
+
velocities[motor][-1] = mid_to_upper_velocities[-1]
|
|
99
|
+
|
|
100
|
+
return cls(
|
|
101
|
+
positions=positions,
|
|
102
|
+
velocities=velocities,
|
|
103
|
+
user_programs=user_programs,
|
|
104
|
+
durations=durations,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class _PmacMotorInfo:
|
|
110
|
+
cs_port: str
|
|
111
|
+
cs_number: int
|
|
112
|
+
motor_cs_index: dict[Motor, int]
|
|
113
|
+
motor_acceleration_rate: dict[Motor, float]
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
async def from_motors(cls, pmac: PmacIO, motors: Sequence[Motor]) -> _PmacMotorInfo:
|
|
117
|
+
"""Creates a _PmacMotorInfo instance based on a controller and list of motors.
|
|
118
|
+
|
|
119
|
+
:param pmac: The PMAC device
|
|
120
|
+
:param motors: Sequence of motors involved in trajectory
|
|
121
|
+
:raises RuntimeError:
|
|
122
|
+
if motors do not share common CS port or CS number, or if
|
|
123
|
+
motors do not have unique CS index assignments
|
|
124
|
+
:returns:
|
|
125
|
+
_PmacMotorInfo instance with motor's common CS port and CS number, and
|
|
126
|
+
dictionaries of motor's to their unique CS index and accelerate rate
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
assignments = {
|
|
130
|
+
motor: pmac.assignment[pmac.motor_assignment_index[motor]]
|
|
131
|
+
for motor in motors
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
cs_ports, cs_numbers, cs_axes, velocities, accls = await asyncio.gather(
|
|
135
|
+
gather_dict(
|
|
136
|
+
{motor: assignments[motor].cs_port.get_value() for motor in motors}
|
|
137
|
+
),
|
|
138
|
+
gather_dict(
|
|
139
|
+
{motor: assignments[motor].cs_number.get_value() for motor in motors}
|
|
140
|
+
),
|
|
141
|
+
gather_dict(
|
|
142
|
+
{
|
|
143
|
+
motor: assignments[motor].cs_axis_letter.get_value()
|
|
144
|
+
for motor in motors
|
|
145
|
+
}
|
|
146
|
+
),
|
|
147
|
+
gather_dict({motor: motor.max_velocity.get_value() for motor in motors}),
|
|
148
|
+
gather_dict(
|
|
149
|
+
{motor: motor.acceleration_time.get_value() for motor in motors}
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# check if the values in cs_port and cs_number are the same
|
|
154
|
+
cs_ports = set(cs_ports.values())
|
|
155
|
+
|
|
156
|
+
if len(cs_ports) != 1:
|
|
157
|
+
raise RuntimeError(
|
|
158
|
+
"Failed to fetch common CS port."
|
|
159
|
+
"Motors passed are assigned to multiple CS ports:"
|
|
160
|
+
f"{list(cs_ports)}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
cs_port = cs_ports.pop()
|
|
164
|
+
|
|
165
|
+
cs_numbers = set(cs_numbers.values())
|
|
166
|
+
if len(cs_numbers) != 1:
|
|
167
|
+
raise RuntimeError(
|
|
168
|
+
"Failed to fetch common CS number."
|
|
169
|
+
"Motors passed are assigned to multiple CS numbers:"
|
|
170
|
+
f"{list(cs_numbers)}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
cs_number = cs_numbers.pop()
|
|
174
|
+
|
|
175
|
+
motor_cs_index = {}
|
|
176
|
+
for motor in cs_axes:
|
|
177
|
+
try:
|
|
178
|
+
if not cs_axes[motor]:
|
|
179
|
+
raise ValueError
|
|
180
|
+
motor_cs_index[motor] = CS_LETTERS.index(cs_axes[motor])
|
|
181
|
+
except ValueError as err:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
"Failed to get motor CS index. "
|
|
184
|
+
f"Motor {motor.name} assigned to '{cs_axes[motor]}' "
|
|
185
|
+
f"but must be assignmed to '{CS_LETTERS}"
|
|
186
|
+
) from err
|
|
187
|
+
if len(set(motor_cs_index.values())) != len(motor_cs_index.items()):
|
|
188
|
+
raise RuntimeError(
|
|
189
|
+
"Failed to fetch distinct CS Axes."
|
|
190
|
+
"Motors passed are assigned to the same CS Axis"
|
|
191
|
+
f"{list(motor_cs_index)}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
motor_acceleration_rate = {
|
|
195
|
+
motor: float(velocities[motor]) / float(accls[motor])
|
|
196
|
+
for motor in velocities
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return _PmacMotorInfo(
|
|
200
|
+
cs_port, cs_number, motor_cs_index, motor_acceleration_rate
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def calculate_ramp_position_and_duration(
|
|
205
|
+
slice: Slice[Motor], motor_info: _PmacMotorInfo, is_up: bool
|
|
206
|
+
) -> tuple[dict[Motor, float], float]:
|
|
207
|
+
if slice.duration is None:
|
|
208
|
+
raise ValueError("Slice must have a duration")
|
|
209
|
+
|
|
210
|
+
scan_axes = slice.axes()
|
|
211
|
+
idx = 0 if is_up else -1
|
|
212
|
+
|
|
213
|
+
velocities: dict[Motor, float] = {}
|
|
214
|
+
ramp_times: list[float] = []
|
|
215
|
+
for axis in scan_axes:
|
|
216
|
+
velocity = (slice.upper[axis][idx] - slice.lower[axis][idx]) / slice.duration[
|
|
217
|
+
idx
|
|
218
|
+
]
|
|
219
|
+
velocities[axis] = velocity
|
|
220
|
+
ramp_times.append(abs(velocity) / motor_info.motor_acceleration_rate[axis])
|
|
221
|
+
|
|
222
|
+
max_ramp_time = max(ramp_times)
|
|
223
|
+
|
|
224
|
+
motor_to_ramp_position = {}
|
|
225
|
+
sign = -1 if is_up else 1
|
|
226
|
+
for axis, v in velocities.items():
|
|
227
|
+
ref_pos = slice.lower[axis][0] if is_up else slice.upper[axis][-1]
|
|
228
|
+
displacement = 0.5 * v * max_ramp_time
|
|
229
|
+
motor_to_ramp_position[axis] = ref_pos + sign * displacement
|
|
230
|
+
|
|
231
|
+
return (motor_to_ramp_position, max_ramp_time)
|
|
@@ -4,8 +4,7 @@ from typing import Annotated as A
|
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
|
|
7
|
-
from ophyd_async.core import Array1D, SignalR, SignalRW, StrictEnum, Table
|
|
8
|
-
from ophyd_async.core._utils import SubsetEnum
|
|
7
|
+
from ophyd_async.core import Array1D, SignalR, SignalRW, StrictEnum, SubsetEnum, Table
|
|
9
8
|
from ophyd_async.epics.core import EpicsDevice, PvSuffix
|
|
10
9
|
|
|
11
10
|
from ._utils import TestingIOC, generate_random_pv_prefix
|
|
@@ -1,57 +1,31 @@
|
|
|
1
1
|
from collections.abc import Sequence
|
|
2
|
-
from xml.etree import ElementTree as ET
|
|
3
2
|
|
|
4
3
|
import bluesky.plan_stubs as bps
|
|
5
4
|
|
|
6
|
-
from ophyd_async.core import Device
|
|
7
5
|
from ophyd_async.epics.adcore import (
|
|
6
|
+
AreaDetector,
|
|
8
7
|
NDArrayBaseIO,
|
|
9
8
|
NDAttributeDataType,
|
|
10
9
|
NDAttributeParam,
|
|
11
10
|
NDAttributePv,
|
|
12
11
|
NDFileHDFIO,
|
|
12
|
+
ndattributes_to_xml,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def setup_ndattributes(
|
|
17
|
-
device: NDArrayBaseIO, ndattributes: Sequence[
|
|
17
|
+
device: NDArrayBaseIO, ndattributes: Sequence[NDAttributeParam | NDAttributePv]
|
|
18
18
|
):
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
root,
|
|
26
|
-
"Attribute",
|
|
27
|
-
name=ndattribute.name,
|
|
28
|
-
type="PARAM",
|
|
29
|
-
source=ndattribute.param,
|
|
30
|
-
addr=str(ndattribute.addr),
|
|
31
|
-
datatype=ndattribute.datatype.value,
|
|
32
|
-
description=ndattribute.description,
|
|
33
|
-
)
|
|
34
|
-
elif isinstance(ndattribute, NDAttributePv):
|
|
35
|
-
ET.SubElement(
|
|
36
|
-
root,
|
|
37
|
-
"Attribute",
|
|
38
|
-
name=ndattribute.name,
|
|
39
|
-
type="EPICS_PV",
|
|
40
|
-
source=ndattribute.signal.source.split("ca://")[-1],
|
|
41
|
-
dbrtype=ndattribute.dbrtype.value,
|
|
42
|
-
description=ndattribute.description,
|
|
43
|
-
)
|
|
44
|
-
else:
|
|
45
|
-
raise ValueError(
|
|
46
|
-
f"Invalid type for ndattributes: {type(ndattribute)}. "
|
|
47
|
-
"Expected NDAttributePv or NDAttributeParam."
|
|
48
|
-
)
|
|
49
|
-
xml_text = ET.tostring(root, encoding="unicode")
|
|
50
|
-
yield from bps.abs_set(device.nd_attributes_file, xml_text, wait=True)
|
|
19
|
+
xml = ndattributes_to_xml(ndattributes)
|
|
20
|
+
yield from bps.abs_set(
|
|
21
|
+
device.nd_attributes_file,
|
|
22
|
+
xml,
|
|
23
|
+
wait=True,
|
|
24
|
+
)
|
|
51
25
|
|
|
52
26
|
|
|
53
|
-
def setup_ndstats_sum(detector:
|
|
54
|
-
"""Set up nd stats for a detector."""
|
|
27
|
+
def setup_ndstats_sum(detector: AreaDetector):
|
|
28
|
+
"""Set up nd stats sum nd attribute for a detector."""
|
|
55
29
|
hdf = getattr(detector, "fileio", None)
|
|
56
30
|
if not isinstance(hdf, NDFileHDFIO):
|
|
57
31
|
msg = (
|
|
@@ -13,10 +13,10 @@ from ophyd_async.core import (
|
|
|
13
13
|
Settings,
|
|
14
14
|
SettingsProvider,
|
|
15
15
|
SignalRW,
|
|
16
|
+
Table,
|
|
16
17
|
walk_config_signals,
|
|
17
18
|
walk_rw_signals,
|
|
18
19
|
)
|
|
19
|
-
from ophyd_async.core._table import Table
|
|
20
20
|
|
|
21
21
|
from ._utils import T
|
|
22
22
|
from ._wait_for_awaitable import wait_for_awaitable
|
|
@@ -212,7 +212,7 @@ class AttributeProxy(TangoProxy):
|
|
|
212
212
|
|
|
213
213
|
task = asyncio.create_task(_write())
|
|
214
214
|
await asyncio.wait_for(task, timeout)
|
|
215
|
-
except
|
|
215
|
+
except TimeoutError as te:
|
|
216
216
|
raise TimeoutError(f"{self._name} attr put failed: Timeout") from te
|
|
217
217
|
except DevFailed as de:
|
|
218
218
|
raise RuntimeError(
|
|
@@ -451,7 +451,7 @@ class CommandProxy(TangoProxy):
|
|
|
451
451
|
timestamp=time.time(),
|
|
452
452
|
alarm_severity=0,
|
|
453
453
|
)
|
|
454
|
-
except
|
|
454
|
+
except TimeoutError as te:
|
|
455
455
|
raise TimeoutError(f"{self._name} command failed: Timeout") from te
|
|
456
456
|
except DevFailed as de:
|
|
457
457
|
raise RuntimeError(
|
ophyd_async/testing/_assert.py
CHANGED
|
@@ -21,11 +21,11 @@ from ophyd_async.core import (
|
|
|
21
21
|
from ._utils import T
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def partial_reading(val: Any) ->
|
|
25
|
-
"""Helper function for building expected reading or configuration
|
|
24
|
+
def partial_reading(val: Any) -> Mapping[str, Any]:
|
|
25
|
+
"""Helper function for building expected reading or configuration mapping.
|
|
26
26
|
|
|
27
|
-
:param val: Value to be wrapped in
|
|
28
|
-
:return: The
|
|
27
|
+
:param val: Value to be wrapped in mapping with "value" as the key.
|
|
28
|
+
:return: The mapping that has wrapped the val with key "value".
|
|
29
29
|
"""
|
|
30
30
|
return {"value": val}
|
|
31
31
|
|
|
@@ -100,7 +100,7 @@ def _assert_readings_approx_equal(
|
|
|
100
100
|
|
|
101
101
|
async def assert_configuration(
|
|
102
102
|
configurable: AsyncConfigurable,
|
|
103
|
-
expected_configuration:
|
|
103
|
+
expected_configuration: Mapping[str, Mapping[str, Any]],
|
|
104
104
|
full_match: bool = True,
|
|
105
105
|
) -> None:
|
|
106
106
|
"""Assert that a configurable Device has the given configuration.
|
|
@@ -108,7 +108,7 @@ async def assert_configuration(
|
|
|
108
108
|
:param configurable:
|
|
109
109
|
Device with an async ``read_configuration()`` method to get the
|
|
110
110
|
configuration from.
|
|
111
|
-
:param
|
|
111
|
+
:param expected_configuration: The expected configuration from the configurable.
|
|
112
112
|
:param full_match: if expected_reading keys set is same as actual keys set.
|
|
113
113
|
true: exact match
|
|
114
114
|
false: expected_reading keys is subset of actual reading keys
|
|
@@ -6,6 +6,7 @@ import numpy as np
|
|
|
6
6
|
from ophyd_async.core import (
|
|
7
7
|
Array1D,
|
|
8
8
|
Device,
|
|
9
|
+
DeviceVector,
|
|
9
10
|
DTypeScalar_co,
|
|
10
11
|
SignalRW,
|
|
11
12
|
StandardReadable,
|
|
@@ -15,7 +16,6 @@ from ophyd_async.core import (
|
|
|
15
16
|
soft_signal_rw,
|
|
16
17
|
)
|
|
17
18
|
from ophyd_async.core import StandardReadableFormat as Format
|
|
18
|
-
from ophyd_async.core._device import DeviceVector
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class ExampleEnum(StrictEnum):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ophyd-async
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.1
|
|
4
4
|
Summary: Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango
|
|
5
5
|
Author-email: Tom Cobb <tom.cobb@diamond.ac.uk>
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -35,10 +35,10 @@ License: BSD 3-Clause License
|
|
|
35
35
|
Project-URL: GitHub, https://github.com/bluesky/ophyd-async
|
|
36
36
|
Classifier: Development Status :: 3 - Alpha
|
|
37
37
|
Classifier: License :: OSI Approved :: BSD License
|
|
38
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
39
38
|
Classifier: Programming Language :: Python :: 3.11
|
|
40
39
|
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
-
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Requires-Python: >=3.11
|
|
42
42
|
Description-Content-Type: text/markdown
|
|
43
43
|
License-File: LICENSE
|
|
44
44
|
Requires-Dist: numpy
|
|
@@ -49,6 +49,7 @@ Requires-Dist: colorlog
|
|
|
49
49
|
Requires-Dist: pydantic>=2.0
|
|
50
50
|
Requires-Dist: pydantic-numpy
|
|
51
51
|
Requires-Dist: stamina>=23.1.0
|
|
52
|
+
Requires-Dist: scanspec>=1.0a1
|
|
52
53
|
Provides-Extra: sim
|
|
53
54
|
Requires-Dist: h5py; extra == "sim"
|
|
54
55
|
Provides-Extra: ca
|
|
@@ -56,7 +57,7 @@ Requires-Dist: aioca>=2.0a4; extra == "ca"
|
|
|
56
57
|
Provides-Extra: pva
|
|
57
58
|
Requires-Dist: p4p>=4.2.0; extra == "pva"
|
|
58
59
|
Provides-Extra: tango
|
|
59
|
-
Requires-Dist: pytango==10.0.
|
|
60
|
+
Requires-Dist: pytango==10.0.2; extra == "tango"
|
|
60
61
|
Provides-Extra: demo
|
|
61
62
|
Requires-Dist: ipython; extra == "demo"
|
|
62
63
|
Requires-Dist: matplotlib; extra == "demo"
|