ophyd-async 0.6.0__py3-none-any.whl → 0.7.0__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/_version.py +2 -2
- ophyd_async/core/__init__.py +4 -4
- ophyd_async/core/_detector.py +74 -37
- ophyd_async/core/_device.py +6 -1
- ophyd_async/core/_flyer.py +5 -20
- ophyd_async/core/_table.py +101 -18
- ophyd_async/epics/adaravis/_aravis_controller.py +4 -4
- ophyd_async/epics/adcore/_core_logic.py +2 -2
- ophyd_async/epics/adkinetix/_kinetix_controller.py +3 -3
- ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -3
- ophyd_async/epics/adsimdetector/_sim.py +1 -1
- ophyd_async/epics/adsimdetector/_sim_controller.py +3 -3
- ophyd_async/epics/advimba/_vimba_controller.py +3 -3
- ophyd_async/epics/eiger/_eiger_controller.py +3 -3
- ophyd_async/fastcs/panda/_block.py +7 -0
- ophyd_async/fastcs/panda/_control.py +2 -2
- ophyd_async/fastcs/panda/_table.py +3 -37
- ophyd_async/fastcs/panda/_trigger.py +3 -3
- ophyd_async/fastcs/panda/_writer.py +2 -2
- ophyd_async/plan_stubs/_fly.py +1 -3
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +5 -3
- ophyd_async/tango/__init__.py +45 -0
- ophyd_async/tango/base_devices/__init__.py +4 -0
- ophyd_async/tango/base_devices/_base_device.py +225 -0
- ophyd_async/tango/base_devices/_tango_readable.py +33 -0
- ophyd_async/tango/demo/__init__.py +12 -0
- ophyd_async/tango/demo/_counter.py +37 -0
- ophyd_async/tango/demo/_detector.py +42 -0
- ophyd_async/tango/demo/_mover.py +77 -0
- ophyd_async/tango/demo/_tango/__init__.py +3 -0
- ophyd_async/tango/demo/_tango/_servers.py +108 -0
- ophyd_async/tango/signal/__init__.py +39 -0
- ophyd_async/tango/signal/_signal.py +223 -0
- ophyd_async/tango/signal/_tango_transport.py +764 -0
- {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/METADATA +5 -1
- {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/RECORD +40 -28
- {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/WHEEL +1 -1
- {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.6.0.dist-info → ophyd_async-0.7.0.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py
CHANGED
ophyd_async/core/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from ._detector import (
|
|
2
|
-
|
|
2
|
+
DetectorController,
|
|
3
3
|
DetectorTrigger,
|
|
4
4
|
DetectorWriter,
|
|
5
5
|
StandardDetector,
|
|
@@ -16,7 +16,7 @@ from ._device_save_loader import (
|
|
|
16
16
|
set_signal_values,
|
|
17
17
|
walk_rw_signals,
|
|
18
18
|
)
|
|
19
|
-
from ._flyer import
|
|
19
|
+
from ._flyer import FlyerController, StandardFlyer
|
|
20
20
|
from ._hdf_dataset import HDFDataset, HDFFile
|
|
21
21
|
from ._log import config_ophyd_async_logging
|
|
22
22
|
from ._mock_signal_backend import MockSignalBackend
|
|
@@ -85,7 +85,7 @@ from ._utils import (
|
|
|
85
85
|
)
|
|
86
86
|
|
|
87
87
|
__all__ = [
|
|
88
|
-
"
|
|
88
|
+
"DetectorController",
|
|
89
89
|
"DetectorTrigger",
|
|
90
90
|
"DetectorWriter",
|
|
91
91
|
"StandardDetector",
|
|
@@ -102,7 +102,7 @@ __all__ = [
|
|
|
102
102
|
"set_signal_values",
|
|
103
103
|
"walk_rw_signals",
|
|
104
104
|
"StandardFlyer",
|
|
105
|
-
"
|
|
105
|
+
"FlyerController",
|
|
106
106
|
"HDFDataset",
|
|
107
107
|
"HDFFile",
|
|
108
108
|
"config_ophyd_async_logging",
|
ophyd_async/core/_detector.py
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import time
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
-
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Sequence
|
|
6
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Iterator, Sequence
|
|
7
7
|
from enum import Enum
|
|
8
|
+
from functools import cached_property
|
|
8
9
|
from typing import (
|
|
9
10
|
Generic,
|
|
10
11
|
)
|
|
@@ -20,7 +21,7 @@ from bluesky.protocols import (
|
|
|
20
21
|
WritesStreamAssets,
|
|
21
22
|
)
|
|
22
23
|
from event_model import DataKey
|
|
23
|
-
from pydantic import BaseModel, Field
|
|
24
|
+
from pydantic import BaseModel, Field, NonNegativeInt, computed_field
|
|
24
25
|
|
|
25
26
|
from ._device import Device
|
|
26
27
|
from ._protocol import AsyncConfigurable, AsyncReadable
|
|
@@ -45,8 +46,16 @@ class DetectorTrigger(str, Enum):
|
|
|
45
46
|
class TriggerInfo(BaseModel):
|
|
46
47
|
"""Minimal set of information required to setup triggering on a detector"""
|
|
47
48
|
|
|
48
|
-
#: Number of triggers that will be sent, 0 means infinite
|
|
49
|
-
|
|
49
|
+
#: Number of triggers that will be sent, (0 means infinite) Can be:
|
|
50
|
+
# - A single integer or
|
|
51
|
+
# - A list of integers for multiple triggers
|
|
52
|
+
# Example for tomography: TriggerInfo(number=[2,3,100,3])
|
|
53
|
+
#: This would trigger:
|
|
54
|
+
#: - 2 times for dark field images
|
|
55
|
+
#: - 3 times for initial flat field images
|
|
56
|
+
#: - 100 times for projections
|
|
57
|
+
#: - 3 times for final flat field images
|
|
58
|
+
number_of_triggers: NonNegativeInt | list[NonNegativeInt]
|
|
50
59
|
#: Sort of triggers that will be sent
|
|
51
60
|
trigger: DetectorTrigger = Field(default=DetectorTrigger.internal)
|
|
52
61
|
#: What is the minimum deadtime between triggers
|
|
@@ -60,13 +69,18 @@ class TriggerInfo(BaseModel):
|
|
|
60
69
|
#: e.g. if num=10 and multiplier=5 then the detector will take 10 frames,
|
|
61
70
|
#: but publish 2 indices, and describe() will show a shape of (5, h, w)
|
|
62
71
|
multiplier: int = 1
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
|
|
73
|
+
@computed_field
|
|
74
|
+
@cached_property
|
|
75
|
+
def total_number_of_triggers(self) -> int:
|
|
76
|
+
return (
|
|
77
|
+
sum(self.number_of_triggers)
|
|
78
|
+
if isinstance(self.number_of_triggers, list)
|
|
79
|
+
else self.number_of_triggers
|
|
80
|
+
)
|
|
67
81
|
|
|
68
82
|
|
|
69
|
-
class
|
|
83
|
+
class DetectorController(ABC):
|
|
70
84
|
"""
|
|
71
85
|
Classes implementing this interface should hold the logic for
|
|
72
86
|
arming and disarming a detector
|
|
@@ -167,7 +181,7 @@ class StandardDetector(
|
|
|
167
181
|
|
|
168
182
|
def __init__(
|
|
169
183
|
self,
|
|
170
|
-
controller:
|
|
184
|
+
controller: DetectorController,
|
|
171
185
|
writer: DetectorWriter,
|
|
172
186
|
config_sigs: Sequence[SignalR] = (),
|
|
173
187
|
name: str = "",
|
|
@@ -192,14 +206,18 @@ class StandardDetector(
|
|
|
192
206
|
# For kickoff
|
|
193
207
|
self._watchers: list[Callable] = []
|
|
194
208
|
self._fly_status: WatchableAsyncStatus | None = None
|
|
195
|
-
self._fly_start: float
|
|
196
|
-
self.
|
|
197
|
-
|
|
198
|
-
|
|
209
|
+
self._fly_start: float | None = None
|
|
210
|
+
self._frames_to_complete: int = 0
|
|
211
|
+
# Represents the total number of frames that will have been completed at the
|
|
212
|
+
# end of the next `complete`.
|
|
213
|
+
self._completable_frames: int = 0
|
|
214
|
+
self._number_of_triggers_iter: Iterator[int] | None = None
|
|
215
|
+
self._initial_frame: int = 0
|
|
216
|
+
|
|
199
217
|
super().__init__(name)
|
|
200
218
|
|
|
201
219
|
@property
|
|
202
|
-
def controller(self) ->
|
|
220
|
+
def controller(self) -> DetectorController:
|
|
203
221
|
return self._controller
|
|
204
222
|
|
|
205
223
|
@property
|
|
@@ -208,7 +226,7 @@ class StandardDetector(
|
|
|
208
226
|
|
|
209
227
|
@AsyncStatus.wrap
|
|
210
228
|
async def stage(self) -> None:
|
|
211
|
-
# Disarm the detector, stop
|
|
229
|
+
# Disarm the detector, stop file writing.
|
|
212
230
|
await self._check_config_sigs()
|
|
213
231
|
await asyncio.gather(self.writer.close(), self.controller.disarm())
|
|
214
232
|
self._trigger_info = None
|
|
@@ -251,7 +269,7 @@ class StandardDetector(
|
|
|
251
269
|
if self._trigger_info is None:
|
|
252
270
|
await self.prepare(
|
|
253
271
|
TriggerInfo(
|
|
254
|
-
|
|
272
|
+
number_of_triggers=1,
|
|
255
273
|
trigger=DetectorTrigger.internal,
|
|
256
274
|
deadtime=None,
|
|
257
275
|
livetime=None,
|
|
@@ -301,8 +319,12 @@ class StandardDetector(
|
|
|
301
319
|
f"but trigger logic provides only {value.deadtime}s"
|
|
302
320
|
)
|
|
303
321
|
self._trigger_info = value
|
|
322
|
+
self._number_of_triggers_iter = iter(
|
|
323
|
+
self._trigger_info.number_of_triggers
|
|
324
|
+
if isinstance(self._trigger_info.number_of_triggers, list)
|
|
325
|
+
else [self._trigger_info.number_of_triggers]
|
|
326
|
+
)
|
|
304
327
|
self._initial_frame = await self.writer.get_indices_written()
|
|
305
|
-
self._last_frame = self._initial_frame + self._trigger_info.number
|
|
306
328
|
self._describe, _ = await asyncio.gather(
|
|
307
329
|
self.writer.open(value.multiplier), self.controller.prepare(value)
|
|
308
330
|
)
|
|
@@ -312,35 +334,50 @@ class StandardDetector(
|
|
|
312
334
|
|
|
313
335
|
@AsyncStatus.wrap
|
|
314
336
|
async def kickoff(self):
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
337
|
+
if self._trigger_info is None or self._number_of_triggers_iter is None:
|
|
338
|
+
raise RuntimeError("Prepare must be called before kickoff!")
|
|
339
|
+
try:
|
|
340
|
+
self._frames_to_complete = next(self._number_of_triggers_iter)
|
|
341
|
+
self._completable_frames += self._frames_to_complete
|
|
342
|
+
except StopIteration as err:
|
|
343
|
+
raise RuntimeError(
|
|
344
|
+
f"Kickoff called more than the configured number of "
|
|
345
|
+
f"{self._trigger_info.total_number_of_triggers} iteration(s)!"
|
|
346
|
+
) from err
|
|
319
347
|
|
|
320
348
|
@WatchableAsyncStatus.wrap
|
|
321
349
|
async def complete(self):
|
|
322
350
|
assert self._trigger_info
|
|
323
|
-
|
|
351
|
+
indices_written = self.writer.observe_indices_written(
|
|
324
352
|
self._trigger_info.frame_timeout
|
|
325
353
|
or (
|
|
326
354
|
DEFAULT_TIMEOUT
|
|
327
355
|
+ (self._trigger_info.livetime or 0)
|
|
328
356
|
+ (self._trigger_info.deadtime or 0)
|
|
329
357
|
)
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
358
|
+
)
|
|
359
|
+
try:
|
|
360
|
+
async for index in indices_written:
|
|
361
|
+
yield WatcherUpdate(
|
|
362
|
+
name=self.name,
|
|
363
|
+
current=index,
|
|
364
|
+
initial=self._initial_frame,
|
|
365
|
+
target=self._frames_to_complete,
|
|
366
|
+
unit="",
|
|
367
|
+
precision=0,
|
|
368
|
+
time_elapsed=time.monotonic() - self._fly_start
|
|
369
|
+
if self._fly_start
|
|
370
|
+
else None,
|
|
371
|
+
)
|
|
372
|
+
if index >= self._frames_to_complete:
|
|
373
|
+
break
|
|
374
|
+
finally:
|
|
375
|
+
await indices_written.aclose()
|
|
376
|
+
if self._completable_frames >= self._trigger_info.total_number_of_triggers:
|
|
377
|
+
self._completable_frames = 0
|
|
378
|
+
self._frames_to_complete = 0
|
|
379
|
+
self._number_of_triggers_iter = None
|
|
380
|
+
await self.controller.wait_for_idle()
|
|
344
381
|
|
|
345
382
|
async def describe_collect(self) -> dict[str, DataKey]:
|
|
346
383
|
return self._describe
|
ophyd_async/core/_device.py
CHANGED
|
@@ -12,7 +12,7 @@ from typing import (
|
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
from bluesky.protocols import HasName
|
|
15
|
-
from bluesky.run_engine import call_in_bluesky_event_loop
|
|
15
|
+
from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
|
|
16
16
|
|
|
17
17
|
from ._utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection
|
|
18
18
|
|
|
@@ -224,6 +224,11 @@ class DeviceCollector:
|
|
|
224
224
|
await self._on_exit()
|
|
225
225
|
|
|
226
226
|
def __exit__(self, type_, value, traceback):
|
|
227
|
+
if in_bluesky_event_loop():
|
|
228
|
+
raise RuntimeError(
|
|
229
|
+
"Cannot use DeviceConnector inside a plan, instead use "
|
|
230
|
+
"`yield from ophyd_async.plan_stubs.ensure_connected(device)`"
|
|
231
|
+
)
|
|
227
232
|
self._objects_on_exit = self._caller_locals()
|
|
228
233
|
try:
|
|
229
234
|
fut = call_in_bluesky_event_loop(self._on_exit())
|
ophyd_async/core/_flyer.py
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from collections.abc import Sequence
|
|
3
2
|
from typing import Generic
|
|
4
3
|
|
|
5
|
-
from bluesky.protocols import Flyable, Preparable,
|
|
6
|
-
from event_model import DataKey
|
|
4
|
+
from bluesky.protocols import Flyable, Preparable, Stageable
|
|
7
5
|
|
|
8
6
|
from ._device import Device
|
|
9
|
-
from ._signal import SignalR
|
|
10
7
|
from ._status import AsyncStatus
|
|
11
|
-
from ._utils import T
|
|
8
|
+
from ._utils import T
|
|
12
9
|
|
|
13
10
|
|
|
14
|
-
class
|
|
11
|
+
class FlyerController(ABC, Generic[T]):
|
|
15
12
|
@abstractmethod
|
|
16
13
|
async def prepare(self, value: T):
|
|
17
14
|
"""Move to the start of the flyscan"""
|
|
@@ -38,16 +35,14 @@ class StandardFlyer(
|
|
|
38
35
|
):
|
|
39
36
|
def __init__(
|
|
40
37
|
self,
|
|
41
|
-
trigger_logic:
|
|
42
|
-
configuration_signals: Sequence[SignalR] = (),
|
|
38
|
+
trigger_logic: FlyerController[T],
|
|
43
39
|
name: str = "",
|
|
44
40
|
):
|
|
45
41
|
self._trigger_logic = trigger_logic
|
|
46
|
-
self._configuration_signals = tuple(configuration_signals)
|
|
47
42
|
super().__init__(name=name)
|
|
48
43
|
|
|
49
44
|
@property
|
|
50
|
-
def trigger_logic(self) ->
|
|
45
|
+
def trigger_logic(self) -> FlyerController[T]:
|
|
51
46
|
return self._trigger_logic
|
|
52
47
|
|
|
53
48
|
@AsyncStatus.wrap
|
|
@@ -73,13 +68,3 @@ class StandardFlyer(
|
|
|
73
68
|
@AsyncStatus.wrap
|
|
74
69
|
async def complete(self) -> None:
|
|
75
70
|
await self._trigger_logic.complete()
|
|
76
|
-
|
|
77
|
-
async def describe_configuration(self) -> dict[str, DataKey]:
|
|
78
|
-
return await merge_gathered_dicts(
|
|
79
|
-
[sig.describe() for sig in self._configuration_signals]
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
async def read_configuration(self) -> dict[str, Reading]:
|
|
83
|
-
return await merge_gathered_dicts(
|
|
84
|
-
[sig.read() for sig in self._configuration_signals]
|
|
85
|
-
)
|
ophyd_async/core/_table.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import TypeVar, get_args, get_origin
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
4
5
|
from pydantic import BaseModel, ConfigDict, model_validator
|
|
@@ -6,6 +7,13 @@ from pydantic import BaseModel, ConfigDict, model_validator
|
|
|
6
7
|
TableSubclass = TypeVar("TableSubclass", bound="Table")
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
def _concat(value1, value2):
|
|
11
|
+
if isinstance(value1, np.ndarray):
|
|
12
|
+
return np.concatenate((value1, value2))
|
|
13
|
+
else:
|
|
14
|
+
return value1 + value2
|
|
15
|
+
|
|
16
|
+
|
|
9
17
|
class Table(BaseModel):
|
|
10
18
|
"""An abstraction of a Table of str to numpy array."""
|
|
11
19
|
|
|
@@ -13,34 +21,105 @@ class Table(BaseModel):
|
|
|
13
21
|
|
|
14
22
|
@staticmethod
|
|
15
23
|
def row(cls: type[TableSubclass], **kwargs) -> TableSubclass: # type: ignore
|
|
16
|
-
arrayified_kwargs = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
arrayified_kwargs = {}
|
|
25
|
+
for field_name, field_value in cls.model_fields.items():
|
|
26
|
+
value = kwargs.pop(field_name)
|
|
27
|
+
if field_value.default_factory is None:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
"`Table` models should have default factories for their "
|
|
30
|
+
"mutable empty columns."
|
|
31
|
+
)
|
|
32
|
+
default_array = field_value.default_factory()
|
|
33
|
+
if isinstance(default_array, np.ndarray):
|
|
34
|
+
arrayified_kwargs[field_name] = np.array(
|
|
35
|
+
[value], dtype=default_array.dtype
|
|
36
|
+
)
|
|
37
|
+
elif issubclass(type(value), Enum) and isinstance(value, str):
|
|
38
|
+
arrayified_kwargs[field_name] = [value]
|
|
39
|
+
else:
|
|
40
|
+
raise TypeError(
|
|
41
|
+
"Row column should be numpy arrays or sequence of string `Enum`."
|
|
21
42
|
)
|
|
43
|
+
if kwargs:
|
|
44
|
+
raise TypeError(
|
|
45
|
+
f"Unexpected keyword arguments {kwargs.keys()} for {cls.__name__}."
|
|
22
46
|
)
|
|
23
|
-
for field_name, field_value in cls.model_fields.items()
|
|
24
|
-
}
|
|
25
47
|
return cls(**arrayified_kwargs)
|
|
26
48
|
|
|
27
49
|
def __add__(self, right: TableSubclass) -> TableSubclass:
|
|
28
50
|
"""Concatenate the arrays in field values."""
|
|
29
51
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
if type(right) is not type(self):
|
|
53
|
+
raise RuntimeError(
|
|
54
|
+
f"{right} is not a `Table`, or is not the same "
|
|
55
|
+
f"type of `Table` as {self}."
|
|
56
|
+
)
|
|
34
57
|
|
|
35
58
|
return type(right)(
|
|
36
59
|
**{
|
|
37
|
-
field_name:
|
|
38
|
-
|
|
60
|
+
field_name: _concat(
|
|
61
|
+
getattr(self, field_name), getattr(right, field_name)
|
|
39
62
|
)
|
|
40
63
|
for field_name in self.model_fields
|
|
41
64
|
}
|
|
42
65
|
)
|
|
43
66
|
|
|
67
|
+
def numpy_dtype(self) -> np.dtype:
|
|
68
|
+
dtype = []
|
|
69
|
+
for field_name, field_value in self.model_fields.items():
|
|
70
|
+
if np.ndarray in (
|
|
71
|
+
get_origin(field_value.annotation),
|
|
72
|
+
field_value.annotation,
|
|
73
|
+
):
|
|
74
|
+
dtype.append((field_name, getattr(self, field_name).dtype))
|
|
75
|
+
else:
|
|
76
|
+
enum_type = get_args(field_value.annotation)[0]
|
|
77
|
+
assert issubclass(enum_type, Enum)
|
|
78
|
+
enum_values = [element.value for element in enum_type]
|
|
79
|
+
max_length_in_enum = max(len(value) for value in enum_values)
|
|
80
|
+
dtype.append((field_name, np.dtype(f"<U{max_length_in_enum}")))
|
|
81
|
+
|
|
82
|
+
return np.dtype(dtype)
|
|
83
|
+
|
|
84
|
+
def numpy_table(self):
|
|
85
|
+
# It would be nice to be able to use np.transpose for this,
|
|
86
|
+
# but it defaults to the largest dtype for everything.
|
|
87
|
+
dtype = self.numpy_dtype()
|
|
88
|
+
transposed_list = [
|
|
89
|
+
np.array(tuple(row), dtype=dtype)
|
|
90
|
+
for row in zip(*self.numpy_columns(), strict=False)
|
|
91
|
+
]
|
|
92
|
+
transposed = np.array(transposed_list, dtype=dtype)
|
|
93
|
+
return transposed
|
|
94
|
+
|
|
95
|
+
def numpy_columns(self) -> list[np.ndarray]:
|
|
96
|
+
"""Columns in the table can be lists of string enums or numpy arrays.
|
|
97
|
+
|
|
98
|
+
This method returns the columns, converting the string enums to numpy arrays.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
columns = []
|
|
102
|
+
for field_name, field_value in self.model_fields.items():
|
|
103
|
+
if np.ndarray in (
|
|
104
|
+
get_origin(field_value.annotation),
|
|
105
|
+
field_value.annotation,
|
|
106
|
+
):
|
|
107
|
+
columns.append(getattr(self, field_name))
|
|
108
|
+
else:
|
|
109
|
+
enum_type = get_args(field_value.annotation)[0]
|
|
110
|
+
assert issubclass(enum_type, Enum)
|
|
111
|
+
enum_values = [element.value for element in enum_type]
|
|
112
|
+
max_length_in_enum = max(len(value) for value in enum_values)
|
|
113
|
+
dtype = np.dtype(f"<U{max_length_in_enum}")
|
|
114
|
+
|
|
115
|
+
columns.append(
|
|
116
|
+
np.array(
|
|
117
|
+
[enum.value for enum in getattr(self, field_name)], dtype=dtype
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return columns
|
|
122
|
+
|
|
44
123
|
@model_validator(mode="after")
|
|
45
124
|
def validate_arrays(self) -> "Table":
|
|
46
125
|
first_length = len(next(iter(self))[1])
|
|
@@ -49,11 +128,15 @@ class Table(BaseModel):
|
|
|
49
128
|
), "Rows should all be of equal size."
|
|
50
129
|
|
|
51
130
|
if not all(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
131
|
+
# Checks if the values are numpy subtypes if the array is a numpy array,
|
|
132
|
+
# or if the value is a string enum.
|
|
133
|
+
np.issubdtype(getattr(self, field_name).dtype, default_array.dtype)
|
|
134
|
+
if isinstance(
|
|
135
|
+
default_array := self.model_fields[field_name].default_factory(), # type: ignore
|
|
136
|
+
np.ndarray,
|
|
55
137
|
)
|
|
56
|
-
|
|
138
|
+
else issubclass(get_args(field_value.annotation)[0], Enum)
|
|
139
|
+
for field_name, field_value in self.model_fields.items()
|
|
57
140
|
):
|
|
58
141
|
raise ValueError(
|
|
59
142
|
f"Cannot construct a `{type(self).__name__}`, "
|
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
from typing import Literal
|
|
3
3
|
|
|
4
4
|
from ophyd_async.core import (
|
|
5
|
-
|
|
5
|
+
DetectorController,
|
|
6
6
|
DetectorTrigger,
|
|
7
7
|
TriggerInfo,
|
|
8
8
|
set_and_wait_for_value,
|
|
@@ -18,7 +18,7 @@ from ._aravis_io import AravisDriverIO, AravisTriggerMode, AravisTriggerSource
|
|
|
18
18
|
_HIGHEST_POSSIBLE_DEADTIME = 1961e-6
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class AravisController(
|
|
21
|
+
class AravisController(DetectorController):
|
|
22
22
|
GPIO_NUMBER = Literal[1, 2, 3, 4]
|
|
23
23
|
|
|
24
24
|
def __init__(self, driver: AravisDriverIO, gpio_number: GPIO_NUMBER) -> None:
|
|
@@ -30,7 +30,7 @@ class AravisController(DetectorControl):
|
|
|
30
30
|
return _HIGHEST_POSSIBLE_DEADTIME
|
|
31
31
|
|
|
32
32
|
async def prepare(self, trigger_info: TriggerInfo):
|
|
33
|
-
if
|
|
33
|
+
if trigger_info.total_number_of_triggers == 0:
|
|
34
34
|
image_mode = adcore.ImageMode.continuous
|
|
35
35
|
else:
|
|
36
36
|
image_mode = adcore.ImageMode.multiple
|
|
@@ -43,7 +43,7 @@ class AravisController(DetectorControl):
|
|
|
43
43
|
|
|
44
44
|
await asyncio.gather(
|
|
45
45
|
self._drv.trigger_source.set(trigger_source),
|
|
46
|
-
self._drv.num_images.set(
|
|
46
|
+
self._drv.num_images.set(trigger_info.total_number_of_triggers),
|
|
47
47
|
self._drv.image_mode.set(image_mode),
|
|
48
48
|
)
|
|
49
49
|
|
|
@@ -4,7 +4,7 @@ from ophyd_async.core import (
|
|
|
4
4
|
DEFAULT_TIMEOUT,
|
|
5
5
|
AsyncStatus,
|
|
6
6
|
DatasetDescriber,
|
|
7
|
-
|
|
7
|
+
DetectorController,
|
|
8
8
|
set_and_wait_for_value,
|
|
9
9
|
)
|
|
10
10
|
from ophyd_async.epics.adcore._utils import convert_ad_dtype_to_np
|
|
@@ -34,7 +34,7 @@ class ADBaseDatasetDescriber(DatasetDescriber):
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
async def set_exposure_time_and_acquire_period_if_supplied(
|
|
37
|
-
controller:
|
|
37
|
+
controller: DetectorController,
|
|
38
38
|
driver: ADBaseIO,
|
|
39
39
|
exposure: float | None = None,
|
|
40
40
|
timeout: float = DEFAULT_TIMEOUT,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
|
-
from ophyd_async.core import
|
|
3
|
+
from ophyd_async.core import DetectorController, DetectorTrigger
|
|
4
4
|
from ophyd_async.core._detector import TriggerInfo
|
|
5
5
|
from ophyd_async.core._status import AsyncStatus
|
|
6
6
|
from ophyd_async.epics import adcore
|
|
@@ -15,7 +15,7 @@ KINETIX_TRIGGER_MODE_MAP = {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class KinetixController(
|
|
18
|
+
class KinetixController(DetectorController):
|
|
19
19
|
def __init__(
|
|
20
20
|
self,
|
|
21
21
|
driver: KinetixDriverIO,
|
|
@@ -29,7 +29,7 @@ class KinetixController(DetectorControl):
|
|
|
29
29
|
async def prepare(self, trigger_info: TriggerInfo):
|
|
30
30
|
await asyncio.gather(
|
|
31
31
|
self._drv.trigger_mode.set(KINETIX_TRIGGER_MODE_MAP[trigger_info.trigger]),
|
|
32
|
-
self._drv.num_images.set(trigger_info.
|
|
32
|
+
self._drv.num_images.set(trigger_info.total_number_of_triggers),
|
|
33
33
|
self._drv.image_mode.set(adcore.ImageMode.multiple),
|
|
34
34
|
)
|
|
35
35
|
if trigger_info.livetime is not None and trigger_info.trigger not in [
|
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
|
|
3
3
|
from ophyd_async.core import (
|
|
4
4
|
DEFAULT_TIMEOUT,
|
|
5
|
-
|
|
5
|
+
DetectorController,
|
|
6
6
|
DetectorTrigger,
|
|
7
7
|
wait_for_value,
|
|
8
8
|
)
|
|
@@ -13,7 +13,7 @@ from ophyd_async.epics import adcore
|
|
|
13
13
|
from ._pilatus_io import PilatusDriverIO, PilatusTriggerMode
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class PilatusController(
|
|
16
|
+
class PilatusController(DetectorController):
|
|
17
17
|
_supported_trigger_types = {
|
|
18
18
|
DetectorTrigger.internal: PilatusTriggerMode.internal,
|
|
19
19
|
DetectorTrigger.constant_gate: PilatusTriggerMode.ext_enable,
|
|
@@ -40,7 +40,9 @@ class PilatusController(DetectorControl):
|
|
|
40
40
|
await asyncio.gather(
|
|
41
41
|
self._drv.trigger_mode.set(self._get_trigger_mode(trigger_info.trigger)),
|
|
42
42
|
self._drv.num_images.set(
|
|
43
|
-
999_999
|
|
43
|
+
999_999
|
|
44
|
+
if trigger_info.total_number_of_triggers == 0
|
|
45
|
+
else trigger_info.total_number_of_triggers
|
|
44
46
|
),
|
|
45
47
|
self._drv.image_mode.set(adcore.ImageMode.multiple),
|
|
46
48
|
)
|
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
|
|
3
3
|
from ophyd_async.core import (
|
|
4
4
|
DEFAULT_TIMEOUT,
|
|
5
|
-
|
|
5
|
+
DetectorController,
|
|
6
6
|
DetectorTrigger,
|
|
7
7
|
)
|
|
8
8
|
from ophyd_async.core._detector import TriggerInfo
|
|
@@ -10,7 +10,7 @@ from ophyd_async.core._status import AsyncStatus
|
|
|
10
10
|
from ophyd_async.epics import adcore
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class SimController(
|
|
13
|
+
class SimController(DetectorController):
|
|
14
14
|
def __init__(
|
|
15
15
|
self,
|
|
16
16
|
driver: adcore.ADBaseIO,
|
|
@@ -32,7 +32,7 @@ class SimController(DetectorControl):
|
|
|
32
32
|
DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
|
|
33
33
|
)
|
|
34
34
|
await asyncio.gather(
|
|
35
|
-
self.driver.num_images.set(trigger_info.
|
|
35
|
+
self.driver.num_images.set(trigger_info.total_number_of_triggers),
|
|
36
36
|
self.driver.image_mode.set(adcore.ImageMode.multiple),
|
|
37
37
|
)
|
|
38
38
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
|
-
from ophyd_async.core import
|
|
3
|
+
from ophyd_async.core import DetectorController, DetectorTrigger
|
|
4
4
|
from ophyd_async.core._detector import TriggerInfo
|
|
5
5
|
from ophyd_async.core._status import AsyncStatus
|
|
6
6
|
from ophyd_async.epics import adcore
|
|
@@ -22,7 +22,7 @@ EXPOSE_OUT_MODE = {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
class VimbaController(
|
|
25
|
+
class VimbaController(DetectorController):
|
|
26
26
|
def __init__(
|
|
27
27
|
self,
|
|
28
28
|
driver: VimbaDriverIO,
|
|
@@ -37,7 +37,7 @@ class VimbaController(DetectorControl):
|
|
|
37
37
|
await asyncio.gather(
|
|
38
38
|
self._drv.trigger_mode.set(TRIGGER_MODE[trigger_info.trigger]),
|
|
39
39
|
self._drv.exposure_mode.set(EXPOSE_OUT_MODE[trigger_info.trigger]),
|
|
40
|
-
self._drv.num_images.set(trigger_info.
|
|
40
|
+
self._drv.num_images.set(trigger_info.total_number_of_triggers),
|
|
41
41
|
self._drv.image_mode.set(adcore.ImageMode.multiple),
|
|
42
42
|
)
|
|
43
43
|
if trigger_info.livetime is not None and trigger_info.trigger not in [
|
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
|
|
3
3
|
from ophyd_async.core import (
|
|
4
4
|
DEFAULT_TIMEOUT,
|
|
5
|
-
|
|
5
|
+
DetectorController,
|
|
6
6
|
DetectorTrigger,
|
|
7
7
|
set_and_wait_for_other_value,
|
|
8
8
|
)
|
|
@@ -18,7 +18,7 @@ EIGER_TRIGGER_MODE_MAP = {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class EigerController(
|
|
21
|
+
class EigerController(DetectorController):
|
|
22
22
|
def __init__(
|
|
23
23
|
self,
|
|
24
24
|
driver: EigerDriverIO,
|
|
@@ -41,7 +41,7 @@ class EigerController(DetectorControl):
|
|
|
41
41
|
self._drv.trigger_mode.set(
|
|
42
42
|
EIGER_TRIGGER_MODE_MAP[trigger_info.trigger].value
|
|
43
43
|
),
|
|
44
|
-
self._drv.num_images.set(trigger_info.
|
|
44
|
+
self._drv.num_images.set(trigger_info.total_number_of_triggers),
|
|
45
45
|
]
|
|
46
46
|
if trigger_info.livetime is not None:
|
|
47
47
|
coros.extend(
|