ophyd-async 0.3a2__py3-none-any.whl → 0.3a4__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 (53) hide show
  1. ophyd_async/_version.py +1 -1
  2. ophyd_async/core/__init__.py +35 -11
  3. ophyd_async/core/async_status.py +2 -0
  4. ophyd_async/core/detector.py +8 -9
  5. ophyd_async/core/device.py +22 -9
  6. ophyd_async/core/flyer.py +2 -2
  7. ophyd_async/core/mock_signal_backend.py +86 -0
  8. ophyd_async/core/mock_signal_utils.py +149 -0
  9. ophyd_async/core/signal.py +140 -49
  10. ophyd_async/core/signal_backend.py +2 -2
  11. ophyd_async/core/{sim_signal_backend.py → soft_signal_backend.py} +29 -39
  12. ophyd_async/core/standard_readable.py +211 -24
  13. ophyd_async/epics/_backend/_aioca.py +17 -13
  14. ophyd_async/epics/_backend/_p4p.py +28 -18
  15. ophyd_async/epics/_backend/common.py +17 -17
  16. ophyd_async/epics/areadetector/__init__.py +4 -4
  17. ophyd_async/epics/areadetector/aravis.py +7 -9
  18. ophyd_async/epics/areadetector/controllers/__init__.py +2 -1
  19. ophyd_async/epics/areadetector/controllers/kinetix_controller.py +49 -0
  20. ophyd_async/epics/areadetector/controllers/vimba_controller.py +66 -0
  21. ophyd_async/epics/areadetector/drivers/__init__.py +6 -0
  22. ophyd_async/epics/areadetector/drivers/ad_base.py +12 -10
  23. ophyd_async/epics/areadetector/drivers/aravis_driver.py +7 -5
  24. ophyd_async/epics/areadetector/drivers/kinetix_driver.py +27 -0
  25. ophyd_async/epics/areadetector/drivers/pilatus_driver.py +5 -2
  26. ophyd_async/epics/areadetector/drivers/vimba_driver.py +63 -0
  27. ophyd_async/epics/areadetector/kinetix.py +46 -0
  28. ophyd_async/epics/areadetector/pilatus.py +7 -12
  29. ophyd_async/epics/areadetector/single_trigger_det.py +14 -6
  30. ophyd_async/epics/areadetector/utils.py +2 -12
  31. ophyd_async/epics/areadetector/vimba.py +43 -0
  32. ophyd_async/epics/areadetector/writers/hdf_writer.py +6 -3
  33. ophyd_async/epics/areadetector/writers/nd_file_hdf.py +21 -18
  34. ophyd_async/epics/areadetector/writers/nd_plugin.py +6 -7
  35. ophyd_async/epics/demo/__init__.py +19 -22
  36. ophyd_async/epics/motion/motor.py +16 -13
  37. ophyd_async/epics/pvi/pvi.py +11 -11
  38. ophyd_async/epics/signal/signal.py +1 -1
  39. ophyd_async/log.py +130 -0
  40. ophyd_async/panda/_hdf_panda.py +3 -3
  41. ophyd_async/panda/writers/_hdf_writer.py +3 -3
  42. ophyd_async/protocols.py +26 -3
  43. ophyd_async/sim/demo/sim_motor.py +14 -12
  44. ophyd_async/sim/pattern_generator.py +9 -9
  45. ophyd_async/sim/sim_pattern_detector_writer.py +2 -2
  46. ophyd_async/sim/sim_pattern_generator.py +2 -2
  47. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a4.dist-info}/METADATA +20 -3
  48. ophyd_async-0.3a4.dist-info/RECORD +85 -0
  49. ophyd_async-0.3a2.dist-info/RECORD +0 -76
  50. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a4.dist-info}/LICENSE +0 -0
  51. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a4.dist-info}/WHEEL +0 -0
  52. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a4.dist-info}/entry_points.txt +0 -0
  53. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a4.dist-info}/top_level.txt +0 -0
@@ -2,27 +2,36 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import functools
5
- from typing import AsyncGenerator, Callable, Dict, Generic, Optional, Tuple, Type, Union
5
+ from typing import (
6
+ Any,
7
+ AsyncGenerator,
8
+ Callable,
9
+ Dict,
10
+ Generic,
11
+ Mapping,
12
+ Optional,
13
+ Tuple,
14
+ Type,
15
+ Union,
16
+ )
6
17
 
7
18
  from bluesky.protocols import (
8
- Descriptor,
19
+ DataKey,
9
20
  Locatable,
10
21
  Location,
11
22
  Movable,
12
23
  Reading,
13
- Stageable,
14
24
  Subscribable,
15
25
  )
16
26
 
17
- from ophyd_async.protocols import AsyncReadable
27
+ from ophyd_async.core.mock_signal_backend import MockSignalBackend
28
+ from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageable
18
29
 
19
30
  from .async_status import AsyncStatus
20
31
  from .device import Device
21
32
  from .signal_backend import SignalBackend
22
- from .sim_signal_backend import SimSignalBackend
23
- from .utils import DEFAULT_TIMEOUT, Callback, ReadingValueCallback, T
24
-
25
- _sim_backends: Dict[Signal, SimSignalBackend] = {}
33
+ from .soft_signal_backend import SoftSignalBackend
34
+ from .utils import DEFAULT_TIMEOUT, Callback, T
26
35
 
27
36
 
28
37
  def _add_timeout(func):
@@ -51,17 +60,17 @@ class Signal(Device, Generic[T]):
51
60
  timeout: Optional[float] = DEFAULT_TIMEOUT,
52
61
  name: str = "",
53
62
  ) -> None:
54
- super().__init__(name)
55
63
  self._timeout = timeout
56
- self._init_backend = self._backend = backend
64
+ self._initial_backend = self._backend = backend
65
+ super().__init__(name)
57
66
 
58
- async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT):
59
- if sim:
60
- self._backend = SimSignalBackend(datatype=self._init_backend.datatype)
61
- _sim_backends[self] = self._backend
62
- else:
63
- self._backend = self._init_backend
64
- _sim_backends.pop(self, None)
67
+ async def connect(self, mock=False, timeout=DEFAULT_TIMEOUT):
68
+ if mock and not isinstance(self._backend, MockSignalBackend):
69
+ # Using a soft backend, look to the initial value
70
+ self._backend = MockSignalBackend(
71
+ initial_backend=self._initial_backend,
72
+ )
73
+ self.log.debug(f"Connecting to {self.source}")
65
74
  await self._backend.connect(timeout=timeout)
66
75
 
67
76
  @property
@@ -86,10 +95,12 @@ class _SignalCache(Generic[T]):
86
95
  self._value: Optional[T] = None
87
96
 
88
97
  self.backend = backend
98
+ signal.log.debug(f"Making subscription on source {signal.source}")
89
99
  backend.set_callback(self._callback)
90
100
 
91
101
  def close(self):
92
102
  self.backend.set_callback(None)
103
+ self._signal.log.debug(f"Closing subscription on source {self._signal.source}")
93
104
 
94
105
  async def get_reading(self) -> Reading:
95
106
  await self._valid.wait()
@@ -102,6 +113,10 @@ class _SignalCache(Generic[T]):
102
113
  return self._value
103
114
 
104
115
  def _callback(self, reading: Reading, value: T):
116
+ self._signal.log.debug(
117
+ f"Updated subscription: reading of source {self._signal.source} changed"
118
+ f"from {self._reading} to {reading}"
119
+ )
105
120
  self._reading = reading
106
121
  self._value = value
107
122
  self._valid.set()
@@ -128,7 +143,7 @@ class _SignalCache(Generic[T]):
128
143
  return self._staged or bool(self._listeners)
129
144
 
130
145
 
131
- class SignalR(Signal[T], AsyncReadable, Stageable, Subscribable):
146
+ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
132
147
  """Signal that can be read from and monitored"""
133
148
 
134
149
  _cache: Optional[_SignalCache] = None
@@ -161,14 +176,16 @@ class SignalR(Signal[T], AsyncReadable, Stageable, Subscribable):
161
176
  return {self.name: await self._backend_or_cache(cached).get_reading()}
162
177
 
163
178
  @_add_timeout
164
- async def describe(self) -> Dict[str, Descriptor]:
179
+ async def describe(self) -> Dict[str, DataKey]:
165
180
  """Return a single item dict with the descriptor in it"""
166
- return {self.name: await self._backend.get_descriptor(self.source)}
181
+ return {self.name: await self._backend.get_datakey(self.source)}
167
182
 
168
183
  @_add_timeout
169
184
  async def get_value(self, cached: Optional[bool] = None) -> T:
170
185
  """The current value"""
171
- return await self._backend_or_cache(cached).get_value()
186
+ value = await self._backend_or_cache(cached).get_value()
187
+ self.log.debug(f"get_value() on source {self.source} returned {value}")
188
+ return value
172
189
 
173
190
  def subscribe_value(self, function: Callback[T]):
174
191
  """Subscribe to updates in value of a device"""
@@ -203,8 +220,15 @@ class SignalW(Signal[T], Movable):
203
220
  """Set the value and return a status saying when it's done"""
204
221
  if timeout is USE_DEFAULT_TIMEOUT:
205
222
  timeout = self._timeout
206
- coro = self._backend.put(value, wait=wait, timeout=timeout)
207
- return AsyncStatus(coro)
223
+
224
+ async def do_set():
225
+ self.log.debug(f"Putting value {value} to backend at source {self.source}")
226
+ await self._backend.put(value, wait=wait, timeout=timeout)
227
+ self.log.debug(
228
+ f"Successfully put value {value} to backend at source {self.source}"
229
+ )
230
+
231
+ return AsyncStatus(do_set())
208
232
 
209
233
 
210
234
  class SignalRW(SignalR[T], SignalW[T], Locatable):
@@ -229,47 +253,114 @@ class SignalX(Signal):
229
253
  return AsyncStatus(coro)
230
254
 
231
255
 
232
- def set_sim_value(signal: Signal[T], value: T):
233
- """Set the value of a signal that is in sim mode."""
234
- _sim_backends[signal]._set_value(value)
235
-
236
-
237
- def set_sim_put_proceeds(signal: Signal[T], proceeds: bool):
238
- """Allow or block a put with wait=True from proceeding"""
239
- event = _sim_backends[signal].put_proceeds
240
- if proceeds:
241
- event.set()
242
- else:
243
- event.clear()
244
-
245
-
246
- def set_sim_callback(signal: Signal[T], callback: ReadingValueCallback[T]) -> None:
247
- """Monitor the value of a signal that is in sim mode"""
248
- return _sim_backends[signal].set_callback(callback)
249
-
250
-
251
256
  def soft_signal_rw(
252
257
  datatype: Optional[Type[T]] = None,
253
258
  initial_value: Optional[T] = None,
254
259
  name: str = "",
255
260
  ) -> SignalRW[T]:
256
- """Creates a read-writable Signal with a SimSignalBackend"""
257
- signal = SignalRW(SimSignalBackend(datatype, initial_value), name=name)
261
+ """Creates a read-writable Signal with a SoftSignalBackend"""
262
+ signal = SignalRW(SoftSignalBackend(datatype, initial_value), name=name)
258
263
  return signal
259
264
 
260
265
 
261
- def soft_signal_r_and_backend(
266
+ def soft_signal_r_and_setter(
262
267
  datatype: Optional[Type[T]] = None,
263
268
  initial_value: Optional[T] = None,
264
269
  name: str = "",
265
- ) -> Tuple[SignalR[T], SimSignalBackend]:
266
- """Returns a tuple of a read-only Signal and its SimSignalBackend through
270
+ ) -> Tuple[SignalR[T], Callable[[T]]]:
271
+ """Returns a tuple of a read-only Signal and a callable through
267
272
  which the signal can be internally modified within the device. Use
268
273
  soft_signal_rw if you want a device that is externally modifiable
269
274
  """
270
- backend = SimSignalBackend(datatype, initial_value)
275
+ backend = SoftSignalBackend(datatype, initial_value)
271
276
  signal = SignalR(backend, name=name)
272
- return (signal, backend)
277
+
278
+ return (signal, backend.set_value)
279
+
280
+
281
+ async def assert_value(signal: SignalR[T], value: Any) -> None:
282
+ """Assert a signal's value and compare it an expected signal.
283
+
284
+ Parameters
285
+ ----------
286
+ signal:
287
+ signal with get_value.
288
+ value:
289
+ The expected value from the signal.
290
+
291
+ Notes
292
+ -----
293
+ Example usage::
294
+ await assert_value(signal, value)
295
+
296
+ """
297
+ assert await signal.get_value() == value
298
+
299
+
300
+ async def assert_reading(
301
+ readable: AsyncReadable, reading: Mapping[str, Reading]
302
+ ) -> None:
303
+ """Assert readings from readable.
304
+
305
+ Parameters
306
+ ----------
307
+ readable:
308
+ Callable with readable.read function that generate readings.
309
+
310
+ reading:
311
+ The expected readings from the readable.
312
+
313
+ Notes
314
+ -----
315
+ Example usage::
316
+ await assert_reading(readable, reading)
317
+
318
+ """
319
+ assert await readable.read() == reading
320
+
321
+
322
+ async def assert_configuration(
323
+ configurable: AsyncConfigurable,
324
+ configuration: Mapping[str, Reading],
325
+ ) -> None:
326
+ """Assert readings from Configurable.
327
+
328
+ Parameters
329
+ ----------
330
+ configurable:
331
+ Configurable with Configurable.read function that generate readings.
332
+
333
+ configuration:
334
+ The expected readings from configurable.
335
+
336
+ Notes
337
+ -----
338
+ Example usage::
339
+ await assert_configuration(configurable configuration)
340
+
341
+ """
342
+ assert await configurable.read_configuration() == configuration
343
+
344
+
345
+ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
346
+ """Assert emitted document generated by running a Bluesky plan
347
+
348
+ Parameters
349
+ ----------
350
+ Doc:
351
+ A dictionary
352
+
353
+ numbers:
354
+ expected emission in kwarg from
355
+
356
+ Notes
357
+ -----
358
+ Example usage::
359
+ assert_emitted(docs, start=1, descriptor=1,
360
+ resource=1, datum=1, event=1, stop=1)
361
+ """
362
+ assert list(docs) == list(numbers)
363
+ assert {name: len(d) for name, d in docs.items()} == numbers
273
364
 
274
365
 
275
366
  async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, None]:
@@ -1,7 +1,7 @@
1
1
  from abc import abstractmethod
2
2
  from typing import Generic, Optional, Type
3
3
 
4
- from bluesky.protocols import Descriptor, Reading
4
+ from bluesky.protocols import DataKey, Reading
5
5
 
6
6
  from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T
7
7
 
@@ -27,7 +27,7 @@ class SignalBackend(Generic[T]):
27
27
  """Put a value to the PV, if wait then wait for completion for up to timeout"""
28
28
 
29
29
  @abstractmethod
30
- async def get_descriptor(self, source: str) -> Descriptor:
30
+ async def get_datakey(self, source: str) -> DataKey:
31
31
  """Metadata like source, dtype, shape, precision, units"""
32
32
 
33
33
  @abstractmethod
@@ -1,15 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
4
3
  import inspect
5
4
  import time
6
5
  from collections import abc
7
6
  from dataclasses import dataclass
8
7
  from enum import Enum
9
- from typing import Any, Dict, Generic, Optional, Type, Union, cast, get_origin
8
+ from typing import Dict, Generic, Optional, Type, Union, cast, get_origin
10
9
 
11
10
  import numpy as np
12
- from bluesky.protocols import Descriptor, Dtype, Reading
11
+ from bluesky.protocols import DataKey, Dtype, Reading
13
12
 
14
13
  from .signal_backend import SignalBackend
15
14
  from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T, get_dtype
@@ -22,7 +21,7 @@ primitive_dtypes: Dict[type, Dtype] = {
22
21
  }
23
22
 
24
23
 
25
- class SimConverter(Generic[T]):
24
+ class SoftConverter(Generic[T]):
26
25
  def value(self, value: T) -> T:
27
26
  return value
28
27
 
@@ -36,7 +35,7 @@ class SimConverter(Generic[T]):
36
35
  alarm_severity=-1 if severity > 2 else severity,
37
36
  )
38
37
 
39
- def descriptor(self, source: str, value) -> Descriptor:
38
+ def get_datakey(self, source: str, value) -> DataKey:
40
39
  dtype = type(value)
41
40
  if np.issubdtype(dtype, np.integer):
42
41
  dtype = int
@@ -55,8 +54,8 @@ class SimConverter(Generic[T]):
55
54
  return datatype()
56
55
 
57
56
 
58
- class SimArrayConverter(SimConverter):
59
- def descriptor(self, source: str, value) -> Descriptor:
57
+ class SoftArrayConverter(SoftConverter):
58
+ def get_datakey(self, source: str, value) -> DataKey:
60
59
  return {"source": source, "dtype": "array", "shape": [len(value)]}
61
60
 
62
61
  def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
@@ -70,7 +69,7 @@ class SimArrayConverter(SimConverter):
70
69
 
71
70
 
72
71
  @dataclass
73
- class SimEnumConverter(SimConverter):
72
+ class SoftEnumConverter(SoftConverter):
74
73
  enum_class: Type[Enum]
75
74
 
76
75
  def write_value(self, value: Union[Enum, str]) -> Enum:
@@ -79,7 +78,7 @@ class SimEnumConverter(SimConverter):
79
78
  else:
80
79
  return self.enum_class(value)
81
80
 
82
- def descriptor(self, source: str, value) -> Descriptor:
81
+ def get_datakey(self, source: str, value) -> DataKey:
83
82
  choices = [e.value for e in self.enum_class]
84
83
  return {"source": source, "dtype": "string", "shape": [], "choices": choices} # type: ignore
85
84
 
@@ -90,26 +89,21 @@ class SimEnumConverter(SimConverter):
90
89
  return cast(T, list(datatype.__members__.values())[0]) # type: ignore
91
90
 
92
91
 
93
- class DisconnectedSimConverter(SimConverter):
94
- def __getattribute__(self, __name: str) -> Any:
95
- raise NotImplementedError("No PV has been set as connect() has not been called")
96
-
97
-
98
92
  def make_converter(datatype):
99
93
  is_array = get_dtype(datatype) is not None
100
94
  is_sequence = get_origin(datatype) == abc.Sequence
101
95
  is_enum = issubclass(datatype, Enum) if inspect.isclass(datatype) else False
102
96
 
103
97
  if is_array or is_sequence:
104
- return SimArrayConverter()
98
+ return SoftArrayConverter()
105
99
  if is_enum:
106
- return SimEnumConverter(datatype)
100
+ return SoftEnumConverter(datatype)
107
101
 
108
- return SimConverter()
102
+ return SoftConverter()
109
103
 
110
104
 
111
- class SimSignalBackend(SignalBackend[T]):
112
- """An simulated backend to a Signal, created with ``Signal.connect(sim=True)``"""
105
+ class SoftSignalBackend(SignalBackend[T]):
106
+ """An backend to a soft Signal, for test signals see ``MockSignalBackend``."""
113
107
 
114
108
  _value: T
115
109
  _initial_value: Optional[T]
@@ -122,25 +116,23 @@ class SimSignalBackend(SignalBackend[T]):
122
116
  initial_value: Optional[T] = None,
123
117
  ) -> None:
124
118
  self.datatype = datatype
125
- self.converter: SimConverter = DisconnectedSimConverter()
126
119
  self._initial_value = initial_value
127
- self.put_proceeds = asyncio.Event()
128
- self.put_proceeds.set()
129
- self.callback: Optional[ReadingValueCallback[T]] = None
130
-
131
- def source(self, name: str) -> str:
132
- return f"soft://{name}"
133
-
134
- async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
135
- self.converter = make_converter(self.datatype)
120
+ self.converter: SoftConverter = make_converter(datatype)
136
121
  if self._initial_value is None:
137
122
  self._initial_value = self.converter.make_initial_value(self.datatype)
138
123
  else:
139
- # convert potentially unconverted initial value passed to init method
140
124
  self._initial_value = self.converter.write_value(self._initial_value)
125
+
126
+ self.callback: Optional[ReadingValueCallback[T]] = None
141
127
  self._severity = 0
128
+ self.set_value(self._initial_value)
142
129
 
143
- await self.put(None)
130
+ def source(self, name: str) -> str:
131
+ return f"soft://{name}"
132
+
133
+ async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
134
+ """Connection isn't required for soft signals."""
135
+ pass
144
136
 
145
137
  async def put(self, value: Optional[T], wait=True, timeout=None):
146
138
  write_value = (
@@ -148,13 +140,11 @@ class SimSignalBackend(SignalBackend[T]):
148
140
  if value is not None
149
141
  else self._initial_value
150
142
  )
151
- self._set_value(write_value)
152
143
 
153
- if wait:
154
- await asyncio.wait_for(self.put_proceeds.wait(), timeout)
144
+ self.set_value(write_value)
155
145
 
156
- def _set_value(self, value: T):
157
- """Method to bypass asynchronous logic, designed to only be used in tests."""
146
+ def set_value(self, value: T):
147
+ """Method to bypass asynchronous logic."""
158
148
  self._value = value
159
149
  self._timestamp = time.monotonic()
160
150
  reading: Reading = self.converter.reading(
@@ -164,8 +154,8 @@ class SimSignalBackend(SignalBackend[T]):
164
154
  if self.callback:
165
155
  self.callback(reading, self._value)
166
156
 
167
- async def get_descriptor(self, source: str) -> Descriptor:
168
- return self.converter.descriptor(source, self._value)
157
+ async def get_datakey(self, source: str) -> DataKey:
158
+ return self.converter.get_datakey(source, self._value)
169
159
 
170
160
  async def get_reading(self) -> Reading:
171
161
  return self.converter.reading(self._value, self._timestamp, self._severity)
@@ -174,7 +164,7 @@ class SimSignalBackend(SignalBackend[T]):
174
164
  return self.converter.value(self._value)
175
165
 
176
166
  async def get_setpoint(self) -> T:
177
- """For a simulated backend, the setpoint and readback values are the same."""
167
+ """For a soft signal, the setpoint and readback values are the same."""
178
168
  return await self.get_value()
179
169
 
180
170
  def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None: