ophyd-async 0.3.4a1__py3-none-any.whl → 0.4.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.
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +20 -8
- ophyd_async/core/_providers.py +186 -24
- ophyd_async/core/detector.py +14 -15
- ophyd_async/core/device.py +18 -6
- ophyd_async/core/signal.py +32 -8
- ophyd_async/core/soft_signal_backend.py +21 -6
- ophyd_async/epics/_backend/_aioca.py +3 -0
- ophyd_async/epics/_backend/_p4p.py +50 -2
- ophyd_async/epics/_backend/common.py +3 -1
- ophyd_async/epics/areadetector/aravis.py +3 -3
- ophyd_async/epics/areadetector/controllers/aravis_controller.py +1 -0
- ophyd_async/epics/areadetector/drivers/ad_base.py +3 -2
- ophyd_async/epics/areadetector/kinetix.py +3 -3
- ophyd_async/epics/areadetector/pilatus.py +3 -3
- ophyd_async/epics/areadetector/vimba.py +3 -3
- ophyd_async/epics/areadetector/writers/__init__.py +2 -2
- ophyd_async/epics/areadetector/writers/general_hdffile.py +97 -0
- ophyd_async/epics/areadetector/writers/hdf_writer.py +27 -10
- ophyd_async/epics/areadetector/writers/nd_file_hdf.py +3 -0
- ophyd_async/epics/areadetector/writers/nd_plugin.py +30 -0
- ophyd_async/epics/demo/demo_ad_sim_detector.py +3 -3
- ophyd_async/epics/motion/motor.py +132 -2
- ophyd_async/panda/__init__.py +15 -1
- ophyd_async/panda/_common_blocks.py +22 -1
- ophyd_async/panda/_hdf_panda.py +5 -3
- ophyd_async/panda/_table.py +20 -18
- ophyd_async/panda/_trigger.py +62 -7
- ophyd_async/panda/writers/_hdf_writer.py +17 -8
- ophyd_async/plan_stubs/ensure_connected.py +7 -2
- ophyd_async/plan_stubs/fly.py +58 -7
- ophyd_async/sim/pattern_generator.py +71 -182
- ophyd_async/sim/sim_pattern_detector_control.py +3 -3
- ophyd_async/sim/sim_pattern_detector_writer.py +9 -5
- ophyd_async/sim/sim_pattern_generator.py +12 -5
- {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.4.0.dist-info}/METADATA +7 -2
- {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.4.0.dist-info}/RECORD +41 -43
- {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.4.0.dist-info}/WHEEL +1 -1
- ophyd_async/epics/areadetector/writers/_hdfdataset.py +0 -10
- ophyd_async/epics/areadetector/writers/_hdffile.py +0 -54
- ophyd_async/panda/writers/_panda_hdf_file.py +0 -54
- {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.4.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.4.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from typing import Optional
|
|
2
3
|
|
|
3
|
-
from bluesky.protocols import Movable, Stoppable
|
|
4
|
+
from bluesky.protocols import Flyable, Movable, Preparable, Stoppable
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
4
6
|
|
|
5
7
|
from ophyd_async.core import (
|
|
6
8
|
ConfigSignal,
|
|
@@ -8,6 +10,7 @@ from ophyd_async.core import (
|
|
|
8
10
|
StandardReadable,
|
|
9
11
|
WatchableAsyncStatus,
|
|
10
12
|
)
|
|
13
|
+
from ophyd_async.core.async_status import AsyncStatus
|
|
11
14
|
from ophyd_async.core.signal import observe_value
|
|
12
15
|
from ophyd_async.core.utils import (
|
|
13
16
|
DEFAULT_TIMEOUT,
|
|
@@ -19,7 +22,39 @@ from ophyd_async.core.utils import (
|
|
|
19
22
|
from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
20
23
|
|
|
21
24
|
|
|
22
|
-
class
|
|
25
|
+
class MotorLimitsException(Exception):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InvalidFlyMotorException(Exception):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
DEFAULT_MOTOR_FLY_TIMEOUT = 60
|
|
34
|
+
DEFAULT_WATCHER_UPDATE_FREQUENCY = 0.2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FlyMotorInfo(BaseModel):
|
|
38
|
+
"""Minimal set of information required to fly a motor:"""
|
|
39
|
+
|
|
40
|
+
#: Absolute position of the motor once it finishes accelerating to desired
|
|
41
|
+
#: velocity, in motor EGUs
|
|
42
|
+
start_position: float = Field(frozen=True)
|
|
43
|
+
|
|
44
|
+
#: Absolute position of the motor once it begins decelerating from desired
|
|
45
|
+
#: velocity, in EGUs
|
|
46
|
+
end_position: float = Field(frozen=True)
|
|
47
|
+
|
|
48
|
+
#: Time taken for the motor to get from start_position to end_position, excluding
|
|
49
|
+
#: run-up and run-down, in seconds.
|
|
50
|
+
time_for_move: float = Field(frozen=True, gt=0)
|
|
51
|
+
|
|
52
|
+
#: Maximum time for the complete motor move, including run up and run down.
|
|
53
|
+
#: Defaults to `time_for_move` + run up and run down times + 10s.
|
|
54
|
+
timeout: CalculatableTimeout = Field(frozen=True, default=CalculateTimeout)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Motor(StandardReadable, Movable, Stoppable, Flyable, Preparable):
|
|
23
58
|
"""Device that moves a motor record"""
|
|
24
59
|
|
|
25
60
|
def __init__(self, prefix: str, name="") -> None:
|
|
@@ -43,6 +78,16 @@ class Motor(StandardReadable, Movable, Stoppable):
|
|
|
43
78
|
self.motor_stop = epics_signal_x(prefix + ".STOP")
|
|
44
79
|
# Whether set() should complete successfully or not
|
|
45
80
|
self._set_success = True
|
|
81
|
+
|
|
82
|
+
# end_position of a fly move, with run_up_distance added on.
|
|
83
|
+
self._fly_completed_position: Optional[float] = None
|
|
84
|
+
|
|
85
|
+
# Set on kickoff(), complete when motor reaches self._fly_completed_position
|
|
86
|
+
self._fly_status: Optional[WatchableAsyncStatus] = None
|
|
87
|
+
|
|
88
|
+
# Set during prepare
|
|
89
|
+
self._fly_timeout: Optional[CalculatableTimeout] = CalculateTimeout
|
|
90
|
+
|
|
46
91
|
super().__init__(name=name)
|
|
47
92
|
|
|
48
93
|
def set_name(self, name: str):
|
|
@@ -50,6 +95,44 @@ class Motor(StandardReadable, Movable, Stoppable):
|
|
|
50
95
|
# Readback should be named the same as its parent in read()
|
|
51
96
|
self.user_readback.set_name(name)
|
|
52
97
|
|
|
98
|
+
@AsyncStatus.wrap
|
|
99
|
+
async def prepare(self, value: FlyMotorInfo):
|
|
100
|
+
"""Calculate required velocity and run-up distance, then if motor limits aren't
|
|
101
|
+
breached, move to start position minus run-up distance"""
|
|
102
|
+
|
|
103
|
+
self._fly_timeout = value.timeout
|
|
104
|
+
|
|
105
|
+
# Velocity, at which motor travels from start_position to end_position, in motor
|
|
106
|
+
# egu/s.
|
|
107
|
+
fly_velocity = await self._prepare_velocity(
|
|
108
|
+
value.start_position,
|
|
109
|
+
value.end_position,
|
|
110
|
+
value.time_for_move,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# start_position with run_up_distance added on.
|
|
114
|
+
fly_prepared_position = await self._prepare_motor_path(
|
|
115
|
+
abs(fly_velocity), value.start_position, value.end_position
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
await self.set(fly_prepared_position)
|
|
119
|
+
|
|
120
|
+
@AsyncStatus.wrap
|
|
121
|
+
async def kickoff(self):
|
|
122
|
+
"""Begin moving motor from prepared position to final position."""
|
|
123
|
+
assert (
|
|
124
|
+
self._fly_completed_position
|
|
125
|
+
), "Motor must be prepared before attempting to kickoff"
|
|
126
|
+
|
|
127
|
+
self._fly_status = self.set(
|
|
128
|
+
self._fly_completed_position, timeout=self._fly_timeout
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def complete(self) -> WatchableAsyncStatus:
|
|
132
|
+
"""Mark as complete once motor reaches completed position."""
|
|
133
|
+
assert self._fly_status, "kickoff not called"
|
|
134
|
+
return self._fly_status
|
|
135
|
+
|
|
53
136
|
@WatchableAsyncStatus.wrap
|
|
54
137
|
async def set(
|
|
55
138
|
self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
|
|
@@ -95,3 +178,50 @@ class Motor(StandardReadable, Movable, Stoppable):
|
|
|
95
178
|
# Put with completion will never complete as we are waiting for completion on
|
|
96
179
|
# the move above, so need to pass wait=False
|
|
97
180
|
await self.motor_stop.trigger(wait=False)
|
|
181
|
+
|
|
182
|
+
async def _prepare_velocity(
|
|
183
|
+
self, start_position: float, end_position: float, time_for_move: float
|
|
184
|
+
) -> float:
|
|
185
|
+
fly_velocity = (start_position - end_position) / time_for_move
|
|
186
|
+
max_speed, egu = await asyncio.gather(
|
|
187
|
+
self.max_velocity.get_value(), self.motor_egu.get_value()
|
|
188
|
+
)
|
|
189
|
+
if abs(fly_velocity) > max_speed:
|
|
190
|
+
raise MotorLimitsException(
|
|
191
|
+
f"Motor speed of {abs(fly_velocity)} {egu}/s was requested for a motor "
|
|
192
|
+
f" with max speed of {max_speed} {egu}/s"
|
|
193
|
+
)
|
|
194
|
+
await self.velocity.set(abs(fly_velocity))
|
|
195
|
+
return fly_velocity
|
|
196
|
+
|
|
197
|
+
async def _prepare_motor_path(
|
|
198
|
+
self, fly_velocity: float, start_position: float, end_position: float
|
|
199
|
+
) -> float:
|
|
200
|
+
# Distance required for motor to accelerate from stationary to fly_velocity, and
|
|
201
|
+
# distance required for motor to decelerate from fly_velocity to stationary
|
|
202
|
+
run_up_distance = (await self.acceleration_time.get_value()) * fly_velocity
|
|
203
|
+
|
|
204
|
+
self._fly_completed_position = end_position + run_up_distance
|
|
205
|
+
|
|
206
|
+
# Prepared position not used after prepare, so no need to store in self
|
|
207
|
+
fly_prepared_position = start_position - run_up_distance
|
|
208
|
+
|
|
209
|
+
motor_lower_limit, motor_upper_limit, egu = await asyncio.gather(
|
|
210
|
+
self.low_limit_travel.get_value(),
|
|
211
|
+
self.high_limit_travel.get_value(),
|
|
212
|
+
self.motor_egu.get_value(),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if (
|
|
216
|
+
not motor_upper_limit >= fly_prepared_position >= motor_lower_limit
|
|
217
|
+
or not motor_upper_limit
|
|
218
|
+
>= self._fly_completed_position
|
|
219
|
+
>= motor_lower_limit
|
|
220
|
+
):
|
|
221
|
+
raise MotorLimitsException(
|
|
222
|
+
f"Motor trajectory for requested fly is from "
|
|
223
|
+
f"{fly_prepared_position}{egu} to "
|
|
224
|
+
f"{self._fly_completed_position}{egu} but motor limits are "
|
|
225
|
+
f"{motor_lower_limit}{egu} <= x <= {motor_upper_limit}{egu} "
|
|
226
|
+
)
|
|
227
|
+
return fly_prepared_position
|
ophyd_async/panda/__init__.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from ._common_blocks import (
|
|
2
2
|
CommonPandaBlocks,
|
|
3
3
|
DataBlock,
|
|
4
|
+
EnableDisableOptions,
|
|
4
5
|
PcapBlock,
|
|
6
|
+
PcompBlock,
|
|
7
|
+
PcompDirectionOptions,
|
|
5
8
|
PulseBlock,
|
|
6
9
|
SeqBlock,
|
|
7
10
|
TimeUnits,
|
|
@@ -15,17 +18,27 @@ from ._table import (
|
|
|
15
18
|
seq_table_from_arrays,
|
|
16
19
|
seq_table_from_rows,
|
|
17
20
|
)
|
|
18
|
-
from ._trigger import
|
|
21
|
+
from ._trigger import (
|
|
22
|
+
PcompInfo,
|
|
23
|
+
SeqTableInfo,
|
|
24
|
+
StaticPcompTriggerLogic,
|
|
25
|
+
StaticSeqTableTriggerLogic,
|
|
26
|
+
)
|
|
19
27
|
from ._utils import phase_sorter
|
|
20
28
|
|
|
21
29
|
__all__ = [
|
|
22
30
|
"CommonPandaBlocks",
|
|
23
31
|
"HDFPanda",
|
|
32
|
+
"PcompBlock",
|
|
33
|
+
"PcompInfo",
|
|
34
|
+
"PcompDirectionOptions",
|
|
35
|
+
"EnableDisableOptions",
|
|
24
36
|
"PcapBlock",
|
|
25
37
|
"PulseBlock",
|
|
26
38
|
"seq_table_from_arrays",
|
|
27
39
|
"seq_table_from_rows",
|
|
28
40
|
"SeqBlock",
|
|
41
|
+
"SeqTableInfo",
|
|
29
42
|
"SeqTable",
|
|
30
43
|
"SeqTableRow",
|
|
31
44
|
"SeqTrigger",
|
|
@@ -35,4 +48,5 @@ __all__ = [
|
|
|
35
48
|
"DataBlock",
|
|
36
49
|
"CommonPandABlocks",
|
|
37
50
|
"StaticSeqTableTriggerLogic",
|
|
51
|
+
"StaticPcompTriggerLogic",
|
|
38
52
|
]
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
|
|
5
5
|
from ophyd_async.core import Device, DeviceVector, SignalR, SignalRW
|
|
6
|
+
from ophyd_async.core.signal_backend import SubsetEnum
|
|
6
7
|
from ophyd_async.panda._table import DatasetTable, SeqTable
|
|
7
8
|
|
|
8
9
|
|
|
@@ -22,6 +23,25 @@ class PulseBlock(Device):
|
|
|
22
23
|
width: SignalRW[float]
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
class PcompDirectionOptions(str, Enum):
|
|
27
|
+
positive = "Positive"
|
|
28
|
+
negative = "Negative"
|
|
29
|
+
either = "Either"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
EnableDisableOptions = SubsetEnum["ZERO", "ONE"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PcompBlock(Device):
|
|
36
|
+
active: SignalR[bool]
|
|
37
|
+
dir: SignalRW[PcompDirectionOptions]
|
|
38
|
+
enable: SignalRW[EnableDisableOptions]
|
|
39
|
+
pulses: SignalRW[int]
|
|
40
|
+
start: SignalRW[int]
|
|
41
|
+
step: SignalRW[int]
|
|
42
|
+
width: SignalRW[int]
|
|
43
|
+
|
|
44
|
+
|
|
25
45
|
class TimeUnits(str, Enum):
|
|
26
46
|
min = "min"
|
|
27
47
|
s = "s"
|
|
@@ -35,7 +55,7 @@ class SeqBlock(Device):
|
|
|
35
55
|
repeats: SignalRW[int]
|
|
36
56
|
prescale: SignalRW[float]
|
|
37
57
|
prescale_units: SignalRW[TimeUnits]
|
|
38
|
-
enable: SignalRW[
|
|
58
|
+
enable: SignalRW[EnableDisableOptions]
|
|
39
59
|
|
|
40
60
|
|
|
41
61
|
class PcapBlock(Device):
|
|
@@ -46,5 +66,6 @@ class PcapBlock(Device):
|
|
|
46
66
|
class CommonPandaBlocks(Device):
|
|
47
67
|
pulse: DeviceVector[PulseBlock]
|
|
48
68
|
seq: DeviceVector[SeqBlock]
|
|
69
|
+
pcomp: DeviceVector[PcompBlock]
|
|
49
70
|
pcap: PcapBlock
|
|
50
71
|
data: DataBlock
|
ophyd_async/panda/_hdf_panda.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Sequence
|
|
|
4
4
|
|
|
5
5
|
from ophyd_async.core import (
|
|
6
6
|
DEFAULT_TIMEOUT,
|
|
7
|
-
|
|
7
|
+
PathProvider,
|
|
8
8
|
SignalR,
|
|
9
9
|
StandardDetector,
|
|
10
10
|
)
|
|
@@ -19,7 +19,7 @@ class HDFPanda(CommonPandaBlocks, StandardDetector):
|
|
|
19
19
|
def __init__(
|
|
20
20
|
self,
|
|
21
21
|
prefix: str,
|
|
22
|
-
|
|
22
|
+
path_provider: PathProvider,
|
|
23
23
|
config_sigs: Sequence[SignalR] = (),
|
|
24
24
|
name: str = "",
|
|
25
25
|
):
|
|
@@ -28,7 +28,9 @@ class HDFPanda(CommonPandaBlocks, StandardDetector):
|
|
|
28
28
|
create_children_from_annotations(self)
|
|
29
29
|
controller = PandaPcapController(pcap=self.pcap)
|
|
30
30
|
writer = PandaHDFWriter(
|
|
31
|
-
|
|
31
|
+
prefix=prefix,
|
|
32
|
+
path_provider=path_provider,
|
|
33
|
+
name_provider=lambda: name,
|
|
32
34
|
panda_device=self,
|
|
33
35
|
)
|
|
34
36
|
super().__init__(
|
ophyd_async/panda/_table.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from typing import Optional, Sequence, Type,
|
|
3
|
+
from typing import Optional, Sequence, Type, TypeVar
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
import numpy.typing as npt
|
|
7
|
+
import pydantic_numpy.typing as pnd
|
|
8
|
+
from typing_extensions import NotRequired, TypedDict
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class PandaHdf5DatasetType(str, Enum):
|
|
@@ -54,23 +56,23 @@ class SeqTableRow:
|
|
|
54
56
|
|
|
55
57
|
|
|
56
58
|
class SeqTable(TypedDict):
|
|
57
|
-
repeats:
|
|
58
|
-
trigger: Sequence[SeqTrigger]
|
|
59
|
-
position:
|
|
60
|
-
time1:
|
|
61
|
-
outa1:
|
|
62
|
-
outb1:
|
|
63
|
-
outc1:
|
|
64
|
-
outd1:
|
|
65
|
-
oute1:
|
|
66
|
-
outf1:
|
|
67
|
-
time2:
|
|
68
|
-
outa2:
|
|
69
|
-
outb2:
|
|
70
|
-
outc2:
|
|
71
|
-
outd2:
|
|
72
|
-
oute2:
|
|
73
|
-
outf2:
|
|
59
|
+
repeats: NotRequired[pnd.Np1DArrayUint16]
|
|
60
|
+
trigger: NotRequired[Sequence[SeqTrigger]]
|
|
61
|
+
position: NotRequired[pnd.Np1DArrayInt32]
|
|
62
|
+
time1: NotRequired[pnd.Np1DArrayUint32]
|
|
63
|
+
outa1: NotRequired[pnd.Np1DArrayBool]
|
|
64
|
+
outb1: NotRequired[pnd.Np1DArrayBool]
|
|
65
|
+
outc1: NotRequired[pnd.Np1DArrayBool]
|
|
66
|
+
outd1: NotRequired[pnd.Np1DArrayBool]
|
|
67
|
+
oute1: NotRequired[pnd.Np1DArrayBool]
|
|
68
|
+
outf1: NotRequired[pnd.Np1DArrayBool]
|
|
69
|
+
time2: NotRequired[pnd.Np1DArrayUint32]
|
|
70
|
+
outa2: NotRequired[pnd.Np1DArrayBool]
|
|
71
|
+
outb2: NotRequired[pnd.Np1DArrayBool]
|
|
72
|
+
outc2: NotRequired[pnd.Np1DArrayBool]
|
|
73
|
+
outd2: NotRequired[pnd.Np1DArrayBool]
|
|
74
|
+
oute2: NotRequired[pnd.Np1DArrayBool]
|
|
75
|
+
outf2: NotRequired[pnd.Np1DArrayBool]
|
|
74
76
|
|
|
75
77
|
|
|
76
78
|
def seq_table_from_rows(*rows: SeqTableRow):
|
ophyd_async/panda/_trigger.py
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
3
5
|
|
|
4
6
|
from ophyd_async.core import TriggerLogic, wait_for_value
|
|
5
|
-
from ophyd_async.panda import
|
|
7
|
+
from ophyd_async.panda import (
|
|
8
|
+
PcompBlock,
|
|
9
|
+
PcompDirectionOptions,
|
|
10
|
+
SeqBlock,
|
|
11
|
+
SeqTable,
|
|
12
|
+
TimeUnits,
|
|
13
|
+
)
|
|
6
14
|
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
prescale_as_us: float = 1 # microseconds
|
|
16
|
+
class SeqTableInfo(BaseModel):
|
|
17
|
+
sequence_table: SeqTable = Field(strict=True)
|
|
18
|
+
repeats: int = Field(ge=0)
|
|
19
|
+
prescale_as_us: float = Field(default=1, ge=0) # microseconds
|
|
13
20
|
|
|
14
21
|
|
|
15
22
|
class StaticSeqTableTriggerLogic(TriggerLogic[SeqTableInfo]):
|
|
@@ -37,3 +44,51 @@ class StaticSeqTableTriggerLogic(TriggerLogic[SeqTableInfo]):
|
|
|
37
44
|
async def stop(self):
|
|
38
45
|
await self.seq.enable.set("ZERO")
|
|
39
46
|
await wait_for_value(self.seq.active, False, timeout=1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PcompInfo(BaseModel):
|
|
50
|
+
start_postion: int = Field(description="start position in counts")
|
|
51
|
+
pulse_width: int = Field(description="width of a single pulse in counts", gt=0)
|
|
52
|
+
rising_edge_step: int = Field(
|
|
53
|
+
description="step between rising edges of pulses in counts", gt=0
|
|
54
|
+
) #
|
|
55
|
+
number_of_pulses: int = Field(
|
|
56
|
+
description=(
|
|
57
|
+
"Number of pulses to send before the PCOMP block is disarmed. "
|
|
58
|
+
"0 means infinite."
|
|
59
|
+
),
|
|
60
|
+
ge=0,
|
|
61
|
+
)
|
|
62
|
+
direction: PcompDirectionOptions = Field(
|
|
63
|
+
description=(
|
|
64
|
+
"Specifies which direction the motor counts should be "
|
|
65
|
+
"moving. Pulses won't be sent unless the values are moving in "
|
|
66
|
+
"this direction."
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class StaticPcompTriggerLogic(TriggerLogic[PcompInfo]):
|
|
72
|
+
def __init__(self, pcomp: PcompBlock) -> None:
|
|
73
|
+
self.pcomp = pcomp
|
|
74
|
+
|
|
75
|
+
async def prepare(self, value: PcompInfo):
|
|
76
|
+
await self.pcomp.enable.set("ZERO")
|
|
77
|
+
await asyncio.gather(
|
|
78
|
+
self.pcomp.start.set(value.start_postion),
|
|
79
|
+
self.pcomp.width.set(value.pulse_width),
|
|
80
|
+
self.pcomp.step.set(value.rising_edge_step),
|
|
81
|
+
self.pcomp.pulses.set(value.number_of_pulses),
|
|
82
|
+
self.pcomp.dir.set(value.direction),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
async def kickoff(self) -> None:
|
|
86
|
+
await self.pcomp.enable.set("ONE")
|
|
87
|
+
await wait_for_value(self.pcomp.active, True, timeout=1)
|
|
88
|
+
|
|
89
|
+
async def complete(self, timeout: Optional[float] = None) -> None:
|
|
90
|
+
await wait_for_value(self.pcomp.active, False, timeout=timeout)
|
|
91
|
+
|
|
92
|
+
async def stop(self):
|
|
93
|
+
await self.pcomp.enable.set("ZERO")
|
|
94
|
+
await wait_for_value(self.pcomp.active, False, timeout=1)
|
|
@@ -8,13 +8,14 @@ from p4p.client.thread import Context
|
|
|
8
8
|
from ophyd_async.core import (
|
|
9
9
|
DEFAULT_TIMEOUT,
|
|
10
10
|
DetectorWriter,
|
|
11
|
-
|
|
11
|
+
NameProvider,
|
|
12
|
+
PathProvider,
|
|
12
13
|
wait_for_value,
|
|
13
14
|
)
|
|
14
15
|
from ophyd_async.core.signal import observe_value
|
|
16
|
+
from ophyd_async.epics.areadetector.writers.general_hdffile import _HDFDataset, _HDFFile
|
|
15
17
|
|
|
16
18
|
from .._common_blocks import CommonPandaBlocks
|
|
17
|
-
from ._panda_hdf_file import _HDFDataset, _HDFFile
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class PandaHDFWriter(DetectorWriter):
|
|
@@ -22,11 +23,15 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
22
23
|
|
|
23
24
|
def __init__(
|
|
24
25
|
self,
|
|
25
|
-
|
|
26
|
+
prefix: str,
|
|
27
|
+
path_provider: PathProvider,
|
|
28
|
+
name_provider: NameProvider,
|
|
26
29
|
panda_device: CommonPandaBlocks,
|
|
27
30
|
) -> None:
|
|
28
31
|
self.panda_device = panda_device
|
|
29
|
-
self.
|
|
32
|
+
self._prefix = prefix
|
|
33
|
+
self._path_provider = path_provider
|
|
34
|
+
self._name_provider = name_provider
|
|
30
35
|
self._datasets: List[_HDFDataset] = []
|
|
31
36
|
self._file: Optional[_HDFFile] = None
|
|
32
37
|
self._multiplier = 1
|
|
@@ -39,16 +44,18 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
39
44
|
await self.panda_device.data.flush_period.set(0)
|
|
40
45
|
|
|
41
46
|
self._file = None
|
|
42
|
-
info = self.
|
|
47
|
+
info = self._path_provider(device_name=self.panda_device.name)
|
|
43
48
|
# Set the initial values
|
|
44
49
|
await asyncio.gather(
|
|
45
50
|
self.panda_device.data.hdf_directory.set(
|
|
46
51
|
str(info.root / info.resource_dir)
|
|
47
52
|
),
|
|
48
53
|
self.panda_device.data.hdf_file_name.set(
|
|
49
|
-
f"{info.
|
|
54
|
+
f"{info.filename}.h5",
|
|
50
55
|
),
|
|
51
56
|
self.panda_device.data.num_capture.set(0),
|
|
57
|
+
# TODO: Set create_dir_depth once available
|
|
58
|
+
# https://github.com/bluesky/ophyd-async/issues/317
|
|
52
59
|
)
|
|
53
60
|
|
|
54
61
|
# Wait for it to start, stashing the status that tells us when it finishes
|
|
@@ -71,6 +78,7 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
71
78
|
source=self.panda_device.data.hdf_directory.source,
|
|
72
79
|
shape=ds.shape,
|
|
73
80
|
dtype="array" if ds.shape != [1] else "number",
|
|
81
|
+
dtype_numpy="<f8", # PandA data should always be written as Float64
|
|
74
82
|
external="STREAM:",
|
|
75
83
|
)
|
|
76
84
|
for ds in self._datasets
|
|
@@ -121,8 +129,9 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
121
129
|
if indices_written:
|
|
122
130
|
if not self._file:
|
|
123
131
|
self._file = _HDFFile(
|
|
124
|
-
self.
|
|
125
|
-
Path(await self.panda_device.data.
|
|
132
|
+
self._path_provider(),
|
|
133
|
+
Path(await self.panda_device.data.hdf_directory.get_value())
|
|
134
|
+
/ Path(await self.panda_device.data.hdf_file_name.get_value()),
|
|
126
135
|
self._datasets,
|
|
127
136
|
)
|
|
128
137
|
for doc in self._file.stream_resources():
|
|
@@ -10,13 +10,18 @@ def ensure_connected(
|
|
|
10
10
|
timeout: float = DEFAULT_TIMEOUT,
|
|
11
11
|
force_reconnect=False,
|
|
12
12
|
):
|
|
13
|
-
yield from bps.wait_for(
|
|
13
|
+
(connect_task,) = yield from bps.wait_for(
|
|
14
14
|
[
|
|
15
15
|
lambda: wait_for_connection(
|
|
16
16
|
**{
|
|
17
|
-
device.name: device.connect(
|
|
17
|
+
device.name: device.connect(
|
|
18
|
+
mock=mock, timeout=timeout, force_reconnect=force_reconnect
|
|
19
|
+
)
|
|
18
20
|
for device in devices
|
|
19
21
|
}
|
|
20
22
|
)
|
|
21
23
|
]
|
|
22
24
|
)
|
|
25
|
+
|
|
26
|
+
if connect_task and connect_task.exception() is not None:
|
|
27
|
+
raise connect_task.exception()
|
ophyd_async/plan_stubs/fly.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List
|
|
1
|
+
from typing import List, Optional
|
|
2
2
|
|
|
3
3
|
import bluesky.plan_stubs as bps
|
|
4
4
|
from bluesky.utils import short_uid
|
|
@@ -6,8 +6,33 @@ from bluesky.utils import short_uid
|
|
|
6
6
|
from ophyd_async.core.detector import DetectorTrigger, StandardDetector, TriggerInfo
|
|
7
7
|
from ophyd_async.core.flyer import HardwareTriggeredFlyable
|
|
8
8
|
from ophyd_async.core.utils import in_micros
|
|
9
|
-
from ophyd_async.panda
|
|
10
|
-
|
|
9
|
+
from ophyd_async.panda import (
|
|
10
|
+
PcompDirectionOptions,
|
|
11
|
+
PcompInfo,
|
|
12
|
+
SeqTable,
|
|
13
|
+
SeqTableInfo,
|
|
14
|
+
SeqTableRow,
|
|
15
|
+
seq_table_from_rows,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def prepare_static_pcomp_flyer_and_detectors(
|
|
20
|
+
flyer: HardwareTriggeredFlyable[PcompInfo],
|
|
21
|
+
detectors: List[StandardDetector],
|
|
22
|
+
pcomp_info: PcompInfo,
|
|
23
|
+
trigger_info: TriggerInfo,
|
|
24
|
+
):
|
|
25
|
+
"""Prepare a hardware triggered flyable and one or more detectors.
|
|
26
|
+
|
|
27
|
+
Prepare a hardware triggered flyable and one or more detectors with the
|
|
28
|
+
same trigger.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
for det in detectors:
|
|
33
|
+
yield from bps.prepare(det, trigger_info, wait=False, group="prep")
|
|
34
|
+
yield from bps.prepare(flyer, pcomp_info, wait=False, group="prep")
|
|
35
|
+
yield from bps.wait(group="prep")
|
|
11
36
|
|
|
12
37
|
|
|
13
38
|
def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
@@ -18,7 +43,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
|
18
43
|
shutter_time: float,
|
|
19
44
|
repeats: int = 1,
|
|
20
45
|
period: float = 0.0,
|
|
21
|
-
frame_timeout: float
|
|
46
|
+
frame_timeout: Optional[float] = None,
|
|
22
47
|
):
|
|
23
48
|
"""Prepare a hardware triggered flyable and one or more detectors.
|
|
24
49
|
|
|
@@ -36,7 +61,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
|
36
61
|
deadtime = max(det.controller.get_deadtime(exposure) for det in detectors)
|
|
37
62
|
|
|
38
63
|
trigger_info = TriggerInfo(
|
|
39
|
-
|
|
64
|
+
number=number_of_frames * repeats,
|
|
40
65
|
trigger=DetectorTrigger.constant_gate,
|
|
41
66
|
deadtime=deadtime,
|
|
42
67
|
livetime=exposure,
|
|
@@ -65,7 +90,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
|
65
90
|
SeqTableRow(time2=in_micros(shutter_time)),
|
|
66
91
|
)
|
|
67
92
|
|
|
68
|
-
table_info = SeqTableInfo(table, repeats)
|
|
93
|
+
table_info = SeqTableInfo(sequence_table=table, repeats=repeats)
|
|
69
94
|
|
|
70
95
|
for det in detectors:
|
|
71
96
|
yield from bps.prepare(det, trigger_info, wait=False, group="prep")
|
|
@@ -75,7 +100,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
|
75
100
|
|
|
76
101
|
def fly_and_collect(
|
|
77
102
|
stream_name: str,
|
|
78
|
-
flyer: HardwareTriggeredFlyable[SeqTableInfo],
|
|
103
|
+
flyer: HardwareTriggeredFlyable[SeqTableInfo] | HardwareTriggeredFlyable[PcompInfo],
|
|
79
104
|
detectors: List[StandardDetector],
|
|
80
105
|
):
|
|
81
106
|
"""Kickoff, complete and collect with a flyer and multiple detectors.
|
|
@@ -113,6 +138,32 @@ def fly_and_collect(
|
|
|
113
138
|
yield from bps.wait(group=group)
|
|
114
139
|
|
|
115
140
|
|
|
141
|
+
def fly_and_collect_with_static_pcomp(
|
|
142
|
+
stream_name: str,
|
|
143
|
+
flyer: HardwareTriggeredFlyable[PcompInfo],
|
|
144
|
+
detectors: List[StandardDetector],
|
|
145
|
+
number_of_pulses: int,
|
|
146
|
+
pulse_width: int,
|
|
147
|
+
rising_edge_step: int,
|
|
148
|
+
direction: PcompDirectionOptions,
|
|
149
|
+
trigger_info: TriggerInfo,
|
|
150
|
+
):
|
|
151
|
+
# Set up scan and prepare trigger
|
|
152
|
+
pcomp_info = PcompInfo(
|
|
153
|
+
start_postion=0,
|
|
154
|
+
pulse_width=pulse_width,
|
|
155
|
+
rising_edge_step=rising_edge_step,
|
|
156
|
+
number_of_pulses=number_of_pulses,
|
|
157
|
+
direction=direction,
|
|
158
|
+
)
|
|
159
|
+
yield from prepare_static_pcomp_flyer_and_detectors(
|
|
160
|
+
flyer, detectors, pcomp_info, trigger_info
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Run the fly scan
|
|
164
|
+
yield from fly_and_collect(stream_name, flyer, detectors)
|
|
165
|
+
|
|
166
|
+
|
|
116
167
|
def time_resolved_fly_and_collect_with_static_seq_table(
|
|
117
168
|
stream_name: str,
|
|
118
169
|
flyer: HardwareTriggeredFlyable[SeqTableInfo],
|