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
@@ -1,9 +1,10 @@
1
- """Module which defines abstract classes to work with detectors"""
1
+ """Module which defines abstract classes to work with detectors."""
2
2
 
3
3
  import asyncio
4
4
  import time
5
5
  from abc import ABC, abstractmethod
6
6
  from collections.abc import AsyncGenerator, AsyncIterator, Callable, Iterator, Sequence
7
+ from enum import Enum
7
8
  from functools import cached_property
8
9
  from typing import (
9
10
  Generic,
@@ -28,48 +29,62 @@ from ._device import Device, DeviceConnector
28
29
  from ._protocol import AsyncConfigurable, AsyncReadable
29
30
  from ._signal import SignalR
30
31
  from ._status import AsyncStatus, WatchableAsyncStatus
31
- from ._utils import DEFAULT_TIMEOUT, StrictEnum, WatcherUpdate, merge_gathered_dicts
32
+ from ._utils import DEFAULT_TIMEOUT, WatcherUpdate, merge_gathered_dicts
32
33
 
33
34
 
34
- class DetectorTrigger(StrictEnum):
35
- """Type of mechanism for triggering a detector to take frames"""
35
+ class DetectorTrigger(Enum):
36
+ """Type of mechanism for triggering a detector to take frames."""
36
37
 
37
- #: Detector generates internal trigger for given rate
38
- INTERNAL = "internal"
39
- #: Expect a series of arbitrary length trigger signals
40
- EDGE_TRIGGER = "edge_trigger"
41
- #: Expect a series of constant width external gate signals
42
- CONSTANT_GATE = "constant_gate"
43
- #: Expect a series of variable width external gate signals
44
- VARIABLE_GATE = "variable_gate"
38
+ INTERNAL = "INTERNAL"
39
+ """Detector generates internal trigger for given rate"""
40
+
41
+ EDGE_TRIGGER = "EDGE_TRIGGER"
42
+ """Expect a series of arbitrary length trigger signals"""
43
+
44
+ CONSTANT_GATE = "CONSTANT_GATE"
45
+ """Expect a series of constant width external gate signals"""
46
+
47
+ VARIABLE_GATE = "VARIABLE_GATE"
48
+ """Expect a series of variable width external gate signals"""
45
49
 
46
50
 
47
51
  class TriggerInfo(BaseModel):
48
- """Minimal set of information required to setup triggering on a detector"""
49
-
50
- #: Number of triggers that will be sent, (0 means infinite) Can be:
51
- # - A single integer or
52
- # - A list of integers for multiple triggers
53
- # Example for tomography: TriggerInfo(number=[2,3,100,3])
54
- #: This would trigger:
55
- #: - 2 times for dark field images
56
- #: - 3 times for initial flat field images
57
- #: - 100 times for projections
58
- #: - 3 times for final flat field images
59
- number_of_triggers: NonNegativeInt | list[NonNegativeInt]
60
- #: Sort of triggers that will be sent
52
+ """Minimal set of information required to setup triggering on a detector."""
53
+
54
+ number_of_triggers: NonNegativeInt | list[NonNegativeInt] = Field(default=1)
55
+ """Number of triggers that will be sent, (0 means infinite).
56
+
57
+ Can be:
58
+ - A single integer or
59
+ - A list of integers for multiple triggers
60
+
61
+ Example for tomography: ``TriggerInfo(number=[2,3,100,3])``.
62
+ This would trigger:
63
+
64
+ - 2 times for dark field images
65
+ - 3 times for initial flat field images
66
+ - 100 times for projections
67
+ - 3 times for final flat field images
68
+ """
69
+
61
70
  trigger: DetectorTrigger = Field(default=DetectorTrigger.INTERNAL)
62
- #: What is the minimum deadtime between triggers
63
- deadtime: float | None = Field(default=None, ge=0)
64
- #: What is the maximum high time of the triggers
71
+ """Sort of triggers that will be sent"""
72
+
73
+ deadtime: float = Field(default=0.0, ge=0)
74
+ """What is the minimum deadtime between triggers"""
75
+
65
76
  livetime: float | None = Field(default=None, ge=0)
66
- #: What is the maximum timeout on waiting for a frame
77
+ """What is the maximum high time of the triggers"""
78
+
67
79
  frame_timeout: float | None = Field(default=None, gt=0)
68
- #: How many triggers make up a single StreamDatum index, to allow multiple frames
69
- #: from a faster detector to be zipped with a single frame from a slow detector
70
- #: e.g. if num=10 and multiplier=5 then the detector will take 10 frames,
71
- #: but publish 2 indices, and describe() will show a shape of (5, h, w)
80
+ """What is the maximum timeout on waiting for a frame"""
81
+
72
82
  multiplier: int = 1
83
+ """How many triggers make up a single StreamDatum index, to allow multiple frames
84
+ from a faster detector to be zipped with a single frame from a slow detector
85
+ e.g. if num=10 and multiplier=5 then the detector will take 10 frames,
86
+ but publish 2 indices, and describe() will show a shape of (5, h, w)
87
+ """
73
88
 
74
89
  @computed_field
75
90
  @cached_property
@@ -82,89 +97,68 @@ class TriggerInfo(BaseModel):
82
97
 
83
98
 
84
99
  class DetectorController(ABC):
85
- """
86
- Classes implementing this interface should hold the logic for
87
- arming and disarming a detector
88
- """
100
+ """Detector logic for arming and disarming the detector."""
89
101
 
90
102
  @abstractmethod
91
103
  def get_deadtime(self, exposure: float | None) -> float:
92
- """For a given exposure, how long should the time between exposures be"""
104
+ """For a given exposure, how long should the time between exposures be."""
93
105
 
94
106
  @abstractmethod
95
107
  async def prepare(self, trigger_info: TriggerInfo) -> None:
96
- """
97
- Do all necessary steps to prepare the detector for triggers.
98
-
99
- Args:
100
- trigger_info: This is a Pydantic model which contains
101
- number Expected number of frames.
102
- trigger Type of trigger for which to prepare the detector. Defaults
103
- to DetectorTrigger.internal.
104
- livetime Livetime / Exposure time with which to set up the detector.
105
- Defaults to None
106
- if not applicable or the detector is expected to use its previously-set
107
- exposure time.
108
- deadtime Defaults to None. This is the minimum deadtime between
109
- triggers.
110
- multiplier The number of triggers grouped into a single StreamDatum
111
- index.
108
+ """Do all necessary steps to prepare the detector for triggers.
109
+
110
+ :param trigger_info: The sort of triggers to expect.
112
111
  """
113
112
 
114
113
  @abstractmethod
115
114
  async def arm(self) -> None:
116
- """
117
- Arm the detector
118
- """
115
+ """Arm the detector."""
119
116
 
120
117
  @abstractmethod
121
118
  async def wait_for_idle(self):
122
- """
123
- This will wait on the internal _arm_status and wait for it to get disarmed/idle
124
- """
119
+ """Wait on the internal _arm_status and wait for it to get disarmed/idle."""
125
120
 
126
121
  @abstractmethod
127
122
  async def disarm(self):
128
- """Disarm the detector, return detector to an idle state"""
123
+ """Disarm the detector, return detector to an idle state."""
129
124
 
130
125
 
131
126
  class DetectorWriter(ABC):
132
- """Logic for making a detector write data to somewhere persistent
133
- (e.g. an HDF5 file)"""
127
+ """Logic for making detector write data to somewhere persistent (e.g. HDF5 file)."""
134
128
 
135
129
  @abstractmethod
136
130
  async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
137
131
  """Open writer and wait for it to be ready for data.
138
132
 
139
- Args:
140
- multiplier: Each StreamDatum index corresponds to this many
141
- written exposures
142
-
143
- Returns:
144
- Output for ``describe()``
133
+ :param multiplier:
134
+ Each StreamDatum index corresponds to this many written exposures
135
+ :return: Output for ``describe()``
145
136
  """
146
137
 
147
- @abstractmethod
148
- def observe_indices_written(
149
- self, timeout=DEFAULT_TIMEOUT
150
- ) -> AsyncGenerator[int, None]:
151
- """Yield the index of each frame (or equivalent data point) as it is written"""
138
+ @property
139
+ def hints(self) -> Hints:
140
+ """The hints to be used for the detector."""
141
+ return {}
152
142
 
153
143
  @abstractmethod
154
144
  async def get_indices_written(self) -> int:
155
- """Get the number of indices written"""
145
+ """Get the number of indices written."""
146
+
147
+ # Note: this method is really async, but if we make it async here then we
148
+ # need to give it a body with a redundant yield statement, which is a bit
149
+ # awkward. So we just leave it as a regular method and let the user
150
+ # implement it as async.
151
+ @abstractmethod
152
+ def observe_indices_written(self, timeout: float) -> AsyncGenerator[int, None]:
153
+ """Yield the index of each frame (or equivalent data point) as it is written."""
156
154
 
157
155
  @abstractmethod
158
156
  def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
159
- """Create Stream docs up to given number written"""
157
+ """Create Stream docs up to given number written."""
160
158
 
161
159
  @abstractmethod
162
160
  async def close(self) -> None:
163
- """Close writer, blocks until I/O is complete"""
164
-
165
- @property
166
- def hints(self) -> Hints:
167
- return {}
161
+ """Close writer, blocks until I/O is complete."""
168
162
 
169
163
 
170
164
  # Add type var for controller so we can define
@@ -192,9 +186,14 @@ class StandardDetector(
192
186
  WritesStreamAssets,
193
187
  Generic[DetectorControllerT, DetectorWriterT],
194
188
  ):
195
- """
196
- Useful detector base class for step and fly scanning detectors.
189
+ """Detector base class for step and fly scanning detectors.
190
+
197
191
  Aggregates controller and writer logic together.
192
+
193
+ :param controller: Logic for arming and disarming the detector
194
+ :param writer: Logic for making the detector write persistent data
195
+ :param config_sigs: Signals to read when describe and read configuration are called
196
+ :param name: Device name
198
197
  """
199
198
 
200
199
  def __init__(
@@ -205,16 +204,6 @@ class StandardDetector(
205
204
  name: str = "",
206
205
  connector: DeviceConnector | None = None,
207
206
  ) -> None:
208
- """
209
- Constructor
210
-
211
- Args:
212
- controller: Logic for arming and disarming the detector
213
- writer: Logic for making the detector write persistent data
214
- config_sigs: Signals to read when describe and read
215
- configuration are called. Defaults to ().
216
- name: Device name. Defaults to "".
217
- """
218
207
  self._controller = controller
219
208
  self._writer = writer
220
209
  self._describe: dict[str, DataKey] = {}
@@ -236,13 +225,13 @@ class StandardDetector(
236
225
 
237
226
  @AsyncStatus.wrap
238
227
  async def stage(self) -> None:
239
- # Disarm the detector, stop file writing.
228
+ """Make sure the detector is idle and ready to be used."""
240
229
  await self._check_config_sigs()
241
230
  await asyncio.gather(self._writer.close(), self._controller.disarm())
242
231
  self._trigger_info = None
243
232
 
244
233
  async def _check_config_sigs(self):
245
- """Checks configuration signals are named and connected."""
234
+ """Check configuration signals are named and connected."""
246
235
  for signal in self._config_sigs:
247
236
  if signal.name == "":
248
237
  raise Exception(
@@ -258,7 +247,7 @@ class StandardDetector(
258
247
 
259
248
  @AsyncStatus.wrap
260
249
  async def unstage(self) -> None:
261
- # Stop data writing.
250
+ """Disarm the detector and stop file writing."""
262
251
  await asyncio.gather(self._writer.close(), self._controller.disarm())
263
252
 
264
253
  async def read_configuration(self) -> dict[str, Reading]:
@@ -268,6 +257,7 @@ class StandardDetector(
268
257
  return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
269
258
 
270
259
  async def read(self) -> dict[str, Reading]:
260
+ """There is no data to be placed in events, so this is empty."""
271
261
  # All data is in StreamResources, not Events, so nothing to output here
272
262
  return {}
273
263
 
@@ -281,14 +271,11 @@ class StandardDetector(
281
271
  TriggerInfo(
282
272
  number_of_triggers=1,
283
273
  trigger=DetectorTrigger.INTERNAL,
284
- deadtime=None,
285
- livetime=None,
286
- frame_timeout=None,
287
274
  )
288
275
  )
289
276
 
290
- self._trigger_info = _ensure_trigger_info_exists(self._trigger_info)
291
- if self._trigger_info.trigger is not DetectorTrigger.INTERNAL:
277
+ trigger_info = _ensure_trigger_info_exists(self._trigger_info)
278
+ if trigger_info.trigger is not DetectorTrigger.INTERNAL:
292
279
  msg = "The trigger method can only be called with INTERNAL triggering"
293
280
  raise ValueError(msg)
294
281
 
@@ -299,28 +286,19 @@ class StandardDetector(
299
286
  end_observation = indices_written + 1
300
287
 
301
288
  async for index in self._writer.observe_indices_written(
302
- DEFAULT_TIMEOUT
303
- + (self._trigger_info.livetime or 0)
304
- + (self._trigger_info.deadtime or 0)
289
+ DEFAULT_TIMEOUT + (trigger_info.livetime or 0) + trigger_info.deadtime
305
290
  ):
306
291
  if index >= end_observation:
307
292
  break
308
293
 
309
294
  @AsyncStatus.wrap
310
295
  async def prepare(self, value: TriggerInfo) -> None:
311
- """
312
- Arm detector.
296
+ """Arm detector.
313
297
 
314
298
  Prepare the detector with trigger information. This is determined at and passed
315
299
  in from the plan level.
316
300
 
317
- This currently only prepares detectors for flyscans and stepscans just use the
318
- trigger information determined in trigger.
319
-
320
- To do: Unify prepare to be use for both fly and step scans.
321
-
322
- Args:
323
- value: TriggerInfo describing how to trigger the detector
301
+ :param value: TriggerInfo describing how to trigger the detector
324
302
  """
325
303
  if value.trigger != DetectorTrigger.INTERNAL and not value.deadtime:
326
304
  msg = "Deadtime must be supplied when in externally triggered mode"
@@ -332,25 +310,28 @@ class StandardDetector(
332
310
  f"deadtime, but trigger logic provides only {value.deadtime}s"
333
311
  )
334
312
  raise ValueError(msg)
335
-
336
- self._trigger_info = value
313
+ elif not value.deadtime:
314
+ value.deadtime = self._controller.get_deadtime(value.livetime)
337
315
  self._number_of_triggers_iter = iter(
338
- self._trigger_info.number_of_triggers
339
- if isinstance(self._trigger_info.number_of_triggers, list)
340
- else [self._trigger_info.number_of_triggers]
316
+ value.number_of_triggers
317
+ if isinstance(value.number_of_triggers, list)
318
+ else [value.number_of_triggers]
341
319
  )
342
- self._initial_frame = await self._writer.get_indices_written()
343
320
  self._describe, _ = await asyncio.gather(
344
321
  self._writer.open(value.multiplier), self._controller.prepare(value)
345
322
  )
323
+ self._initial_frame = await self._writer.get_indices_written()
346
324
  if value.trigger != DetectorTrigger.INTERNAL:
347
325
  await self._controller.arm()
348
- self._fly_start = time.monotonic()
326
+ self._trigger_info = value
349
327
 
350
328
  @AsyncStatus.wrap
351
329
  async def kickoff(self):
352
330
  if self._trigger_info is None or self._number_of_triggers_iter is None:
353
331
  raise RuntimeError("Prepare must be called before kickoff!")
332
+ if self._trigger_info.trigger == DetectorTrigger.INTERNAL:
333
+ await self._controller.arm()
334
+ self._fly_start = time.monotonic()
354
335
  try:
355
336
  self._frames_to_complete = next(self._number_of_triggers_iter)
356
337
  self._completable_frames += self._frames_to_complete
@@ -362,13 +343,13 @@ class StandardDetector(
362
343
 
363
344
  @WatchableAsyncStatus.wrap
364
345
  async def complete(self):
365
- self._trigger_info = _ensure_trigger_info_exists(self._trigger_info)
346
+ trigger_info = _ensure_trigger_info_exists(self._trigger_info)
366
347
  indices_written = self._writer.observe_indices_written(
367
- self._trigger_info.frame_timeout
348
+ trigger_info.frame_timeout
368
349
  or (
369
350
  DEFAULT_TIMEOUT
370
- + (self._trigger_info.livetime or 0)
371
- + (self._trigger_info.deadtime or 0)
351
+ + (trigger_info.livetime or 0)
352
+ + (trigger_info.deadtime or 0)
372
353
  )
373
354
  )
374
355
  try:
@@ -388,7 +369,7 @@ class StandardDetector(
388
369
  break
389
370
  finally:
390
371
  await indices_written.aclose()
391
- if self._completable_frames >= self._trigger_info.total_number_of_triggers:
372
+ if self._completable_frames >= trigger_info.total_number_of_triggers:
392
373
  self._completable_frames = 0
393
374
  self._frames_to_complete = 0
394
375
  self._number_of_triggers_iter = None
@@ -17,7 +17,7 @@ class DeviceConnector:
17
17
  """Defines how a `Device` should be connected and type hints processed."""
18
18
 
19
19
  def create_children_from_annotations(self, device: Device):
20
- """Used when children can be created from introspecting the hardware.
20
+ """Use when children can be created from introspecting the hardware.
21
21
 
22
22
  Some control systems allow introspection of a device to determine what
23
23
  children it has. To allow this to work nicely with typing we add these
@@ -26,15 +26,20 @@ class DeviceConnector:
26
26
  my_signal: SignalRW[int]
27
27
  my_device: MyDevice
28
28
 
29
- This method will be run during ``Device.__init__``, and is responsible
29
+ This method will be run during `Device.__init__`, and is responsible
30
30
  for turning all of those type hints into real Signal and Device instances.
31
31
 
32
32
  Subsequent runs of this function should do nothing, to allow it to be
33
33
  called early in Devices that need to pass references to their children
34
- during ``__init__``.
34
+ during `__init__`.
35
35
  """
36
36
 
37
37
  async def connect_mock(self, device: Device, mock: LazyMock):
38
+ """Use during [](#Device.connect) with `mock=True`.
39
+
40
+ This is called when there is no cached connect done in `mock=True`
41
+ mode. It connects the Device and all its children in mock mode.
42
+ """
38
43
  # Connect serially, no errors to gather up as in mock mode
39
44
  exceptions: dict[str, Exception] = {}
40
45
  for name, child_device in device.children():
@@ -46,11 +51,10 @@ class DeviceConnector:
46
51
  raise NotConnected.with_other_exceptions_logged(exceptions)
47
52
 
48
53
  async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
49
- """Used during ``Device.connect``.
54
+ """Use during [](#Device.connect) with `mock=False`.
50
55
 
51
- This is called when a previous connect has not been done, or has been
52
- done in a different mock more. It should connect the Device and all its
53
- children.
56
+ This is called when there is no cached connect done in `mock=False`
57
+ mode. It connects the Device and all its children in real mode in parallel.
54
58
  """
55
59
  # Connect in parallel, gathering up NotConnected errors
56
60
  coros = {
@@ -61,11 +65,15 @@ class DeviceConnector:
61
65
 
62
66
 
63
67
  class Device(HasName):
64
- """Common base class for all Ophyd Async Devices."""
68
+ """Common base class for all Ophyd Async Devices.
69
+
70
+ :param name: Optional name of the Device
71
+ :param connector: Optional DeviceConnector instance to use at connect()
72
+ """
65
73
 
66
- _name: str = ""
67
- #: The parent Device if it exists
68
74
  parent: Device | None = None
75
+ """The parent Device if it exists"""
76
+ _name: str = ""
69
77
  # None if connect hasn't started, a Task if it has
70
78
  _connect_task: asyncio.Task | None = None
71
79
  # The mock if we have connected in mock mode
@@ -83,7 +91,7 @@ class Device(HasName):
83
91
 
84
92
  @property
85
93
  def name(self) -> str:
86
- """Return the name of the Device"""
94
+ """Return the name of the Device."""
87
95
  return self._name
88
96
 
89
97
  @cached_property
@@ -91,24 +99,26 @@ class Device(HasName):
91
99
  return {}
92
100
 
93
101
  def children(self) -> Iterator[tuple[str, Device]]:
102
+ """For each attribute that is a Device, yield the name and Device.
103
+
104
+ :yields: `(attr_name, attr)` for each child attribute that is a Device.
105
+ """
94
106
  yield from self._child_devices.items()
95
107
 
96
108
  @cached_property
97
109
  def log(self) -> LoggerAdapter:
110
+ """Return a logger configured with the device name."""
98
111
  return LoggerAdapter(
99
112
  getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
100
113
  )
101
114
 
102
115
  def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
103
- """Set ``self.name=name`` and each ``self.child.name=name+"-child"``.
104
-
105
- Parameters
106
- ----------
107
- name:
108
- New name to set
109
- child_name_separator:
110
- Use this as a separator instead of "-". Use "_" instead to make the same
111
- names as the equivalent ophyd sync device.
116
+ """Set `self.name=name` and each `self.child.name=name+"-child"`.
117
+
118
+ :param name: New name to set.
119
+ :param child_name_separator:
120
+ Use this as a separator instead of "-". Use "_" instead to make the
121
+ same names as the equivalent ophyd sync device.
112
122
  """
113
123
  self._name = name
114
124
  if child_name_separator:
@@ -147,16 +157,19 @@ class Device(HasName):
147
157
  timeout: float = DEFAULT_TIMEOUT,
148
158
  force_reconnect: bool = False,
149
159
  ) -> None:
150
- """Connect self and all child Devices.
151
-
152
- Contains a timeout that gets propagated to child.connect methods.
153
-
154
- Parameters
155
- ----------
156
- mock:
157
- If True then use ``MockSignalBackend`` for all Signals
158
- timeout:
159
- Time to wait before failing with a TimeoutError.
160
+ """Connect the device and all child devices.
161
+
162
+ Successful connects will be cached so subsequent calls will return
163
+ immediately. Contains a timeout that gets propagated to child.connect
164
+ methods.
165
+
166
+ :param mock:
167
+ If True then use [](#MockSignalBackend) for all Signals. If passed a
168
+ [](#LazyMock) then pass this down for use within the Signals,
169
+ otherwise create one.
170
+ :param timeout: Time to wait before failing with a TimeoutError.
171
+ :param force_reconnect:
172
+ If True, force a reconnect even if the last connect succeeded.
160
173
  """
161
174
  if not hasattr(self, "_connector"):
162
175
  msg = (
@@ -205,12 +218,9 @@ DeviceT = TypeVar("DeviceT", bound=Device)
205
218
 
206
219
 
207
220
  class DeviceVector(MutableMapping[int, DeviceT], Device):
208
- """
209
- Defines device components with indices.
221
+ """Defines a dictionary of Device children with arbitrary integer keys.
210
222
 
211
- In the below example, foos becomes a dictionary on the parent device
212
- at runtime, so parent.foos[2] returns a FooDevice. For example usage see
213
- :class:`~ophyd_async.epics.sim.DynamicSensorGroup`
223
+ :see-also: [](#implementing-devices) for examples of how to use this class.
214
224
  """
215
225
 
216
226
  def __init__(
@@ -274,7 +284,7 @@ class DeviceProcessor:
274
284
  self._locals_on_exit: dict[str, Any] = {}
275
285
 
276
286
  def _caller_locals(self) -> dict[str, Any]:
277
- """Walk up until we find a stack frame that doesn't have us as self"""
287
+ """Walk up until we find a stack frame that doesn't have us as self."""
278
288
  try:
279
289
  raise ValueError
280
290
  except ValueError:
@@ -341,33 +351,29 @@ def init_devices(
341
351
  connect=True,
342
352
  mock=False,
343
353
  timeout: float = 10.0,
344
- ) -> DeviceProcessor:
345
- """Auto initialise top level Device instances to be used as a context manager
346
-
347
- Parameters
348
- ----------
349
- set_name:
350
- If True, call ``device.set_name(variable_name)`` on all Devices
351
- created within the context manager that have an empty ``name``
352
- child_name_separator:
353
- Use this as a separator if we call ``set_name``.
354
- connect:
355
- If True, call ``device.connect(mock, timeout)`` in parallel on all
356
- Devices created within the context manager
357
- mock:
358
- If True, connect Signals in mock mode
359
- timeout:
360
- How long to wait for connect before logging an exception
361
-
362
- Notes
363
- -----
364
- Example usage::
365
-
366
- [async] with init_devices():
367
- t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
368
- t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
369
- # Names and connects devices here
370
- assert t1x.name == "t1x"
354
+ ):
355
+ """Auto initialize top level Device instances: to be used as a context manager.
356
+
357
+ :param set_name:
358
+ If True, call `device.set_name(variable_name)` on all Devices created
359
+ within the context manager that have an empty `name`.
360
+ :param child_name_separator: Separator for child names if `set_name` is True.
361
+ :param connect:
362
+ If True, call `device.connect(mock, timeout)` in parallel on all Devices
363
+ created within the context manager.
364
+ :param mock: If True, connect Signals in mock mode.
365
+ :param timeout: How long to wait for connect before logging an exception.
366
+ :raises RuntimeError: If used inside a plan, use [](#ensure_connected) instead.
367
+ :raises NotConnected: If devices could not be connected.
368
+
369
+ For example, to connect and name 2 motors in parallel:
370
+ ```python
371
+ [async] with init_devices():
372
+ t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
373
+ t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
374
+ # Names and connects devices here
375
+ assert t1x.name == "t1x"
376
+ ```
371
377
  """
372
378
 
373
379
  async def process_devices(devices: dict[str, Device]):