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.
- ophyd_async/__init__.py +5 -8
- ophyd_async/_docs_parser.py +12 -0
- ophyd_async/_version.py +9 -4
- ophyd_async/core/__init__.py +102 -74
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +158 -153
- ophyd_async/core/_device.py +143 -115
- ophyd_async/core/_device_filler.py +82 -9
- ophyd_async/core/_flyer.py +16 -7
- ophyd_async/core/_hdf_dataset.py +29 -22
- ophyd_async/core/_log.py +14 -23
- ophyd_async/core/_mock_signal_backend.py +11 -3
- ophyd_async/core/_protocol.py +65 -45
- ophyd_async/core/_providers.py +28 -9
- ophyd_async/core/_readable.py +74 -58
- ophyd_async/core/_settings.py +113 -0
- ophyd_async/core/_signal.py +304 -174
- ophyd_async/core/_signal_backend.py +60 -14
- ophyd_async/core/_soft_signal_backend.py +18 -12
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +54 -17
- ophyd_async/core/_utils.py +101 -52
- ophyd_async/core/_yaml_settings.py +66 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +51 -0
- ophyd_async/epics/adandor/_andor_io.py +34 -0
- ophyd_async/epics/adaravis/__init__.py +8 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -41
- ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +36 -14
- ophyd_async/epics/adcore/_core_detector.py +81 -0
- ophyd_async/epics/adcore/_core_io.py +145 -95
- ophyd_async/epics/adcore/_core_logic.py +179 -88
- ophyd_async/epics/adcore/_core_writer.py +223 -0
- ophyd_async/epics/adcore/_hdf_writer.py +51 -92
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +6 -5
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +3 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +32 -27
- ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +7 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +22 -16
- ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +26 -25
- ophyd_async/epics/advimba/_vimba_controller.py +12 -24
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +66 -30
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +50 -18
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +5 -5
- ophyd_async/epics/demo/__init__.py +11 -49
- ophyd_async/epics/demo/__main__.py +31 -0
- ophyd_async/epics/demo/_ioc.py +32 -0
- ophyd_async/epics/demo/_motor.py +82 -0
- ophyd_async/epics/demo/_point_detector.py +42 -0
- ophyd_async/epics/demo/_point_detector_channel.py +22 -0
- ophyd_async/epics/demo/_stage.py +15 -0
- ophyd_async/epics/demo/{mover.db → motor.db} +2 -1
- ophyd_async/epics/demo/point_detector.db +59 -0
- ophyd_async/epics/demo/point_detector_channel.db +21 -0
- ophyd_async/epics/eiger/_eiger.py +1 -3
- ophyd_async/epics/eiger/_eiger_controller.py +11 -4
- ophyd_async/epics/eiger/_eiger_io.py +2 -0
- ophyd_async/epics/eiger/_odin_io.py +1 -2
- ophyd_async/epics/motor.py +83 -38
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/__init__.py +14 -14
- ophyd_async/epics/testing/_example_ioc.py +68 -73
- ophyd_async/epics/testing/_utils.py +19 -44
- ophyd_async/epics/testing/test_records.db +16 -0
- ophyd_async/epics/testing/test_records_pva.db +17 -16
- ophyd_async/fastcs/__init__.py +1 -0
- ophyd_async/fastcs/core.py +6 -0
- ophyd_async/fastcs/odin/__init__.py +1 -0
- ophyd_async/fastcs/panda/__init__.py +8 -8
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +12 -2
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
- ophyd_async/fastcs/panda/_table.py +13 -7
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +16 -0
- ophyd_async/plan_stubs/_ensure_connected.py +12 -17
- ophyd_async/plan_stubs/_fly.py +3 -5
- ophyd_async/plan_stubs/_nd_attributes.py +9 -5
- ophyd_async/plan_stubs/_panda.py +14 -0
- ophyd_async/plan_stubs/_settings.py +152 -0
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +29 -0
- ophyd_async/sim/__main__.py +43 -0
- ophyd_async/sim/_blob_detector.py +33 -0
- ophyd_async/sim/_blob_detector_controller.py +48 -0
- ophyd_async/sim/_blob_detector_writer.py +105 -0
- ophyd_async/sim/_mirror_horizontal.py +46 -0
- ophyd_async/sim/_mirror_vertical.py +74 -0
- ophyd_async/sim/_motor.py +233 -0
- ophyd_async/sim/_pattern_generator.py +124 -0
- ophyd_async/sim/_point_detector.py +86 -0
- ophyd_async/sim/_stage.py +19 -0
- ophyd_async/tango/__init__.py +1 -0
- ophyd_async/tango/core/__init__.py +6 -1
- ophyd_async/tango/core/_base_device.py +41 -33
- ophyd_async/tango/core/_converters.py +81 -0
- ophyd_async/tango/core/_signal.py +21 -33
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +148 -74
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/demo/_counter.py +2 -0
- ophyd_async/tango/demo/_detector.py +2 -0
- ophyd_async/tango/demo/_mover.py +10 -6
- ophyd_async/tango/demo/_tango/_servers.py +4 -0
- ophyd_async/tango/testing/__init__.py +6 -0
- ophyd_async/tango/testing/_one_of_everything.py +200 -0
- ophyd_async/testing/__init__.py +48 -7
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +200 -96
- ophyd_async/testing/_mock_signal_utils.py +59 -73
- ophyd_async/testing/_one_of_everything.py +146 -0
- ophyd_async/testing/_single_derived.py +87 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/demo/_mover.py +0 -95
- ophyd_async/epics/demo/_sensor.py +0 -37
- ophyd_async/epics/demo/sensor.db +0 -19
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
- ophyd_async/sim/demo/_sim_motor.py +0 -107
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
- ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
ophyd_async/core/_detector.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
30
|
-
"""
|
|
44
|
+
CONSTANT_GATE = "CONSTANT_GATE"
|
|
45
|
+
"""Expect a series of constant width external gate signals"""
|
|
31
46
|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
77
|
+
"""What is the maximum high time of the triggers"""
|
|
78
|
+
|
|
62
79
|
frame_timeout: float | None = Field(default=None, gt=0)
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
@
|
|
143
|
-
def
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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:
|
|
180
|
-
writer:
|
|
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
|
-
|
|
228
|
+
"""Make sure the detector is idle and ready to be used."""
|
|
225
229
|
await self._check_config_sigs()
|
|
226
|
-
await asyncio.gather(self.
|
|
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
|
-
"""
|
|
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
|
-
|
|
247
|
-
await asyncio.gather(self.
|
|
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
|
-
|
|
275
|
-
|
|
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.
|
|
278
|
-
await self.
|
|
279
|
-
await self.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if value.deadtime:
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
f"
|
|
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
|
-
|
|
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
|
-
|
|
319
|
-
if isinstance(
|
|
320
|
-
else [
|
|
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.
|
|
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.
|
|
328
|
-
|
|
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
|
-
|
|
346
|
-
indices_written = self.
|
|
347
|
-
|
|
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
|
-
+ (
|
|
351
|
-
+ (
|
|
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 >=
|
|
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.
|
|
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.
|
|
388
|
-
async for doc in self.
|
|
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.
|
|
393
|
+
return await self._writer.get_indices_written()
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def hints(self) -> Hints:
|
|
397
|
+
return self._writer.hints
|