ophyd-async 0.9.0a2__py3-none-any.whl → 0.10.0a2__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 (151) hide show
  1. ophyd_async/__init__.py +5 -8
  2. ophyd_async/_docs_parser.py +12 -0
  3. ophyd_async/_version.py +9 -4
  4. ophyd_async/core/__init__.py +97 -62
  5. ophyd_async/core/_derived_signal.py +271 -0
  6. ophyd_async/core/_derived_signal_backend.py +300 -0
  7. ophyd_async/core/_detector.py +106 -125
  8. ophyd_async/core/_device.py +69 -63
  9. ophyd_async/core/_device_filler.py +65 -1
  10. ophyd_async/core/_flyer.py +14 -5
  11. ophyd_async/core/_hdf_dataset.py +29 -22
  12. ophyd_async/core/_log.py +14 -23
  13. ophyd_async/core/_mock_signal_backend.py +11 -3
  14. ophyd_async/core/_protocol.py +65 -45
  15. ophyd_async/core/_providers.py +28 -9
  16. ophyd_async/core/_readable.py +44 -35
  17. ophyd_async/core/_settings.py +36 -27
  18. ophyd_async/core/_signal.py +262 -170
  19. ophyd_async/core/_signal_backend.py +56 -13
  20. ophyd_async/core/_soft_signal_backend.py +16 -11
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +41 -11
  23. ophyd_async/core/_utils.py +96 -49
  24. ophyd_async/core/_yaml_settings.py +2 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/_andor.py +2 -2
  27. ophyd_async/epics/adandor/_andor_controller.py +4 -2
  28. ophyd_async/epics/adandor/_andor_io.py +2 -4
  29. ophyd_async/epics/adaravis/__init__.py +5 -0
  30. ophyd_async/epics/adaravis/_aravis.py +4 -8
  31. ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
  32. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  33. ophyd_async/epics/adcore/__init__.py +23 -8
  34. ophyd_async/epics/adcore/_core_detector.py +42 -2
  35. ophyd_async/epics/adcore/_core_io.py +124 -99
  36. ophyd_async/epics/adcore/_core_logic.py +106 -27
  37. ophyd_async/epics/adcore/_core_writer.py +12 -8
  38. ophyd_async/epics/adcore/_hdf_writer.py +21 -38
  39. ophyd_async/epics/adcore/_single_trigger.py +2 -2
  40. ophyd_async/epics/adcore/_utils.py +2 -2
  41. ophyd_async/epics/adkinetix/__init__.py +2 -1
  42. ophyd_async/epics/adkinetix/_kinetix.py +3 -3
  43. ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
  44. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  45. ophyd_async/epics/adpilatus/__init__.py +5 -0
  46. ophyd_async/epics/adpilatus/_pilatus.py +1 -1
  47. ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
  48. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  49. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  50. ophyd_async/epics/adsimdetector/_sim.py +4 -14
  51. ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
  52. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  53. ophyd_async/epics/advimba/__init__.py +10 -1
  54. ophyd_async/epics/advimba/_vimba.py +3 -2
  55. ophyd_async/epics/advimba/_vimba_controller.py +4 -2
  56. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  57. ophyd_async/epics/core/_aioca.py +35 -16
  58. ophyd_async/epics/core/_epics_connector.py +4 -0
  59. ophyd_async/epics/core/_epics_device.py +2 -0
  60. ophyd_async/epics/core/_p4p.py +10 -2
  61. ophyd_async/epics/core/_pvi_connector.py +65 -8
  62. ophyd_async/epics/core/_signal.py +51 -51
  63. ophyd_async/epics/core/_util.py +4 -4
  64. ophyd_async/epics/demo/__init__.py +16 -0
  65. ophyd_async/epics/demo/__main__.py +31 -0
  66. ophyd_async/epics/demo/_ioc.py +32 -0
  67. ophyd_async/epics/demo/_motor.py +82 -0
  68. ophyd_async/epics/demo/_point_detector.py +42 -0
  69. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  70. ophyd_async/epics/demo/_stage.py +15 -0
  71. ophyd_async/epics/{sim/mover.db → demo/motor.db} +2 -1
  72. ophyd_async/epics/demo/point_detector.db +59 -0
  73. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  74. ophyd_async/epics/eiger/_eiger.py +1 -3
  75. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  76. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  77. ophyd_async/epics/eiger/_odin_io.py +1 -2
  78. ophyd_async/epics/motor.py +65 -28
  79. ophyd_async/epics/signal.py +4 -1
  80. ophyd_async/epics/testing/_example_ioc.py +21 -9
  81. ophyd_async/epics/testing/_utils.py +3 -0
  82. ophyd_async/epics/testing/test_records.db +8 -0
  83. ophyd_async/epics/testing/test_records_pva.db +17 -16
  84. ophyd_async/fastcs/__init__.py +1 -0
  85. ophyd_async/fastcs/core.py +6 -0
  86. ophyd_async/fastcs/odin/__init__.py +1 -0
  87. ophyd_async/fastcs/panda/__init__.py +8 -6
  88. ophyd_async/fastcs/panda/_block.py +29 -9
  89. ophyd_async/fastcs/panda/_control.py +5 -0
  90. ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
  91. ophyd_async/fastcs/panda/_table.py +9 -6
  92. ophyd_async/fastcs/panda/_trigger.py +23 -9
  93. ophyd_async/fastcs/panda/_writer.py +27 -30
  94. ophyd_async/plan_stubs/__init__.py +2 -0
  95. ophyd_async/plan_stubs/_ensure_connected.py +1 -0
  96. ophyd_async/plan_stubs/_fly.py +2 -4
  97. ophyd_async/plan_stubs/_nd_attributes.py +2 -0
  98. ophyd_async/plan_stubs/_panda.py +1 -0
  99. ophyd_async/plan_stubs/_settings.py +43 -16
  100. ophyd_async/plan_stubs/_utils.py +3 -0
  101. ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
  102. ophyd_async/sim/__init__.py +24 -14
  103. ophyd_async/sim/__main__.py +43 -0
  104. ophyd_async/sim/_blob_detector.py +33 -0
  105. ophyd_async/sim/_blob_detector_controller.py +48 -0
  106. ophyd_async/sim/_blob_detector_writer.py +105 -0
  107. ophyd_async/sim/_mirror_horizontal.py +46 -0
  108. ophyd_async/sim/_mirror_vertical.py +74 -0
  109. ophyd_async/sim/_motor.py +233 -0
  110. ophyd_async/sim/_pattern_generator.py +124 -0
  111. ophyd_async/sim/_point_detector.py +86 -0
  112. ophyd_async/sim/_stage.py +19 -0
  113. ophyd_async/tango/__init__.py +1 -0
  114. ophyd_async/tango/core/__init__.py +6 -1
  115. ophyd_async/tango/core/_base_device.py +41 -33
  116. ophyd_async/tango/core/_converters.py +81 -0
  117. ophyd_async/tango/core/_signal.py +18 -32
  118. ophyd_async/tango/core/_tango_readable.py +2 -19
  119. ophyd_async/tango/core/_tango_transport.py +136 -60
  120. ophyd_async/tango/core/_utils.py +47 -0
  121. ophyd_async/tango/{sim → demo}/_counter.py +2 -0
  122. ophyd_async/tango/{sim → demo}/_detector.py +2 -0
  123. ophyd_async/tango/{sim → demo}/_mover.py +5 -4
  124. ophyd_async/tango/{sim → demo}/_tango/_servers.py +4 -0
  125. ophyd_async/tango/testing/__init__.py +6 -0
  126. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  127. ophyd_async/testing/__init__.py +29 -7
  128. ophyd_async/testing/_assert.py +145 -83
  129. ophyd_async/testing/_mock_signal_utils.py +56 -70
  130. ophyd_async/testing/_one_of_everything.py +41 -21
  131. ophyd_async/testing/_single_derived.py +89 -0
  132. ophyd_async/testing/_utils.py +3 -0
  133. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/METADATA +25 -26
  134. ophyd_async-0.10.0a2.dist-info/RECORD +149 -0
  135. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/WHEEL +1 -1
  136. ophyd_async/epics/sim/__init__.py +0 -54
  137. ophyd_async/epics/sim/_ioc.py +0 -29
  138. ophyd_async/epics/sim/_mover.py +0 -101
  139. ophyd_async/epics/sim/_sensor.py +0 -37
  140. ophyd_async/epics/sim/sensor.db +0 -19
  141. ophyd_async/sim/_pattern_detector/__init__.py +0 -13
  142. ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
  143. ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
  144. ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
  145. ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
  146. ophyd_async/sim/_sim_motor.py +0 -107
  147. ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
  148. /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
  149. /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
  150. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info/licenses}/LICENSE +0 -0
  151. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/top_level.txt +0 -0
@@ -11,10 +11,10 @@ from unittest.mock import Mock
11
11
  import numpy as np
12
12
 
13
13
  T = TypeVar("T")
14
+ V = TypeVar("V")
14
15
  P = ParamSpec("P")
15
16
  Callback = Callable[[T], None]
16
17
  DEFAULT_TIMEOUT = 10.0
17
- ErrorText = str | Mapping[str, Exception]
18
18
 
19
19
  logger = logging.getLogger("ophyd_async")
20
20
 
@@ -29,18 +29,33 @@ class StrictEnumMeta(EnumMeta):
29
29
 
30
30
 
31
31
  class StrictEnum(str, Enum, metaclass=StrictEnumMeta):
32
- """All members should exist in the Backend, and there will be no extras"""
32
+ """All members should exist in the Backend, and there will be no extras."""
33
33
 
34
34
 
35
35
  class SubsetEnumMeta(StrictEnumMeta):
36
36
  def __call__(self, value, *args, **kwargs): # type: ignore
37
+ """Return given value if it is a string and not a member of the enum.
38
+
39
+ If the value is not a string or is an enum member, default enum behavior
40
+ is applied. Type checking will complain if provided arbitrary string.
41
+
42
+ Returns:
43
+ Union[str, SubsetEnum]: If the value is a string and not a member of the
44
+ enum, the string is returned as is. Otherwise, the corresponding enum
45
+ member is returned.
46
+
47
+ Raises:
48
+ ValueError: If the value is not a string and cannot be converted to an enum
49
+ member.
50
+
51
+ """
37
52
  if isinstance(value, str) and not isinstance(value, self):
38
53
  return value
39
54
  return super().__call__(value, *args, **kwargs)
40
55
 
41
56
 
42
57
  class SubsetEnum(StrictEnum, metaclass=SubsetEnumMeta):
43
- """All members should exist in the Backend, but there may be extras"""
58
+ """All members should exist in the Backend, but there may be extras."""
44
59
 
45
60
 
46
61
  CALCULATE_TIMEOUT = "CALCULATE_TIMEOUT"
@@ -55,22 +70,16 @@ CalculatableTimeout = float | None | Literal["CALCULATE_TIMEOUT"]
55
70
 
56
71
 
57
72
  class NotConnected(Exception):
58
- """Exception to be raised if a `Device.connect` is cancelled"""
73
+ """Exception to be raised if a `Device.connect` is cancelled.
59
74
 
60
- _indent_width = " "
75
+ :param errors:
76
+ Mapping of device name to Exception or another NotConnected.
77
+ Alternatively a string with the signal error text.
78
+ """
61
79
 
62
- def __init__(self, errors: ErrorText):
63
- """
64
- NotConnected holds a mapping of device/signal names to
65
- errors.
66
-
67
- Parameters
68
- ----------
69
- errors: ErrorText
70
- Mapping of device name to Exception or another NotConnected.
71
- Alternatively a string with the signal error text.
72
- """
80
+ _indent_width = " "
73
81
 
82
+ def __init__(self, errors: str | Mapping[str, Exception]):
74
83
  self._errors = errors
75
84
 
76
85
  @property
@@ -126,21 +135,38 @@ class NotConnected(Exception):
126
135
 
127
136
  @dataclass(frozen=True)
128
137
  class WatcherUpdate(Generic[T]):
129
- """A dataclass such that, when expanded, it provides the kwargs for a watcher"""
138
+ """A dataclass such that, when expanded, it provides the kwargs for a watcher."""
130
139
 
131
140
  current: T
141
+ """The current value, where it currently is."""
142
+
132
143
  initial: T
144
+ """The initial value, where it was when it started."""
145
+
133
146
  target: T
147
+ """The target value, where it will be when it finishes."""
148
+
134
149
  name: str | None = None
150
+ """An optional name for the device, if available."""
151
+
135
152
  unit: str | None = None
153
+ """Units of the value, if applicable."""
154
+
136
155
  precision: float | None = None
156
+ """How many decimal places the value should be displayed to."""
157
+
137
158
  fraction: float | None = None
159
+ """The fraction of the way between initial and target."""
160
+
138
161
  time_elapsed: float | None = None
162
+ """The time elapsed since the start of the operation."""
163
+
139
164
  time_remaining: float | None = None
165
+ """The time remaining until the operation completes."""
140
166
 
141
167
 
142
168
  async def wait_for_connection(**coros: Awaitable[None]):
143
- """Call many underlying signals, accumulating exceptions and returning them
169
+ """Call many underlying signals, accumulating exceptions and returning them.
144
170
 
145
171
  Expected kwargs should be a mapping of names to coroutine tasks to execute.
146
172
  """
@@ -164,12 +190,15 @@ async def wait_for_connection(**coros: Awaitable[None]):
164
190
 
165
191
 
166
192
  def get_dtype(datatype: type) -> np.dtype:
167
- """Get the runtime dtype from a numpy ndarray type annotation
193
+ """Get the runtime dtype from a numpy ndarray type annotation.
168
194
 
195
+ ```python
169
196
  >>> from ophyd_async.core import Array1D
170
197
  >>> import numpy as np
171
198
  >>> get_dtype(Array1D[np.int8])
172
199
  dtype('int8')
200
+
201
+ ```
173
202
  """
174
203
  if not get_origin(datatype) == np.ndarray:
175
204
  raise TypeError(f"Expected Array1D[dtype], got {datatype}")
@@ -179,12 +208,21 @@ def get_dtype(datatype: type) -> np.dtype:
179
208
 
180
209
 
181
210
  def get_enum_cls(datatype: type | None) -> type[StrictEnum] | None:
182
- """Get the runtime dtype from a numpy ndarray type annotation
183
-
184
- >>> import numpy.typing as npt
185
- >>> import numpy as np
186
- >>> get_dtype(npt.NDArray[np.int8])
187
- dtype('int8')
211
+ """Get the enum class from a datatype.
212
+
213
+ :raises TypeError: if type is not a [](#StrictEnum) or [](#SubsetEnum) subclass
214
+ ```python
215
+ >>> from ophyd_async.core import StrictEnum
216
+ >>> from collections.abc import Sequence
217
+ >>> class MyEnum(StrictEnum):
218
+ ... A = "A value"
219
+ >>> get_enum_cls(str)
220
+ >>> get_enum_cls(MyEnum)
221
+ <enum 'MyEnum'>
222
+ >>> get_enum_cls(Sequence[MyEnum])
223
+ <enum 'MyEnum'>
224
+
225
+ ```
188
226
  """
189
227
  if get_origin(datatype) is Sequence:
190
228
  datatype = get_args(datatype)[0]
@@ -195,17 +233,21 @@ def get_enum_cls(datatype: type | None) -> type[StrictEnum] | None:
195
233
  "or ophyd_async.core.StrictEnum"
196
234
  )
197
235
  return datatype
236
+ return None
198
237
 
199
238
 
200
239
  def get_unique(values: dict[str, T], types: str) -> T:
201
- """If all values are the same, return that value, otherwise raise TypeError
240
+ """If all values are the same, return that value, otherwise raise TypeError.
202
241
 
242
+ ```python
203
243
  >>> get_unique({"a": 1, "b": 1}, "integers")
204
244
  1
205
245
  >>> get_unique({"a": 1, "b": 2}, "integers")
206
246
  Traceback (most recent call last):
207
247
  ...
208
248
  TypeError: Differing integers: a has 1, b has 2
249
+
250
+ ```
209
251
  """
210
252
  set_values = set(values.values())
211
253
  if len(set_values) != 1:
@@ -219,9 +261,12 @@ async def merge_gathered_dicts(
219
261
  ) -> dict[str, T]:
220
262
  """Merge dictionaries produced by a sequence of coroutines.
221
263
 
222
- Can be used for merging ``read()`` or ``describe``. For instance::
264
+ Can be used for merging `read()` or `describe()`.
223
265
 
224
- combined_read = await merge_gathered_dicts(s.read() for s in signals)
266
+ :example:
267
+ ```python
268
+ combined_read = await merge_gathered_dicts(s.read() for s in signals)
269
+ ```
225
270
  """
226
271
  ret: dict[str, T] = {}
227
272
  for result in await asyncio.gather(*coros):
@@ -229,21 +274,18 @@ async def merge_gathered_dicts(
229
274
  return ret
230
275
 
231
276
 
232
- async def gather_list(coros: Iterable[Awaitable[T]]) -> list[T]:
233
- return await asyncio.gather(*coros)
277
+ async def gather_dict(coros: Mapping[T, Awaitable[V]]) -> dict[T, V]:
278
+ """Take named coros and return a dict of their name to their return value."""
279
+ values = await asyncio.gather(*coros.values())
280
+ return dict(zip(coros, values, strict=True))
234
281
 
235
282
 
236
283
  def in_micros(t: float) -> int:
237
- """
238
- Converts between a positive number of seconds and an equivalent
239
- number of microseconds.
240
-
241
- Args:
242
- t (float): A time in seconds
243
- Raises:
244
- ValueError: if t < 0
245
- Returns:
246
- t (int): A time in microseconds, rounded up to the nearest whole microsecond,
284
+ """Convert between a seconds and microseconds.
285
+
286
+ :param t: A time in seconds
287
+ :return: A time in microseconds, rounded up to the nearest whole microsecond
288
+ :raises ValueError: if t < 0
247
289
  """
248
290
  if t < 0:
249
291
  raise ValueError(f"Expected a positive time in seconds, got {t!r}")
@@ -254,6 +296,7 @@ def get_origin_class(annotatation: Any) -> type | None:
254
296
  origin = get_origin(annotatation) or annotatation
255
297
  if isinstance(origin, type):
256
298
  return origin
299
+ return None
257
300
 
258
301
 
259
302
  class Reference(Generic[T]):
@@ -261,16 +304,16 @@ class Reference(Generic[T]):
261
304
 
262
305
  Used to opt out of the naming/parent-child relationship of `Device`.
263
306
 
264
- For example::
265
-
266
- class DeviceWithRefToSignal(Device):
267
- def __init__(self, signal: SignalRW[int]):
268
- self.signal_ref = Reference(signal)
269
- super().__init__()
270
-
271
- def set(self, value) -> AsyncStatus:
272
- return self.signal_ref().set(value + 1)
307
+ :example:
308
+ ```python
309
+ class DeviceWithRefToSignal(Device):
310
+ def __init__(self, signal: SignalRW[int]):
311
+ self.signal_ref = Reference(signal)
312
+ super().__init__()
273
313
 
314
+ def set(self, value) -> AsyncStatus:
315
+ return self.signal_ref().set(value + 1)
316
+ ```
274
317
  """
275
318
 
276
319
  def __init__(self, obj: T):
@@ -289,6 +332,7 @@ class LazyMock:
289
332
  constructed so that when the leaf is created, so are its parents.
290
333
  Any calls to the child are then accessible from the parent mock.
291
334
 
335
+ ```python
292
336
  >>> parent = LazyMock()
293
337
  >>> child = parent.child("child")
294
338
  >>> child_mock = child()
@@ -297,6 +341,8 @@ class LazyMock:
297
341
  >>> parent_mock = parent()
298
342
  >>> parent_mock.mock_calls
299
343
  [call.child()]
344
+
345
+ ```
300
346
  """
301
347
 
302
348
  def __init__(self, name: str = "", parent: LazyMock | None = None) -> None:
@@ -305,6 +351,7 @@ class LazyMock:
305
351
  self._mock: Mock | None = None
306
352
 
307
353
  def child(self, name: str) -> LazyMock:
354
+ """Return a child of this LazyMock with the given name."""
308
355
  return LazyMock(name, self)
309
356
 
310
357
  def __call__(self) -> Mock:
@@ -28,6 +28,8 @@ def enum_representer(dumper: yaml.Dumper, enum: Enum) -> yaml.Node:
28
28
 
29
29
 
30
30
  class YamlSettingsProvider(SettingsProvider):
31
+ """For providing settings from yaml to signals."""
32
+
31
33
  def __init__(self, directory: Path | str):
32
34
  self._directory = Path(directory)
33
35
 
@@ -0,0 +1 @@
1
+ """EPICS support for Signals, and Devices that use them."""
@@ -9,8 +9,8 @@ from ._andor_io import Andor2DriverIO
9
9
 
10
10
 
11
11
  class Andor2Detector(adcore.AreaDetector[Andor2Controller]):
12
- """
13
- Andor 2 area detector device (CCD detector 56fps with full chip readout).
12
+ """Andor 2 area detector device (CCD detector 56fps with full chip readout).
13
+
14
14
  Andor model:DU897_BV.
15
15
  """
16
16
 
@@ -13,10 +13,12 @@ _MAX_NUM_IMAGE = 999_999
13
13
 
14
14
 
15
15
  class Andor2Controller(adcore.ADBaseController[Andor2DriverIO]):
16
+ """For controlling the Andor 2 detector."""
17
+
16
18
  def __init__(
17
19
  self,
18
20
  driver: Andor2DriverIO,
19
- good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
21
+ good_states: frozenset[adcore.ADState] = adcore.DEFAULT_GOOD_STATES,
20
22
  ) -> None:
21
23
  super().__init__(driver, good_states=good_states)
22
24
 
@@ -32,7 +34,7 @@ class Andor2Controller(adcore.ADBaseController[Andor2DriverIO]):
32
34
  self.driver.num_images.set(
33
35
  trigger_info.total_number_of_triggers or _MAX_NUM_IMAGE
34
36
  ),
35
- self.driver.image_mode.set(adcore.ImageMode.MULTIPLE),
37
+ self.driver.image_mode.set(adcore.ADImageMode.MULTIPLE),
36
38
  )
37
39
 
38
40
  def _get_trigger_mode(self, trigger: DetectorTrigger) -> Andor2TriggerMode:
@@ -23,14 +23,12 @@ class Andor2DataType(SubsetEnum):
23
23
 
24
24
 
25
25
  class Andor2DriverIO(ADBaseIO):
26
- """
27
- Epics pv for andor model:DU897_BV as deployed on p99
28
- """
26
+ """Epics pv for andor model:DU897_BV as deployed on p99."""
29
27
 
30
28
  def __init__(self, prefix: str, name: str = "") -> None:
31
29
  super().__init__(prefix, name=name)
32
30
  self.trigger_mode = epics_signal_rw(Andor2TriggerMode, prefix + "TriggerMode")
33
- self.data_type = epics_signal_r(Andor2DataType, prefix + "DataType_RBV")
31
+ self.data_type = epics_signal_r(Andor2DataType, prefix + "DataType_RBV") # type: ignore
34
32
  self.andor_accumulate_period = epics_signal_r(
35
33
  float, prefix + "AndorAccumulatePeriod_RBV"
36
34
  )
@@ -1,3 +1,8 @@
1
+ """Support for the ADAravis areaDetector driver.
2
+
3
+ https://github.com/areaDetector/ADAravis
4
+ """
5
+
1
6
  from ._aravis import AravisDetector
2
7
  from ._aravis_controller import AravisController
3
8
  from ._aravis_io import AravisDriverIO, AravisTriggerMode, AravisTriggerSource
@@ -1,7 +1,6 @@
1
1
  from collections.abc import Sequence
2
2
 
3
- from ophyd_async.core import PathProvider
4
- from ophyd_async.core._signal import SignalR
3
+ from ophyd_async.core import PathProvider, SignalR
5
4
  from ophyd_async.epics import adcore
6
5
 
7
6
  from ._aravis_controller import AravisController
@@ -9,8 +8,8 @@ from ._aravis_io import AravisDriverIO
9
8
 
10
9
 
11
10
  class AravisDetector(adcore.AreaDetector[AravisController]):
12
- """
13
- Ophyd-async implementation of an ADAravis Detector.
11
+ """Implementation of an ADAravis Detector.
12
+
14
13
  The detector may be configured for an external trigger on a GPIO port,
15
14
  which must be done prior to preparing the detector
16
15
  """
@@ -23,13 +22,11 @@ class AravisDetector(adcore.AreaDetector[AravisController]):
23
22
  writer_cls: type[adcore.ADWriter] = adcore.ADHDFWriter,
24
23
  fileio_suffix: str | None = None,
25
24
  name: str = "",
26
- gpio_number: AravisController.GPIO_NUMBER = 1,
27
25
  config_sigs: Sequence[SignalR] = (),
28
26
  plugins: dict[str, adcore.NDPluginBaseIO] | None = None,
29
27
  ):
30
28
  driver = AravisDriverIO(prefix + drv_suffix)
31
- controller = AravisController(driver, gpio_number=gpio_number)
32
-
29
+ controller = AravisController(driver)
33
30
  writer = writer_cls.with_io(
34
31
  prefix,
35
32
  path_provider,
@@ -37,7 +34,6 @@ class AravisDetector(adcore.AreaDetector[AravisController]):
37
34
  fileio_suffix=fileio_suffix,
38
35
  plugins=plugins,
39
36
  )
40
-
41
37
  super().__init__(
42
38
  controller=controller,
43
39
  writer=writer,
@@ -1,10 +1,6 @@
1
1
  import asyncio
2
- from typing import Literal
3
2
 
4
- from ophyd_async.core import (
5
- DetectorTrigger,
6
- TriggerInfo,
7
- )
3
+ from ophyd_async.core import DetectorTrigger, TriggerInfo
8
4
  from ophyd_async.epics import adcore
9
5
 
10
6
  from ._aravis_io import AravisDriverIO, AravisTriggerMode, AravisTriggerSource
@@ -16,53 +12,34 @@ _HIGHEST_POSSIBLE_DEADTIME = 1961e-6
16
12
 
17
13
 
18
14
  class AravisController(adcore.ADBaseController[AravisDriverIO]):
19
- GPIO_NUMBER = Literal[1, 2, 3, 4]
20
-
21
- def __init__(
22
- self,
23
- driver: AravisDriverIO,
24
- good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
25
- gpio_number: GPIO_NUMBER = 1,
26
- ) -> None:
27
- super().__init__(driver, good_states=good_states)
28
- self.gpio_number = gpio_number
15
+ """`DetectorController` for an `AravisDriverIO`."""
29
16
 
30
17
  def get_deadtime(self, exposure: float | None) -> float:
31
18
  return _HIGHEST_POSSIBLE_DEADTIME
32
19
 
33
- async def prepare(self, trigger_info: TriggerInfo):
34
- if trigger_info.total_number_of_triggers == 0:
35
- image_mode = adcore.ImageMode.CONTINUOUS
36
- else:
37
- image_mode = adcore.ImageMode.MULTIPLE
20
+ async def prepare(self, trigger_info: TriggerInfo) -> None:
38
21
  if (exposure := trigger_info.livetime) is not None:
39
22
  await self.driver.acquire_time.set(exposure)
40
23
 
41
- trigger_mode, trigger_source = self._get_trigger_info(trigger_info.trigger)
42
- # trigger mode must be set first and on it's own!
43
- await self.driver.trigger_mode.set(trigger_mode)
24
+ if trigger_info.trigger is DetectorTrigger.INTERNAL:
25
+ # Set trigger mode off to ignore the trigger source
26
+ await self.driver.trigger_mode.set(AravisTriggerMode.OFF)
27
+ elif trigger_info.trigger in {
28
+ DetectorTrigger.CONSTANT_GATE,
29
+ DetectorTrigger.EDGE_TRIGGER,
30
+ }:
31
+ # Trigger on the rising edge of Line1
32
+ # trigger mode must be set first and on it's own!
33
+ await self.driver.trigger_mode.set(AravisTriggerMode.ON)
34
+ await self.driver.trigger_source.set(AravisTriggerSource.LINE1)
35
+ else:
36
+ raise ValueError(f"ADAravis does not support {trigger_info.trigger}")
44
37
 
38
+ if trigger_info.total_number_of_triggers == 0:
39
+ image_mode = adcore.ADImageMode.CONTINUOUS
40
+ else:
41
+ image_mode = adcore.ADImageMode.MULTIPLE
45
42
  await asyncio.gather(
46
- self.driver.trigger_source.set(trigger_source),
47
43
  self.driver.num_images.set(trigger_info.total_number_of_triggers),
48
44
  self.driver.image_mode.set(image_mode),
49
45
  )
50
-
51
- def _get_trigger_info(
52
- self, trigger: DetectorTrigger
53
- ) -> tuple[AravisTriggerMode, AravisTriggerSource]:
54
- supported_trigger_types = (
55
- DetectorTrigger.CONSTANT_GATE,
56
- DetectorTrigger.EDGE_TRIGGER,
57
- DetectorTrigger.INTERNAL,
58
- )
59
- if trigger not in supported_trigger_types:
60
- raise ValueError(
61
- f"{self.__class__.__name__} only supports the following trigger "
62
- f"types: {supported_trigger_types} but was asked to "
63
- f"use {trigger}"
64
- )
65
- if trigger == DetectorTrigger.INTERNAL:
66
- return AravisTriggerMode.OFF, AravisTriggerSource.FREERUN
67
- else:
68
- return (AravisTriggerMode.ON, f"Line{self.gpio_number}") # type: ignore
@@ -1,46 +1,31 @@
1
- from ophyd_async.core import StrictEnum, SubsetEnum
1
+ from typing import Annotated as A
2
+
3
+ from ophyd_async.core import SignalRW, StrictEnum, SubsetEnum
2
4
  from ophyd_async.epics import adcore
3
- from ophyd_async.epics.core import epics_signal_rw_rbv
5
+ from ophyd_async.epics.core import PvSuffix
4
6
 
5
7
 
6
8
  class AravisTriggerMode(StrictEnum):
7
- """GigEVision GenICAM standard: on=externally triggered"""
9
+ """GigEVision GenICAM standard TriggerMode."""
8
10
 
9
11
  ON = "On"
10
- OFF = "Off"
12
+ """Use TriggerSource to trigger each frame"""
11
13
 
12
-
13
- """A minimal set of TriggerSources that must be supported by the underlying record.
14
- To enable hardware triggered scanning, line_N must support each N in GPIO_NUMBER.
15
- To enable software triggered scanning, freerun must be supported.
16
- Other enumerated values may or may not be preset.
17
- To prevent requiring one Enum class per possible configuration, we set as this Enum
18
- but read from the underlying signal as a str.
19
- """
14
+ OFF = "Off"
15
+ """Just trigger as fast as you can"""
20
16
 
21
17
 
22
18
  class AravisTriggerSource(SubsetEnum):
23
- FREERUN = "Freerun"
19
+ """Which trigger source to use when TriggerMode=On."""
20
+
24
21
  LINE1 = "Line1"
25
22
 
26
23
 
27
24
  class AravisDriverIO(adcore.ADBaseIO):
28
- # If instantiating a new instance, ensure it is supported in the _deadtimes dict
29
- """Generic Driver supporting the Manta and Mako drivers.
30
- Fetches deadtime prior to use in a Streaming scan.
31
-
32
- Requires driver firmware up to date:
33
- - Model_RBV must be of the form "^(Mako|Manta) (model)$"
25
+ """Generic Driver supporting all GiGE cameras.
34
26
 
35
27
  This mirrors the interface provided by ADAravis/db/aravisCamera.template.
36
28
  """
37
29
 
38
- def __init__(self, prefix: str, name: str = "") -> None:
39
- self.trigger_mode = epics_signal_rw_rbv(
40
- AravisTriggerMode, prefix + "TriggerMode"
41
- )
42
- self.trigger_source = epics_signal_rw_rbv(
43
- AravisTriggerSource, # type: ignore
44
- prefix + "TriggerSource",
45
- )
46
- super().__init__(prefix, name=name)
30
+ trigger_mode: A[SignalRW[AravisTriggerMode], PvSuffix.rbv("TriggerMode")]
31
+ trigger_source: A[SignalRW[AravisTriggerSource], PvSuffix.rbv("TriggerSource")]
@@ -1,15 +1,24 @@
1
- from ._core_detector import AreaDetector
1
+ """Core components of the areaDetector software.
2
+
3
+ https://github.com/areaDetector/ADCore
4
+ """
5
+
6
+ from ._core_detector import AreaDetector, ContAcqAreaDetector
2
7
  from ._core_io import (
3
8
  ADBaseDatasetDescriber,
4
9
  ADBaseIO,
5
- DetectorState,
10
+ ADCallbacks,
11
+ ADCompression,
12
+ ADState,
6
13
  NDArrayBaseIO,
14
+ NDCBFlushOnSoftTrgMode,
7
15
  NDFileHDFIO,
8
16
  NDFileIO,
9
17
  NDPluginBaseIO,
18
+ NDPluginCBIO,
10
19
  NDPluginStatsIO,
11
20
  )
12
- from ._core_logic import DEFAULT_GOOD_STATES, ADBaseController
21
+ from ._core_logic import DEFAULT_GOOD_STATES, ADBaseContAcqController, ADBaseController
13
22
  from ._core_writer import ADWriter
14
23
  from ._hdf_writer import ADHDFWriter
15
24
  from ._jpeg_writer import ADJPEGWriter
@@ -17,8 +26,8 @@ from ._single_trigger import SingleTriggerDetector
17
26
  from ._tiff_writer import ADTIFFWriter
18
27
  from ._utils import (
19
28
  ADBaseDataType,
20
- FileWriteMode,
21
- ImageMode,
29
+ ADFileWriteMode,
30
+ ADImageMode,
22
31
  NDAttributeDataType,
23
32
  NDAttributeParam,
24
33
  NDAttributePv,
@@ -28,8 +37,12 @@ from ._utils import (
28
37
 
29
38
  __all__ = [
30
39
  "ADBaseIO",
40
+ "ADCallbacks",
41
+ "ADCompression",
42
+ "ADBaseContAcqController",
31
43
  "AreaDetector",
32
- "DetectorState",
44
+ "ADState",
45
+ "ContAcqAreaDetector",
33
46
  "NDArrayBaseIO",
34
47
  "NDFileIO",
35
48
  "NDFileHDFIO",
@@ -44,11 +57,13 @@ __all__ = [
44
57
  "ADJPEGWriter",
45
58
  "SingleTriggerDetector",
46
59
  "ADBaseDataType",
47
- "FileWriteMode",
48
- "ImageMode",
60
+ "ADFileWriteMode",
61
+ "ADImageMode",
49
62
  "NDAttributePv",
50
63
  "NDAttributeParam",
51
64
  "NDAttributeDataType",
52
65
  "stop_busy_record",
53
66
  "NDAttributePvDbrType",
67
+ "NDCBFlushOnSoftTrgMode",
68
+ "NDPluginCBIO",
54
69
  ]
@@ -1,10 +1,12 @@
1
1
  from collections.abc import Sequence
2
2
 
3
3
  from ophyd_async.core import SignalR, StandardDetector
4
+ from ophyd_async.core._providers import PathProvider
4
5
 
5
- from ._core_io import NDPluginBaseIO
6
- from ._core_logic import ADBaseControllerT
6
+ from ._core_io import ADBaseIO, NDPluginBaseIO, NDPluginCBIO
7
+ from ._core_logic import ADBaseContAcqController, ADBaseControllerT
7
8
  from ._core_writer import ADWriter
9
+ from ._hdf_writer import ADHDFWriter
8
10
 
9
11
 
10
12
  class AreaDetector(StandardDetector[ADBaseControllerT, ADWriter]):
@@ -39,3 +41,41 @@ class AreaDetector(StandardDetector[ADBaseControllerT, ADWriter]):
39
41
  f"Expected {self.name}.{name} to be a {plugin_type}, got {plugin}"
40
42
  )
41
43
  return plugin
44
+
45
+
46
+ class ContAcqAreaDetector(AreaDetector[ADBaseContAcqController]):
47
+ """Ophyd-async implementation of a continuously acquiring AreaDetector."""
48
+
49
+ def __init__(
50
+ self,
51
+ prefix: str,
52
+ path_provider: PathProvider,
53
+ drv_cls: type[ADBaseIO] = ADBaseIO,
54
+ drv_suffix: str = "cam1:",
55
+ cb_suffix: str = "CB1:",
56
+ writer_cls: type[ADWriter] = ADHDFWriter,
57
+ fileio_suffix: str | None = None,
58
+ name: str = "",
59
+ plugins: dict[str, NDPluginBaseIO] | None = None,
60
+ config_sigs: Sequence[SignalR] = (),
61
+ ):
62
+ self.cb_plugin = NDPluginCBIO(prefix + cb_suffix)
63
+ driver = drv_cls(prefix + drv_suffix)
64
+ controller = ADBaseContAcqController(driver, self.cb_plugin)
65
+
66
+ writer = writer_cls.with_io(
67
+ prefix,
68
+ path_provider,
69
+ # Since the CB plugin controls acq, use it when checking shape
70
+ dataset_source=self.cb_plugin,
71
+ fileio_suffix=fileio_suffix,
72
+ plugins=plugins,
73
+ )
74
+
75
+ super().__init__(
76
+ controller=controller,
77
+ writer=writer,
78
+ plugins=plugins,
79
+ name=name,
80
+ config_sigs=config_sigs,
81
+ )