ophyd-async 0.2.0__py3-none-any.whl → 0.3a1__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.
Files changed (32) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +5 -9
  3. ophyd_async/core/_providers.py +36 -5
  4. ophyd_async/core/async_status.py +3 -3
  5. ophyd_async/core/detector.py +159 -37
  6. ophyd_async/core/device.py +37 -38
  7. ophyd_async/core/device_save_loader.py +96 -23
  8. ophyd_async/core/flyer.py +32 -237
  9. ophyd_async/core/signal.py +11 -4
  10. ophyd_async/core/signal_backend.py +2 -2
  11. ophyd_async/core/sim_signal_backend.py +2 -2
  12. ophyd_async/core/utils.py +75 -29
  13. ophyd_async/epics/_backend/_aioca.py +18 -26
  14. ophyd_async/epics/_backend/_p4p.py +58 -27
  15. ophyd_async/epics/_backend/common.py +20 -0
  16. ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +1 -1
  17. ophyd_async/epics/areadetector/controllers/pilatus_controller.py +1 -1
  18. ophyd_async/epics/areadetector/writers/_hdffile.py +17 -3
  19. ophyd_async/epics/areadetector/writers/hdf_writer.py +21 -15
  20. ophyd_async/epics/pvi.py +70 -0
  21. ophyd_async/epics/signal/__init__.py +0 -2
  22. ophyd_async/panda/__init__.py +5 -2
  23. ophyd_async/panda/panda.py +41 -94
  24. ophyd_async/panda/panda_controller.py +41 -0
  25. ophyd_async/panda/utils.py +15 -0
  26. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/METADATA +2 -2
  27. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/RECORD +31 -28
  28. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/WHEEL +1 -1
  29. ophyd_async/epics/signal/pvi_get.py +0 -22
  30. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/LICENSE +0 -0
  31. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/entry_points.txt +0 -0
  32. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.2.0'
16
- __version_tuple__ = version_tuple = (0, 2, 0)
15
+ __version__ = version = '0.3a1'
16
+ __version_tuple__ = version_tuple = (0, 3)
@@ -10,18 +10,14 @@ from .detector import DetectorControl, DetectorTrigger, DetectorWriter, Standard
10
10
  from .device import Device, DeviceCollector, DeviceVector
11
11
  from .device_save_loader import (
12
12
  get_signal_values,
13
+ load_device,
13
14
  load_from_yaml,
15
+ save_device,
14
16
  save_to_yaml,
15
17
  set_signal_values,
16
18
  walk_rw_signals,
17
19
  )
18
- from .flyer import (
19
- DetectorGroupLogic,
20
- HardwareTriggeredFlyable,
21
- SameTriggerDetectorGroupLogic,
22
- TriggerInfo,
23
- TriggerLogic,
24
- )
20
+ from .flyer import HardwareTriggeredFlyable, TriggerInfo, TriggerLogic
25
21
  from .signal import (
26
22
  Signal,
27
23
  SignalR,
@@ -79,8 +75,6 @@ __all__ = [
79
75
  "StaticDirectoryProvider",
80
76
  "StandardReadable",
81
77
  "TriggerInfo",
82
- "DetectorGroupLogic",
83
- "SameTriggerDetectorGroupLogic",
84
78
  "TriggerLogic",
85
79
  "HardwareTriggeredFlyable",
86
80
  "DEFAULT_TIMEOUT",
@@ -97,4 +91,6 @@ __all__ = [
97
91
  "save_to_yaml",
98
92
  "set_signal_values",
99
93
  "walk_rw_signals",
94
+ "load_device",
95
+ "save_device",
100
96
  ]
@@ -1,12 +1,30 @@
1
1
  from abc import abstractmethod
2
2
  from dataclasses import dataclass
3
- from typing import Protocol, Sequence
3
+ from pathlib import Path
4
+ from typing import Optional, Protocol, Sequence, Union
4
5
 
5
6
 
6
7
  @dataclass
7
8
  class DirectoryInfo:
8
- directory_path: str
9
- filename_prefix: str
9
+ """
10
+ Information about where and how to write a file.
11
+
12
+ The bluesky event model splits the URI for a resource into two segments to aid in
13
+ different applications mounting filesystems at different mount points.
14
+ The portion of this path which is relevant only for the writer is the 'root',
15
+ while the path from an agreed upon mutual mounting is the resource_path.
16
+ The resource_dir is used with the filename to construct the resource_path.
17
+
18
+ :param root: Path of a root directory, relevant only for the file writer
19
+ :param resource_dir: Directory into which files should be written, relative to root
20
+ :param prefix: Optional filename prefix to add to all files
21
+ :param suffix: Optional filename suffix to add to all files
22
+ """
23
+
24
+ root: Path
25
+ resource_dir: Path
26
+ prefix: Optional[str] = ""
27
+ suffix: Optional[str] = ""
10
28
 
11
29
 
12
30
  class DirectoryProvider(Protocol):
@@ -16,8 +34,21 @@ class DirectoryProvider(Protocol):
16
34
 
17
35
 
18
36
  class StaticDirectoryProvider(DirectoryProvider):
19
- def __init__(self, directory_path: str, filename_prefix: str) -> None:
20
- self._directory_info = DirectoryInfo(directory_path, filename_prefix)
37
+ def __init__(
38
+ self,
39
+ directory_path: Union[str, Path],
40
+ filename_prefix: str = "",
41
+ filename_suffix: str = "",
42
+ resource_dir: Path = Path("."),
43
+ ) -> None:
44
+ if isinstance(directory_path, str):
45
+ directory_path = Path(directory_path)
46
+ self._directory_info = DirectoryInfo(
47
+ root=directory_path,
48
+ resource_dir=resource_dir,
49
+ prefix=filename_prefix,
50
+ suffix=filename_suffix,
51
+ )
21
52
 
22
53
  def __call__(self) -> DirectoryInfo:
23
54
  return self._directory_info
@@ -85,12 +85,12 @@ class AsyncStatus(Status):
85
85
 
86
86
  def __repr__(self) -> str:
87
87
  if self.done:
88
- if self.exception() is not None:
89
- status = "errored"
88
+ if e := self.exception():
89
+ status = f"errored: {repr(e)}"
90
90
  else:
91
91
  status = "done"
92
92
  else:
93
93
  status = "pending"
94
- return f"<{type(self).__name__} {status}>"
94
+ return f"<{type(self).__name__}, task: {self.task.get_coro()}, {status}>"
95
95
 
96
96
  __str__ = __repr__
@@ -1,18 +1,33 @@
1
1
  """Module which defines abstract classes to work with detectors"""
2
+
2
3
  import asyncio
4
+ import time
3
5
  from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass
4
7
  from enum import Enum
5
- from typing import AsyncIterator, Dict, Optional, Sequence
8
+ from typing import (
9
+ AsyncGenerator,
10
+ AsyncIterator,
11
+ Callable,
12
+ Dict,
13
+ List,
14
+ Optional,
15
+ Sequence,
16
+ TypeVar,
17
+ )
6
18
 
7
19
  from bluesky.protocols import (
8
- Asset,
20
+ Collectable,
9
21
  Configurable,
10
22
  Descriptor,
23
+ Flyable,
24
+ Preparable,
11
25
  Readable,
12
26
  Reading,
13
27
  Stageable,
28
+ StreamAsset,
14
29
  Triggerable,
15
- WritesExternalAssets,
30
+ WritesStreamAssets,
16
31
  )
17
32
 
18
33
  from .async_status import AsyncStatus
@@ -20,6 +35,8 @@ from .device import Device
20
35
  from .signal import SignalR
21
36
  from .utils import DEFAULT_TIMEOUT, merge_gathered_dicts
22
37
 
38
+ T = TypeVar("T")
39
+
23
40
 
24
41
  class DetectorTrigger(str, Enum):
25
42
  #: Detector generates internal trigger for given rate
@@ -32,6 +49,18 @@ class DetectorTrigger(str, Enum):
32
49
  variable_gate = "variable_gate"
33
50
 
34
51
 
52
+ @dataclass(frozen=True)
53
+ class TriggerInfo:
54
+ #: Number of triggers that will be sent
55
+ num: int
56
+ #: Sort of triggers that will be sent
57
+ trigger: DetectorTrigger
58
+ #: What is the minimum deadtime between triggers
59
+ deadtime: float
60
+ #: What is the maximum high time of the triggers
61
+ livetime: float
62
+
63
+
35
64
  class DetectorControl(ABC):
36
65
  @abstractmethod
37
66
  def get_deadtime(self, exposure: float) -> float:
@@ -40,8 +69,8 @@ class DetectorControl(ABC):
40
69
  @abstractmethod
41
70
  async def arm(
42
71
  self,
72
+ num: int,
43
73
  trigger: DetectorTrigger = DetectorTrigger.internal,
44
- num: int = 0,
45
74
  exposure: Optional[float] = None,
46
75
  ) -> AsyncStatus:
47
76
  """Arm the detector and return AsyncStatus.
@@ -68,17 +97,17 @@ class DetectorWriter(ABC):
68
97
  """
69
98
 
70
99
  @abstractmethod
71
- async def wait_for_index(
72
- self, index: int, timeout: Optional[float] = DEFAULT_TIMEOUT
73
- ) -> None:
74
- """Wait until a specific index is ready to be collected"""
100
+ def observe_indices_written(
101
+ self, timeout=DEFAULT_TIMEOUT
102
+ ) -> AsyncGenerator[int, None]:
103
+ """Yield each index as it is written"""
75
104
 
76
105
  @abstractmethod
77
106
  async def get_indices_written(self) -> int:
78
107
  """Get the number of indices written"""
79
108
 
80
109
  @abstractmethod
81
- def collect_stream_docs(self, indices_written: int) -> AsyncIterator[Asset]:
110
+ def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
82
111
  """Create Stream docs up to given number written"""
83
112
 
84
113
  @abstractmethod
@@ -92,16 +121,18 @@ class StandardDetector(
92
121
  Configurable,
93
122
  Readable,
94
123
  Triggerable,
95
- WritesExternalAssets,
124
+ Preparable,
125
+ Flyable,
126
+ Collectable,
127
+ WritesStreamAssets,
96
128
  ):
97
- """Detector with useful default behaviour.
129
+ """Detector with useful step and flyscan behaviour.
98
130
 
99
131
  Must be supplied instances of classes that inherit from DetectorControl and
100
132
  DetectorData, to dictate how the detector will be controlled (i.e. arming and
101
133
  disarming) as well as how the detector data will be written (i.e. opening and
102
134
  closing the writer, and handling data writing indices).
103
135
 
104
- NOTE: only for step-scans.
105
136
  """
106
137
 
107
138
  def __init__(
@@ -127,6 +158,16 @@ class StandardDetector(
127
158
  self._describe: Dict[str, Descriptor] = {}
128
159
  self._config_sigs = list(config_sigs)
129
160
  self._frame_writing_timeout = writer_timeout
161
+ # For prepare
162
+ self._arm_status: Optional[AsyncStatus] = None
163
+ self._trigger_info: Optional[TriggerInfo] = None
164
+ # For kickoff
165
+ self._watchers: List[Callable] = []
166
+ self._fly_status: Optional[AsyncStatus] = None
167
+ self._fly_start: float
168
+
169
+ self._intial_frame: int
170
+ self._last_frame: int
130
171
  super().__init__(name)
131
172
 
132
173
  @property
@@ -137,6 +178,13 @@ class StandardDetector(
137
178
  def writer(self) -> DetectorWriter:
138
179
  return self._writer
139
180
 
181
+ @AsyncStatus.wrap
182
+ async def stage(self) -> None:
183
+ """Disarm the detector, stop filewriting, and open file for writing."""
184
+ await self.check_config_sigs()
185
+ await asyncio.gather(self.writer.close(), self.controller.disarm())
186
+ self._describe = await self.writer.open()
187
+
140
188
  async def check_config_sigs(self):
141
189
  """Checks configuration signals are named and connected."""
142
190
  for signal in self._config_sigs:
@@ -144,7 +192,6 @@ class StandardDetector(
144
192
  raise Exception(
145
193
  "config signal must be named before it is passed to the detector"
146
194
  )
147
-
148
195
  try:
149
196
  await signal.get_value()
150
197
  except NotImplementedError:
@@ -154,17 +201,20 @@ class StandardDetector(
154
201
  )
155
202
 
156
203
  @AsyncStatus.wrap
157
- async def stage(self) -> None:
158
- """Disarm the detector, stop filewriting, and open file for writing."""
159
- await self.check_config_sigs()
160
- await asyncio.gather(self.writer.close(), self.controller.disarm())
161
- self._describe = await self.writer.open()
204
+ async def unstage(self) -> None:
205
+ """Stop data writing."""
206
+ await self.writer.close()
207
+
208
+ async def read_configuration(self) -> Dict[str, Reading]:
209
+ return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
162
210
 
163
211
  async def describe_configuration(self) -> Dict[str, Descriptor]:
164
212
  return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
165
213
 
166
- async def read_configuration(self) -> Dict[str, Reading]:
167
- return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
214
+ async def read(self) -> Dict[str, Reading]:
215
+ """Read the detector"""
216
+ # All data is in StreamResources, not Events, so nothing to output here
217
+ return {}
168
218
 
169
219
  def describe(self) -> Dict[str, Descriptor]:
170
220
  return self._describe
@@ -173,27 +223,99 @@ class StandardDetector(
173
223
  async def trigger(self) -> None:
174
224
  """Arm the detector and wait for it to finish."""
175
225
  indices_written = await self.writer.get_indices_written()
176
- written_status = await self.controller.arm(DetectorTrigger.internal, num=1)
226
+ written_status = await self.controller.arm(
227
+ num=1,
228
+ trigger=DetectorTrigger.internal,
229
+ )
177
230
  await written_status
178
- await self.writer.wait_for_index(
179
- indices_written + 1, timeout=self._frame_writing_timeout
231
+ end_observation = indices_written + 1
232
+
233
+ async for index in self.writer.observe_indices_written(
234
+ self._frame_writing_timeout
235
+ ):
236
+ if index >= end_observation:
237
+ break
238
+
239
+ def prepare(
240
+ self,
241
+ value: T,
242
+ ) -> AsyncStatus:
243
+ """Arm detector"""
244
+ return AsyncStatus(self._prepare(value))
245
+
246
+ async def _prepare(self, value: T) -> None:
247
+ """Arm detector.
248
+
249
+ Prepare the detector with trigger information. This is determined at and passed
250
+ in from the plan level.
251
+
252
+ This currently only prepares detectors for flyscans and stepscans just use the
253
+ trigger information determined in trigger.
254
+
255
+ To do: Unify prepare to be use for both fly and step scans.
256
+ """
257
+ assert type(value) is TriggerInfo
258
+ self._trigger_info = value
259
+ self._initial_frame = await self.writer.get_indices_written()
260
+ self._last_frame = self._initial_frame + self._trigger_info.num
261
+
262
+ required = self.controller.get_deadtime(self._trigger_info.livetime)
263
+ assert required <= self._trigger_info.deadtime, (
264
+ f"Detector {self.controller} needs at least {required}s deadtime, "
265
+ f"but trigger logic provides only {self._trigger_info.deadtime}s"
180
266
  )
181
267
 
182
- async def read(self) -> Dict[str, Reading]:
183
- """Read the detector"""
184
- # All data is in StreamResources, not Events, so nothing to output here
185
- return {}
268
+ self._arm_status = await self.controller.arm(
269
+ num=self._trigger_info.num,
270
+ trigger=self._trigger_info.trigger,
271
+ exposure=self._trigger_info.livetime,
272
+ )
186
273
 
187
- async def collect_asset_docs(self) -> AsyncIterator[Asset]:
188
- """Collect stream datum documents for all indices written."""
189
- indices_written = await self.writer.get_indices_written()
274
+ @AsyncStatus.wrap
275
+ async def kickoff(self) -> None:
276
+ self._fly_status = AsyncStatus(self._fly(), self._watchers)
277
+ self._fly_start = time.monotonic()
278
+
279
+ async def _fly(self) -> None:
280
+ await self._observe_writer_indicies(self._last_frame)
281
+
282
+ async def _observe_writer_indicies(self, end_observation: int):
283
+ async for index in self.writer.observe_indices_written(
284
+ self._frame_writing_timeout
285
+ ):
286
+ for watcher in self._watchers:
287
+ watcher(
288
+ name=self.name,
289
+ current=index,
290
+ initial=self._initial_frame,
291
+ target=end_observation,
292
+ unit="",
293
+ precision=0,
294
+ time_elapsed=time.monotonic() - self._fly_start,
295
+ )
296
+ if index >= end_observation:
297
+ break
298
+
299
+ @AsyncStatus.wrap
300
+ async def complete(self) -> AsyncStatus:
301
+ assert self._fly_status, "Kickoff not run"
302
+ return await self._fly_status
303
+
304
+ async def describe_collect(self) -> Dict[str, Descriptor]:
305
+ return self._describe
306
+
307
+ async def collect_asset_docs(
308
+ self, index: Optional[int] = None
309
+ ) -> AsyncIterator[StreamAsset]:
310
+ """Collect stream datum documents for all indices written.
190
311
 
191
- async for doc in self.writer.collect_stream_docs(indices_written):
312
+ The index is optional, and provided for flyscans, however this needs to be
313
+ retrieved for stepscans.
314
+ """
315
+ if not index:
316
+ index = await self.writer.get_indices_written()
317
+ async for doc in self.writer.collect_stream_docs(index):
192
318
  yield doc
193
- # async for doc in self.writer.collect_stream_docs(indices_written):
194
- # yield doc
195
319
 
196
- @AsyncStatus.wrap
197
- async def unstage(self) -> None:
198
- """Stop data writing."""
199
- await self.writer.close()
320
+ async def get_index(self) -> int:
321
+ return await self.writer.get_indices_written()
@@ -1,16 +1,24 @@
1
1
  """Base device"""
2
+
2
3
  from __future__ import annotations
3
4
 
4
- import asyncio
5
- import logging
6
5
  import sys
7
- from contextlib import suppress
8
- from typing import Any, Dict, Generator, Iterator, Optional, Set, Tuple, TypeVar
6
+ from typing import (
7
+ Any,
8
+ Coroutine,
9
+ Dict,
10
+ Generator,
11
+ Iterator,
12
+ Optional,
13
+ Set,
14
+ Tuple,
15
+ TypeVar,
16
+ )
9
17
 
10
18
  from bluesky.protocols import HasName
11
19
  from bluesky.run_engine import call_in_bluesky_event_loop
12
20
 
13
- from .utils import NotConnected, wait_for_connection
21
+ from .utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection
14
22
 
15
23
 
16
24
  class Device(HasName):
@@ -50,16 +58,21 @@ class Device(HasName):
50
58
  child.set_name(child_name)
51
59
  child.parent = self
52
60
 
53
- async def connect(self, sim: bool = False):
61
+ async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT):
54
62
  """Connect self and all child Devices.
55
63
 
64
+ Contains a timeout that gets propagated to child.connect methods.
65
+
56
66
  Parameters
57
67
  ----------
58
68
  sim:
59
69
  If True then connect in simulation mode.
70
+ timeout:
71
+ Time to wait before failing with a TimeoutError.
60
72
  """
61
73
  coros = {
62
- name: child_device.connect(sim) for name, child_device in self.children()
74
+ name: child_device.connect(sim, timeout=timeout)
75
+ for name, child_device in self.children()
63
76
  }
64
77
  if coros:
65
78
  await wait_for_connection(**coros)
@@ -140,41 +153,19 @@ class DeviceCollector:
140
153
 
141
154
  async def _on_exit(self) -> None:
142
155
  # Name and kick off connect for devices
143
- tasks: Dict[asyncio.Task, str] = {}
156
+ connect_coroutines: Dict[str, Coroutine] = {}
144
157
  for name, obj in self._objects_on_exit.items():
145
158
  if name not in self._names_on_enter and isinstance(obj, Device):
146
159
  if self._set_name and not obj.name:
147
160
  obj.set_name(name)
148
161
  if self._connect:
149
- task = asyncio.create_task(obj.connect(self._sim))
150
- tasks[task] = name
151
- # Wait for all the signals to have finished
152
- if tasks:
153
- await self._wait_for_tasks(tasks)
154
-
155
- async def _wait_for_tasks(self, tasks: Dict[asyncio.Task, str]):
156
- done, pending = await asyncio.wait(tasks, timeout=self._timeout)
157
- if pending:
158
- msg = f"{len(pending)} Devices did not connect:"
159
- for t in pending:
160
- t.cancel()
161
- with suppress(Exception):
162
- await t
163
- e = t.exception()
164
- msg += f"\n {tasks[t]}: {type(e).__name__}"
165
- lines = str(e).splitlines()
166
- if len(lines) <= 1:
167
- msg += f": {e}"
168
- else:
169
- msg += "".join(f"\n {line}" for line in lines)
170
- logging.error(msg)
171
- raised = [t for t in done if t.exception()]
172
- if raised:
173
- logging.error(f"{len(raised)} Devices raised an error:")
174
- for t in raised:
175
- logging.exception(f" {tasks[t]}:", exc_info=t.exception())
176
- if pending or raised:
177
- raise NotConnected("Not all Devices connected")
162
+ connect_coroutines[name] = obj.connect(
163
+ self._sim, timeout=self._timeout
164
+ )
165
+
166
+ # Connect to all the devices
167
+ if connect_coroutines:
168
+ await wait_for_connection(**connect_coroutines)
178
169
 
179
170
  async def __aexit__(self, type, value, traceback):
180
171
  self._objects_on_exit = self._caller_locals()
@@ -182,4 +173,12 @@ class DeviceCollector:
182
173
 
183
174
  def __exit__(self, type_, value, traceback):
184
175
  self._objects_on_exit = self._caller_locals()
185
- return call_in_bluesky_event_loop(self._on_exit())
176
+ try:
177
+ fut = call_in_bluesky_event_loop(self._on_exit())
178
+ except RuntimeError:
179
+ raise NotConnected(
180
+ "Could not connect devices. Is the bluesky event loop running? See "
181
+ "https://blueskyproject.io/ophyd-async/main/"
182
+ "user/explanations/event-loop-choice.html for more info."
183
+ )
184
+ return fut