ophyd-async 0.9.0a1__py3-none-any.whl → 0.10.0a1__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 (157) 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 +102 -74
  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 +158 -153
  8. ophyd_async/core/_device.py +143 -115
  9. ophyd_async/core/_device_filler.py +82 -9
  10. ophyd_async/core/_flyer.py +16 -7
  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 +74 -58
  17. ophyd_async/core/_settings.py +113 -0
  18. ophyd_async/core/_signal.py +304 -174
  19. ophyd_async/core/_signal_backend.py +60 -14
  20. ophyd_async/core/_soft_signal_backend.py +18 -12
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +54 -17
  23. ophyd_async/core/_utils.py +101 -52
  24. ophyd_async/core/_yaml_settings.py +66 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/__init__.py +9 -0
  27. ophyd_async/epics/adandor/_andor.py +45 -0
  28. ophyd_async/epics/adandor/_andor_controller.py +51 -0
  29. ophyd_async/epics/adandor/_andor_io.py +34 -0
  30. ophyd_async/epics/adaravis/__init__.py +8 -1
  31. ophyd_async/epics/adaravis/_aravis.py +23 -41
  32. ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
  33. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  34. ophyd_async/epics/adcore/__init__.py +36 -14
  35. ophyd_async/epics/adcore/_core_detector.py +81 -0
  36. ophyd_async/epics/adcore/_core_io.py +145 -95
  37. ophyd_async/epics/adcore/_core_logic.py +179 -88
  38. ophyd_async/epics/adcore/_core_writer.py +223 -0
  39. ophyd_async/epics/adcore/_hdf_writer.py +51 -92
  40. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  41. ophyd_async/epics/adcore/_single_trigger.py +6 -5
  42. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  43. ophyd_async/epics/adcore/_utils.py +3 -2
  44. ophyd_async/epics/adkinetix/__init__.py +2 -1
  45. ophyd_async/epics/adkinetix/_kinetix.py +32 -27
  46. ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
  47. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  48. ophyd_async/epics/adpilatus/__init__.py +7 -2
  49. ophyd_async/epics/adpilatus/_pilatus.py +28 -40
  50. ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
  51. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  52. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  53. ophyd_async/epics/adsimdetector/_sim.py +22 -16
  54. ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
  55. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  56. ophyd_async/epics/advimba/__init__.py +10 -1
  57. ophyd_async/epics/advimba/_vimba.py +26 -25
  58. ophyd_async/epics/advimba/_vimba_controller.py +12 -24
  59. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  60. ophyd_async/epics/core/_aioca.py +66 -30
  61. ophyd_async/epics/core/_epics_connector.py +4 -0
  62. ophyd_async/epics/core/_epics_device.py +2 -0
  63. ophyd_async/epics/core/_p4p.py +50 -18
  64. ophyd_async/epics/core/_pvi_connector.py +65 -8
  65. ophyd_async/epics/core/_signal.py +51 -51
  66. ophyd_async/epics/core/_util.py +5 -5
  67. ophyd_async/epics/demo/__init__.py +11 -49
  68. ophyd_async/epics/demo/__main__.py +31 -0
  69. ophyd_async/epics/demo/_ioc.py +32 -0
  70. ophyd_async/epics/demo/_motor.py +82 -0
  71. ophyd_async/epics/demo/_point_detector.py +42 -0
  72. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  73. ophyd_async/epics/demo/_stage.py +15 -0
  74. ophyd_async/epics/demo/{mover.db → motor.db} +2 -1
  75. ophyd_async/epics/demo/point_detector.db +59 -0
  76. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  77. ophyd_async/epics/eiger/_eiger.py +1 -3
  78. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  79. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  80. ophyd_async/epics/eiger/_odin_io.py +1 -2
  81. ophyd_async/epics/motor.py +83 -38
  82. ophyd_async/epics/signal.py +4 -1
  83. ophyd_async/epics/testing/__init__.py +14 -14
  84. ophyd_async/epics/testing/_example_ioc.py +68 -73
  85. ophyd_async/epics/testing/_utils.py +19 -44
  86. ophyd_async/epics/testing/test_records.db +16 -0
  87. ophyd_async/epics/testing/test_records_pva.db +17 -16
  88. ophyd_async/fastcs/__init__.py +1 -0
  89. ophyd_async/fastcs/core.py +6 -0
  90. ophyd_async/fastcs/odin/__init__.py +1 -0
  91. ophyd_async/fastcs/panda/__init__.py +8 -8
  92. ophyd_async/fastcs/panda/_block.py +29 -9
  93. ophyd_async/fastcs/panda/_control.py +12 -2
  94. ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
  95. ophyd_async/fastcs/panda/_table.py +13 -7
  96. ophyd_async/fastcs/panda/_trigger.py +23 -9
  97. ophyd_async/fastcs/panda/_writer.py +27 -30
  98. ophyd_async/plan_stubs/__init__.py +16 -0
  99. ophyd_async/plan_stubs/_ensure_connected.py +12 -17
  100. ophyd_async/plan_stubs/_fly.py +3 -5
  101. ophyd_async/plan_stubs/_nd_attributes.py +9 -5
  102. ophyd_async/plan_stubs/_panda.py +14 -0
  103. ophyd_async/plan_stubs/_settings.py +152 -0
  104. ophyd_async/plan_stubs/_utils.py +3 -0
  105. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  106. ophyd_async/sim/__init__.py +29 -0
  107. ophyd_async/sim/__main__.py +43 -0
  108. ophyd_async/sim/_blob_detector.py +33 -0
  109. ophyd_async/sim/_blob_detector_controller.py +48 -0
  110. ophyd_async/sim/_blob_detector_writer.py +105 -0
  111. ophyd_async/sim/_mirror_horizontal.py +46 -0
  112. ophyd_async/sim/_mirror_vertical.py +74 -0
  113. ophyd_async/sim/_motor.py +233 -0
  114. ophyd_async/sim/_pattern_generator.py +124 -0
  115. ophyd_async/sim/_point_detector.py +86 -0
  116. ophyd_async/sim/_stage.py +19 -0
  117. ophyd_async/tango/__init__.py +1 -0
  118. ophyd_async/tango/core/__init__.py +6 -1
  119. ophyd_async/tango/core/_base_device.py +41 -33
  120. ophyd_async/tango/core/_converters.py +81 -0
  121. ophyd_async/tango/core/_signal.py +21 -33
  122. ophyd_async/tango/core/_tango_readable.py +2 -19
  123. ophyd_async/tango/core/_tango_transport.py +148 -74
  124. ophyd_async/tango/core/_utils.py +47 -0
  125. ophyd_async/tango/demo/_counter.py +2 -0
  126. ophyd_async/tango/demo/_detector.py +2 -0
  127. ophyd_async/tango/demo/_mover.py +10 -6
  128. ophyd_async/tango/demo/_tango/_servers.py +4 -0
  129. ophyd_async/tango/testing/__init__.py +6 -0
  130. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  131. ophyd_async/testing/__init__.py +48 -7
  132. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  133. ophyd_async/testing/_assert.py +200 -96
  134. ophyd_async/testing/_mock_signal_utils.py +59 -73
  135. ophyd_async/testing/_one_of_everything.py +146 -0
  136. ophyd_async/testing/_single_derived.py +87 -0
  137. ophyd_async/testing/_utils.py +3 -0
  138. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
  139. ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
  140. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
  141. ophyd_async/core/_device_save_loader.py +0 -274
  142. ophyd_async/epics/demo/_mover.py +0 -95
  143. ophyd_async/epics/demo/_sensor.py +0 -37
  144. ophyd_async/epics/demo/sensor.db +0 -19
  145. ophyd_async/fastcs/panda/_utils.py +0 -16
  146. ophyd_async/sim/demo/__init__.py +0 -19
  147. ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
  148. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
  149. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
  150. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
  151. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
  152. ophyd_async/sim/demo/_sim_motor.py +0 -107
  153. ophyd_async/sim/testing/__init__.py +0 -0
  154. ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
  155. ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
  156. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
  157. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,20 @@
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
9
+ from typing import (
10
+ Generic,
11
+ TypeVar,
12
+ )
8
13
 
9
14
  from bluesky.protocols import (
10
15
  Collectable,
11
16
  Flyable,
17
+ Hints,
12
18
  Preparable,
13
19
  Reading,
14
20
  Stageable,
@@ -23,48 +29,62 @@ from ._device import Device, DeviceConnector
23
29
  from ._protocol import AsyncConfigurable, AsyncReadable
24
30
  from ._signal import SignalR
25
31
  from ._status import AsyncStatus, WatchableAsyncStatus
26
- from ._utils import DEFAULT_TIMEOUT, StrictEnum, WatcherUpdate, merge_gathered_dicts
32
+ from ._utils import DEFAULT_TIMEOUT, WatcherUpdate, merge_gathered_dicts
33
+
34
+
35
+ class DetectorTrigger(Enum):
36
+ """Type of mechanism for triggering a detector to take frames."""
37
+
38
+ INTERNAL = "INTERNAL"
39
+ """Detector generates internal trigger for given rate"""
27
40
 
41
+ EDGE_TRIGGER = "EDGE_TRIGGER"
42
+ """Expect a series of arbitrary length trigger signals"""
28
43
 
29
- class DetectorTrigger(StrictEnum):
30
- """Type of mechanism for triggering a detector to take frames"""
44
+ CONSTANT_GATE = "CONSTANT_GATE"
45
+ """Expect a series of constant width external gate signals"""
31
46
 
32
- #: Detector generates internal trigger for given rate
33
- INTERNAL = "internal"
34
- #: Expect a series of arbitrary length trigger signals
35
- EDGE_TRIGGER = "edge_trigger"
36
- #: Expect a series of constant width external gate signals
37
- CONSTANT_GATE = "constant_gate"
38
- #: Expect a series of variable width external gate signals
39
- VARIABLE_GATE = "variable_gate"
47
+ VARIABLE_GATE = "VARIABLE_GATE"
48
+ """Expect a series of variable width external gate signals"""
40
49
 
41
50
 
42
51
  class TriggerInfo(BaseModel):
43
- """Minimal set of information required to setup triggering on a detector"""
44
-
45
- #: Number of triggers that will be sent, (0 means infinite) Can be:
46
- # - A single integer or
47
- # - A list of integers for multiple triggers
48
- # Example for tomography: TriggerInfo(number=[2,3,100,3])
49
- #: This would trigger:
50
- #: - 2 times for dark field images
51
- #: - 3 times for initial flat field images
52
- #: - 100 times for projections
53
- #: - 3 times for final flat field images
54
- number_of_triggers: NonNegativeInt | list[NonNegativeInt]
55
- #: 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
+
56
70
  trigger: DetectorTrigger = Field(default=DetectorTrigger.INTERNAL)
57
- #: What is the minimum deadtime between triggers
58
- deadtime: float | None = Field(default=None, ge=0)
59
- #: 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
+
60
76
  livetime: float | None = Field(default=None, ge=0)
61
- #: What is the maximum timeout on waiting for a frame
77
+ """What is the maximum high time of the triggers"""
78
+
62
79
  frame_timeout: float | None = Field(default=None, gt=0)
63
- #: How many triggers make up a single StreamDatum index, to allow multiple frames
64
- #: from a faster detector to be zipped with a single frame from a slow detector
65
- #: e.g. if num=10 and multiplier=5 then the detector will take 10 frames,
66
- #: 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
+
67
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
+ """
68
88
 
69
89
  @computed_field
70
90
  @cached_property
@@ -77,85 +97,81 @@ class TriggerInfo(BaseModel):
77
97
 
78
98
 
79
99
  class DetectorController(ABC):
80
- """
81
- Classes implementing this interface should hold the logic for
82
- arming and disarming a detector
83
- """
100
+ """Detector logic for arming and disarming the detector."""
84
101
 
85
102
  @abstractmethod
86
103
  def get_deadtime(self, exposure: float | None) -> float:
87
- """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."""
88
105
 
89
106
  @abstractmethod
90
- async def prepare(self, trigger_info: TriggerInfo):
91
- """
92
- Do all necessary steps to prepare the detector for triggers.
93
-
94
- Args:
95
- trigger_info: This is a Pydantic model which contains
96
- number Expected number of frames.
97
- trigger Type of trigger for which to prepare the detector. Defaults
98
- to DetectorTrigger.internal.
99
- livetime Livetime / Exposure time with which to set up the detector.
100
- Defaults to None
101
- if not applicable or the detector is expected to use its previously-set
102
- exposure time.
103
- deadtime Defaults to None. This is the minimum deadtime between
104
- triggers.
105
- multiplier The number of triggers grouped into a single StreamDatum
106
- index.
107
+ async def prepare(self, trigger_info: TriggerInfo) -> None:
108
+ """Do all necessary steps to prepare the detector for triggers.
109
+
110
+ :param trigger_info: The sort of triggers to expect.
107
111
  """
108
112
 
109
113
  @abstractmethod
110
114
  async def arm(self) -> None:
111
- """
112
- Arm the detector
113
- """
115
+ """Arm the detector."""
114
116
 
115
117
  @abstractmethod
116
118
  async def wait_for_idle(self):
117
- """
118
- This will wait on the internal _arm_status and wait for it to get disarmed/idle
119
- """
119
+ """Wait on the internal _arm_status and wait for it to get disarmed/idle."""
120
120
 
121
121
  @abstractmethod
122
122
  async def disarm(self):
123
- """Disarm the detector, return detector to an idle state"""
123
+ """Disarm the detector, return detector to an idle state."""
124
124
 
125
125
 
126
126
  class DetectorWriter(ABC):
127
- """Logic for making a detector write data to somewhere persistent
128
- (e.g. an HDF5 file)"""
127
+ """Logic for making detector write data to somewhere persistent (e.g. HDF5 file)."""
129
128
 
130
129
  @abstractmethod
131
130
  async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
132
131
  """Open writer and wait for it to be ready for data.
133
132
 
134
- Args:
135
- multiplier: Each StreamDatum index corresponds to this many
136
- written exposures
137
-
138
- Returns:
139
- Output for ``describe()``
133
+ :param multiplier:
134
+ Each StreamDatum index corresponds to this many written exposures
135
+ :return: Output for ``describe()``
140
136
  """
141
137
 
142
- @abstractmethod
143
- def observe_indices_written(
144
- self, timeout=DEFAULT_TIMEOUT
145
- ) -> AsyncGenerator[int, None]:
146
- """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 {}
147
142
 
148
143
  @abstractmethod
149
144
  async def get_indices_written(self) -> int:
150
- """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."""
151
154
 
152
155
  @abstractmethod
153
156
  def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
154
- """Create Stream docs up to given number written"""
157
+ """Create Stream docs up to given number written."""
155
158
 
156
159
  @abstractmethod
157
160
  async def close(self) -> None:
158
- """Close writer, blocks until I/O is complete"""
161
+ """Close writer, blocks until I/O is complete."""
162
+
163
+
164
+ # Add type var for controller so we can define
165
+ # StandardDetector[KinetixController, ADWriter] for example
166
+ DetectorControllerT = TypeVar("DetectorControllerT", bound=DetectorController)
167
+ DetectorWriterT = TypeVar("DetectorWriterT", bound=DetectorWriter)
168
+
169
+
170
+ def _ensure_trigger_info_exists(trigger_info: TriggerInfo | None) -> TriggerInfo:
171
+ # make absolute sure we realy have a valid TriggerInfo ... mostly for pylance
172
+ if trigger_info is None:
173
+ raise RuntimeError("Trigger info must be set before calling this method.")
174
+ return trigger_info
159
175
 
160
176
 
161
177
  class StandardDetector(
@@ -168,30 +184,26 @@ class StandardDetector(
168
184
  Flyable,
169
185
  Collectable,
170
186
  WritesStreamAssets,
187
+ Generic[DetectorControllerT, DetectorWriterT],
171
188
  ):
172
- """
173
- Useful detector base class for step and fly scanning detectors.
189
+ """Detector base class for step and fly scanning detectors.
190
+
174
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
175
197
  """
176
198
 
177
199
  def __init__(
178
200
  self,
179
- controller: DetectorController,
180
- writer: DetectorWriter,
201
+ controller: DetectorControllerT,
202
+ writer: DetectorWriterT,
181
203
  config_sigs: Sequence[SignalR] = (),
182
204
  name: str = "",
183
205
  connector: DeviceConnector | None = None,
184
206
  ) -> None:
185
- """
186
- Constructor
187
-
188
- Args:
189
- controller: Logic for arming and disarming the detector
190
- writer: Logic for making the detector write persistent data
191
- config_sigs: Signals to read when describe and read
192
- configuration are called. Defaults to ().
193
- name: Device name. Defaults to "".
194
- """
195
207
  self._controller = controller
196
208
  self._writer = writer
197
209
  self._describe: dict[str, DataKey] = {}
@@ -211,23 +223,15 @@ class StandardDetector(
211
223
  self._initial_frame: int = 0
212
224
  super().__init__(name, connector=connector)
213
225
 
214
- @property
215
- def controller(self) -> DetectorController:
216
- return self._controller
217
-
218
- @property
219
- def writer(self) -> DetectorWriter:
220
- return self._writer
221
-
222
226
  @AsyncStatus.wrap
223
227
  async def stage(self) -> None:
224
- # Disarm the detector, stop file writing.
228
+ """Make sure the detector is idle and ready to be used."""
225
229
  await self._check_config_sigs()
226
- await asyncio.gather(self.writer.close(), self.controller.disarm())
230
+ await asyncio.gather(self._writer.close(), self._controller.disarm())
227
231
  self._trigger_info = None
228
232
 
229
233
  async def _check_config_sigs(self):
230
- """Checks configuration signals are named and connected."""
234
+ """Check configuration signals are named and connected."""
231
235
  for signal in self._config_sigs:
232
236
  if signal.name == "":
233
237
  raise Exception(
@@ -243,8 +247,8 @@ class StandardDetector(
243
247
 
244
248
  @AsyncStatus.wrap
245
249
  async def unstage(self) -> None:
246
- # Stop data writing.
247
- await asyncio.gather(self.writer.close(), self.controller.disarm())
250
+ """Disarm the detector and stop file writing."""
251
+ await asyncio.gather(self._writer.close(), self._controller.disarm())
248
252
 
249
253
  async def read_configuration(self) -> dict[str, Reading]:
250
254
  return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
@@ -253,6 +257,7 @@ class StandardDetector(
253
257
  return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
254
258
 
255
259
  async def read(self) -> dict[str, Reading]:
260
+ """There is no data to be placed in events, so this is empty."""
256
261
  # All data is in StreamResources, not Events, so nothing to output here
257
262
  return {}
258
263
 
@@ -266,71 +271,67 @@ class StandardDetector(
266
271
  TriggerInfo(
267
272
  number_of_triggers=1,
268
273
  trigger=DetectorTrigger.INTERNAL,
269
- deadtime=None,
270
- livetime=None,
271
- frame_timeout=None,
272
274
  )
273
275
  )
274
- assert self._trigger_info
275
- assert self._trigger_info.trigger is DetectorTrigger.INTERNAL
276
+
277
+ trigger_info = _ensure_trigger_info_exists(self._trigger_info)
278
+ if trigger_info.trigger is not DetectorTrigger.INTERNAL:
279
+ msg = "The trigger method can only be called with INTERNAL triggering"
280
+ raise ValueError(msg)
281
+
276
282
  # Arm the detector and wait for it to finish.
277
- indices_written = await self.writer.get_indices_written()
278
- await self.controller.arm()
279
- await self.controller.wait_for_idle()
283
+ indices_written = await self._writer.get_indices_written()
284
+ await self._controller.arm()
285
+ await self._controller.wait_for_idle()
280
286
  end_observation = indices_written + 1
281
287
 
282
- async for index in self.writer.observe_indices_written(
283
- DEFAULT_TIMEOUT
284
- + (self._trigger_info.livetime or 0)
285
- + (self._trigger_info.deadtime or 0)
288
+ async for index in self._writer.observe_indices_written(
289
+ DEFAULT_TIMEOUT + (trigger_info.livetime or 0) + trigger_info.deadtime
286
290
  ):
287
291
  if index >= end_observation:
288
292
  break
289
293
 
290
294
  @AsyncStatus.wrap
291
295
  async def prepare(self, value: TriggerInfo) -> None:
292
- """
293
- Arm detector.
296
+ """Arm detector.
294
297
 
295
298
  Prepare the detector with trigger information. This is determined at and passed
296
299
  in from the plan level.
297
300
 
298
- This currently only prepares detectors for flyscans and stepscans just use the
299
- trigger information determined in trigger.
300
-
301
- To do: Unify prepare to be use for both fly and step scans.
302
-
303
- Args:
304
- value: TriggerInfo describing how to trigger the detector
301
+ :param value: TriggerInfo describing how to trigger the detector
305
302
  """
306
- if value.trigger != DetectorTrigger.INTERNAL:
307
- assert (
308
- value.deadtime
309
- ), "Deadtime must be supplied when in externally triggered mode"
310
- if value.deadtime:
311
- required = self.controller.get_deadtime(value.livetime)
312
- assert required <= value.deadtime, (
313
- f"Detector {self.controller} needs at least {required}s deadtime, "
314
- f"but trigger logic provides only {value.deadtime}s"
303
+ if value.trigger != DetectorTrigger.INTERNAL and not value.deadtime:
304
+ msg = "Deadtime must be supplied when in externally triggered mode"
305
+ raise ValueError(msg)
306
+ required_deadtime = self._controller.get_deadtime(value.livetime)
307
+ if value.deadtime and required_deadtime > value.deadtime:
308
+ msg = (
309
+ f"Detector {self._controller} needs at least {required_deadtime}s "
310
+ f"deadtime, but trigger logic provides only {value.deadtime}s"
315
311
  )
316
- self._trigger_info = value
312
+ raise ValueError(msg)
313
+ elif not value.deadtime:
314
+ value.deadtime = self._controller.get_deadtime(value.livetime)
317
315
  self._number_of_triggers_iter = iter(
318
- self._trigger_info.number_of_triggers
319
- if isinstance(self._trigger_info.number_of_triggers, list)
320
- 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]
321
319
  )
322
- self._initial_frame = await self.writer.get_indices_written()
323
320
  self._describe, _ = await asyncio.gather(
324
- self.writer.open(value.multiplier), self.controller.prepare(value)
321
+ self._writer.open(value.multiplier), self._controller.prepare(value)
325
322
  )
323
+ self._initial_frame = await self._writer.get_indices_written()
326
324
  if value.trigger != DetectorTrigger.INTERNAL:
327
- await self.controller.arm()
328
- self._fly_start = time.monotonic()
325
+ await self._controller.arm()
326
+ self._trigger_info = value
329
327
 
330
328
  @AsyncStatus.wrap
331
329
  async def kickoff(self):
332
330
  if self._trigger_info is None or self._number_of_triggers_iter is None:
333
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()
334
335
  try:
335
336
  self._frames_to_complete = next(self._number_of_triggers_iter)
336
337
  self._completable_frames += self._frames_to_complete
@@ -342,13 +343,13 @@ class StandardDetector(
342
343
 
343
344
  @WatchableAsyncStatus.wrap
344
345
  async def complete(self):
345
- assert self._trigger_info
346
- indices_written = self.writer.observe_indices_written(
347
- self._trigger_info.frame_timeout
346
+ trigger_info = _ensure_trigger_info_exists(self._trigger_info)
347
+ indices_written = self._writer.observe_indices_written(
348
+ trigger_info.frame_timeout
348
349
  or (
349
350
  DEFAULT_TIMEOUT
350
- + (self._trigger_info.livetime or 0)
351
- + (self._trigger_info.deadtime or 0)
351
+ + (trigger_info.livetime or 0)
352
+ + (trigger_info.deadtime or 0)
352
353
  )
353
354
  )
354
355
  try:
@@ -368,11 +369,11 @@ class StandardDetector(
368
369
  break
369
370
  finally:
370
371
  await indices_written.aclose()
371
- if self._completable_frames >= self._trigger_info.total_number_of_triggers:
372
+ if self._completable_frames >= trigger_info.total_number_of_triggers:
372
373
  self._completable_frames = 0
373
374
  self._frames_to_complete = 0
374
375
  self._number_of_triggers_iter = None
375
- await self.controller.wait_for_idle()
376
+ await self._controller.wait_for_idle()
376
377
 
377
378
  async def describe_collect(self) -> dict[str, DataKey]:
378
379
  return self._describe
@@ -384,9 +385,13 @@ class StandardDetector(
384
385
  # The index is optional, and provided for fly scans, however this needs to be
385
386
  # retrieved for step scans.
386
387
  if index is None:
387
- index = await self.writer.get_indices_written()
388
- async for doc in self.writer.collect_stream_docs(index):
388
+ index = await self._writer.get_indices_written()
389
+ async for doc in self._writer.collect_stream_docs(index):
389
390
  yield doc
390
391
 
391
392
  async def get_index(self) -> int:
392
- return await self.writer.get_indices_written()
393
+ return await self._writer.get_indices_written()
394
+
395
+ @property
396
+ def hints(self) -> Hints:
397
+ return self._writer.hints