ophyd-async 0.5.2__py3-none-any.whl → 0.6.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.
Files changed (66) hide show
  1. ophyd_async/__init__.py +10 -1
  2. ophyd_async/__main__.py +12 -4
  3. ophyd_async/_version.py +2 -2
  4. ophyd_async/core/__init__.py +11 -3
  5. ophyd_async/core/_detector.py +72 -63
  6. ophyd_async/core/_device.py +13 -15
  7. ophyd_async/core/_device_save_loader.py +30 -19
  8. ophyd_async/core/_flyer.py +6 -4
  9. ophyd_async/core/_hdf_dataset.py +8 -9
  10. ophyd_async/core/_log.py +3 -1
  11. ophyd_async/core/_mock_signal_backend.py +11 -9
  12. ophyd_async/core/_mock_signal_utils.py +8 -5
  13. ophyd_async/core/_protocol.py +7 -7
  14. ophyd_async/core/_providers.py +11 -11
  15. ophyd_async/core/_readable.py +30 -22
  16. ophyd_async/core/_signal.py +52 -51
  17. ophyd_async/core/_signal_backend.py +20 -7
  18. ophyd_async/core/_soft_signal_backend.py +62 -32
  19. ophyd_async/core/_status.py +7 -9
  20. ophyd_async/core/_table.py +63 -0
  21. ophyd_async/core/_utils.py +24 -28
  22. ophyd_async/epics/adaravis/_aravis_controller.py +17 -16
  23. ophyd_async/epics/adaravis/_aravis_io.py +2 -1
  24. ophyd_async/epics/adcore/_core_io.py +2 -0
  25. ophyd_async/epics/adcore/_core_logic.py +2 -3
  26. ophyd_async/epics/adcore/_hdf_writer.py +19 -8
  27. ophyd_async/epics/adcore/_single_trigger.py +1 -1
  28. ophyd_async/epics/adcore/_utils.py +5 -6
  29. ophyd_async/epics/adkinetix/_kinetix_controller.py +19 -14
  30. ophyd_async/epics/adpilatus/_pilatus_controller.py +18 -16
  31. ophyd_async/epics/adsimdetector/_sim.py +6 -5
  32. ophyd_async/epics/adsimdetector/_sim_controller.py +20 -15
  33. ophyd_async/epics/advimba/_vimba_controller.py +21 -16
  34. ophyd_async/epics/demo/_mover.py +4 -5
  35. ophyd_async/epics/demo/sensor.db +0 -1
  36. ophyd_async/epics/eiger/_eiger.py +1 -1
  37. ophyd_async/epics/eiger/_eiger_controller.py +16 -16
  38. ophyd_async/epics/eiger/_odin_io.py +6 -5
  39. ophyd_async/epics/motor.py +8 -10
  40. ophyd_async/epics/pvi/_pvi.py +30 -33
  41. ophyd_async/epics/signal/_aioca.py +55 -25
  42. ophyd_async/epics/signal/_common.py +3 -10
  43. ophyd_async/epics/signal/_epics_transport.py +11 -8
  44. ophyd_async/epics/signal/_p4p.py +79 -30
  45. ophyd_async/epics/signal/_signal.py +6 -8
  46. ophyd_async/fastcs/panda/__init__.py +0 -6
  47. ophyd_async/fastcs/panda/_control.py +14 -15
  48. ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
  49. ophyd_async/fastcs/panda/_table.py +111 -138
  50. ophyd_async/fastcs/panda/_trigger.py +1 -2
  51. ophyd_async/fastcs/panda/_utils.py +3 -2
  52. ophyd_async/fastcs/panda/_writer.py +28 -13
  53. ophyd_async/plan_stubs/_fly.py +16 -16
  54. ophyd_async/plan_stubs/_nd_attributes.py +12 -6
  55. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
  56. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +24 -20
  57. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
  58. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
  59. ophyd_async/sim/demo/_sim_motor.py +2 -1
  60. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/METADATA +46 -45
  61. ophyd_async-0.6.0.dist-info/RECORD +96 -0
  62. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/WHEEL +1 -1
  63. ophyd_async-0.5.2.dist-info/RECORD +0 -95
  64. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/LICENSE +0 -0
  65. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/entry_points.txt +0 -0
  66. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,14 @@
1
- from dataclasses import dataclass
1
+ from collections.abc import Sequence
2
2
  from enum import Enum
3
- from typing import Optional, Sequence, Type, TypeVar
3
+ from typing import Annotated
4
4
 
5
5
  import numpy as np
6
6
  import numpy.typing as npt
7
- import pydantic_numpy.typing as pnd
8
- from typing_extensions import NotRequired, TypedDict
7
+ from pydantic import Field, field_validator, model_validator
8
+ from pydantic_numpy.helper.annotation import NpArrayPydanticAnnotation
9
+ from typing_extensions import TypedDict
10
+
11
+ from ophyd_async.core import Table
9
12
 
10
13
 
11
14
  class PandaHdf5DatasetType(str, Enum):
@@ -34,137 +37,107 @@ class SeqTrigger(str, Enum):
34
37
  POSC_LT = "POSC<=POSITION"
35
38
 
36
39
 
37
- @dataclass
38
- class SeqTableRow:
39
- repeats: int = 1
40
- trigger: SeqTrigger = SeqTrigger.IMMEDIATE
41
- position: int = 0
42
- time1: int = 0
43
- outa1: bool = False
44
- outb1: bool = False
45
- outc1: bool = False
46
- outd1: bool = False
47
- oute1: bool = False
48
- outf1: bool = False
49
- time2: int = 0
50
- outa2: bool = False
51
- outb2: bool = False
52
- outc2: bool = False
53
- outd2: bool = False
54
- oute2: bool = False
55
- outf2: bool = False
56
-
57
-
58
- class SeqTable(TypedDict):
59
- repeats: NotRequired[pnd.Np1DArrayUint16]
60
- trigger: NotRequired[Sequence[SeqTrigger]]
61
- position: NotRequired[pnd.Np1DArrayInt32]
62
- time1: NotRequired[pnd.Np1DArrayUint32]
63
- outa1: NotRequired[pnd.Np1DArrayBool]
64
- outb1: NotRequired[pnd.Np1DArrayBool]
65
- outc1: NotRequired[pnd.Np1DArrayBool]
66
- outd1: NotRequired[pnd.Np1DArrayBool]
67
- oute1: NotRequired[pnd.Np1DArrayBool]
68
- outf1: NotRequired[pnd.Np1DArrayBool]
69
- time2: NotRequired[pnd.Np1DArrayUint32]
70
- outa2: NotRequired[pnd.Np1DArrayBool]
71
- outb2: NotRequired[pnd.Np1DArrayBool]
72
- outc2: NotRequired[pnd.Np1DArrayBool]
73
- outd2: NotRequired[pnd.Np1DArrayBool]
74
- oute2: NotRequired[pnd.Np1DArrayBool]
75
- outf2: NotRequired[pnd.Np1DArrayBool]
76
-
77
-
78
- def seq_table_from_rows(*rows: SeqTableRow):
79
- """
80
- Constructs a sequence table from a series of rows.
81
- """
82
- return seq_table_from_arrays(
83
- repeats=np.array([row.repeats for row in rows], dtype=np.uint16),
84
- trigger=[row.trigger for row in rows],
85
- position=np.array([row.position for row in rows], dtype=np.int32),
86
- time1=np.array([row.time1 for row in rows], dtype=np.uint32),
87
- outa1=np.array([row.outa1 for row in rows], dtype=np.bool_),
88
- outb1=np.array([row.outb1 for row in rows], dtype=np.bool_),
89
- outc1=np.array([row.outc1 for row in rows], dtype=np.bool_),
90
- outd1=np.array([row.outd1 for row in rows], dtype=np.bool_),
91
- oute1=np.array([row.oute1 for row in rows], dtype=np.bool_),
92
- outf1=np.array([row.outf1 for row in rows], dtype=np.bool_),
93
- time2=np.array([row.time2 for row in rows], dtype=np.uint32),
94
- outa2=np.array([row.outa2 for row in rows], dtype=np.bool_),
95
- outb2=np.array([row.outb2 for row in rows], dtype=np.bool_),
96
- outc2=np.array([row.outc2 for row in rows], dtype=np.bool_),
97
- outd2=np.array([row.outd2 for row in rows], dtype=np.bool_),
98
- oute2=np.array([row.oute2 for row in rows], dtype=np.bool_),
99
- outf2=np.array([row.outf2 for row in rows], dtype=np.bool_),
100
- )
101
-
102
-
103
- T = TypeVar("T", bound=np.generic)
104
-
105
-
106
- def seq_table_from_arrays(
107
- *,
108
- repeats: Optional[npt.NDArray[np.uint16]] = None,
109
- trigger: Optional[Sequence[SeqTrigger]] = None,
110
- position: Optional[npt.NDArray[np.int32]] = None,
111
- time1: Optional[npt.NDArray[np.uint32]] = None,
112
- outa1: Optional[npt.NDArray[np.bool_]] = None,
113
- outb1: Optional[npt.NDArray[np.bool_]] = None,
114
- outc1: Optional[npt.NDArray[np.bool_]] = None,
115
- outd1: Optional[npt.NDArray[np.bool_]] = None,
116
- oute1: Optional[npt.NDArray[np.bool_]] = None,
117
- outf1: Optional[npt.NDArray[np.bool_]] = None,
118
- time2: npt.NDArray[np.uint32],
119
- outa2: Optional[npt.NDArray[np.bool_]] = None,
120
- outb2: Optional[npt.NDArray[np.bool_]] = None,
121
- outc2: Optional[npt.NDArray[np.bool_]] = None,
122
- outd2: Optional[npt.NDArray[np.bool_]] = None,
123
- oute2: Optional[npt.NDArray[np.bool_]] = None,
124
- outf2: Optional[npt.NDArray[np.bool_]] = None,
125
- ) -> SeqTable:
126
- """
127
- Constructs a sequence table from a series of columns as arrays.
128
- time2 is the only required argument and must not be None.
129
- All other provided arguments must be of equal length to time2.
130
- If any other argument is not given, or else given as None or empty,
131
- an array of length len(time2) filled with the following is defaulted:
132
- repeats: 1
133
- trigger: SeqTrigger.IMMEDIATE
134
- all others: 0/False as appropriate
135
- """
136
- assert time2 is not None, "time2 must be provided"
137
- length = len(time2)
138
- assert 0 < length < 4096, f"Length {length} not in range"
139
-
140
- def or_default(
141
- value: Optional[npt.NDArray[T]], dtype: Type[T], default_value: int = 0
142
- ) -> npt.NDArray[T]:
143
- if value is None or len(value) == 0:
144
- return np.full(length, default_value, dtype=dtype)
145
- return value
146
-
147
- table = SeqTable(
148
- repeats=or_default(repeats, np.uint16, 1),
149
- trigger=trigger or [SeqTrigger.IMMEDIATE] * length,
150
- position=or_default(position, np.int32),
151
- time1=or_default(time1, np.uint32),
152
- outa1=or_default(outa1, np.bool_),
153
- outb1=or_default(outb1, np.bool_),
154
- outc1=or_default(outc1, np.bool_),
155
- outd1=or_default(outd1, np.bool_),
156
- oute1=or_default(oute1, np.bool_),
157
- outf1=or_default(outf1, np.bool_),
158
- time2=time2,
159
- outa2=or_default(outa2, np.bool_),
160
- outb2=or_default(outb2, np.bool_),
161
- outc2=or_default(outc2, np.bool_),
162
- outd2=or_default(outd2, np.bool_),
163
- oute2=or_default(oute2, np.bool_),
164
- outf2=or_default(outf2, np.bool_),
165
- )
166
- for k, v in table.items():
167
- size = len(v) # type: ignore
168
- if size != length:
169
- raise ValueError(f"{k}: has length {size} not {length}")
170
- return table
40
+ PydanticNp1DArrayInt32 = Annotated[
41
+ np.ndarray[tuple[int], np.dtype[np.int32]],
42
+ NpArrayPydanticAnnotation.factory(
43
+ data_type=np.int32, dimensions=1, strict_data_typing=False
44
+ ),
45
+ Field(default_factory=lambda: np.array([], np.int32)),
46
+ ]
47
+ PydanticNp1DArrayBool = Annotated[
48
+ np.ndarray[tuple[int], np.dtype[np.bool_]],
49
+ NpArrayPydanticAnnotation.factory(
50
+ data_type=np.bool_, dimensions=1, strict_data_typing=False
51
+ ),
52
+ Field(default_factory=lambda: np.array([], dtype=np.bool_)),
53
+ ]
54
+ TriggerStr = Annotated[
55
+ np.ndarray[tuple[int], np.dtype[np.unicode_]],
56
+ NpArrayPydanticAnnotation.factory(
57
+ data_type=np.unicode_, dimensions=1, strict_data_typing=False
58
+ ),
59
+ Field(default_factory=lambda: np.array([], dtype=np.dtype("<U32"))),
60
+ ]
61
+
62
+
63
+ class SeqTable(Table):
64
+ repeats: PydanticNp1DArrayInt32
65
+ trigger: TriggerStr
66
+ position: PydanticNp1DArrayInt32
67
+ time1: PydanticNp1DArrayInt32
68
+ outa1: PydanticNp1DArrayBool
69
+ outb1: PydanticNp1DArrayBool
70
+ outc1: PydanticNp1DArrayBool
71
+ outd1: PydanticNp1DArrayBool
72
+ oute1: PydanticNp1DArrayBool
73
+ outf1: PydanticNp1DArrayBool
74
+ time2: PydanticNp1DArrayInt32
75
+ outa2: PydanticNp1DArrayBool
76
+ outb2: PydanticNp1DArrayBool
77
+ outc2: PydanticNp1DArrayBool
78
+ outd2: PydanticNp1DArrayBool
79
+ oute2: PydanticNp1DArrayBool
80
+ outf2: PydanticNp1DArrayBool
81
+
82
+ @classmethod
83
+ def row( # type: ignore
84
+ cls,
85
+ *,
86
+ repeats: int = 1,
87
+ trigger: str = SeqTrigger.IMMEDIATE,
88
+ position: int = 0,
89
+ time1: int = 0,
90
+ outa1: bool = False,
91
+ outb1: bool = False,
92
+ outc1: bool = False,
93
+ outd1: bool = False,
94
+ oute1: bool = False,
95
+ outf1: bool = False,
96
+ time2: int = 0,
97
+ outa2: bool = False,
98
+ outb2: bool = False,
99
+ outc2: bool = False,
100
+ outd2: bool = False,
101
+ oute2: bool = False,
102
+ outf2: bool = False,
103
+ ) -> "SeqTable":
104
+ if isinstance(trigger, SeqTrigger):
105
+ trigger = trigger.value
106
+ return super().row(**locals())
107
+
108
+ @field_validator("trigger", mode="before")
109
+ @classmethod
110
+ def trigger_to_np_array(cls, trigger_column):
111
+ """
112
+ The user can provide a list of SeqTrigger enum elements instead of a numpy str.
113
+ """
114
+ if isinstance(trigger_column, Sequence) and all(
115
+ isinstance(trigger, SeqTrigger) for trigger in trigger_column
116
+ ):
117
+ trigger_column = np.array(
118
+ [trigger.value for trigger in trigger_column], dtype=np.dtype("<U32")
119
+ )
120
+ elif isinstance(trigger_column, Sequence) or isinstance(
121
+ trigger_column, np.ndarray
122
+ ):
123
+ for trigger in trigger_column:
124
+ SeqTrigger(
125
+ trigger
126
+ ) # To check all the given strings are actually `SeqTrigger`s
127
+ else:
128
+ raise ValueError(
129
+ "Expected a numpy array or a sequence of `SeqTrigger`, got "
130
+ f"{type(trigger_column)}."
131
+ )
132
+ return trigger_column
133
+
134
+ @model_validator(mode="after")
135
+ def validate_max_length(self) -> "SeqTable":
136
+ """
137
+ Used to check max_length. Unfortunately trying the `max_length` arg in
138
+ the pydantic field doesn't work
139
+ """
140
+
141
+ first_length = len(next(iter(self))[1])
142
+ assert 0 <= first_length < 4096, f"Length {first_length} not in range."
143
+ return self
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- from typing import Optional
3
2
 
4
3
  from pydantic import BaseModel, Field
5
4
 
@@ -82,7 +81,7 @@ class StaticPcompTriggerLogic(TriggerLogic[PcompInfo]):
82
81
  await self.pcomp.enable.set("ONE")
83
82
  await wait_for_value(self.pcomp.active, True, timeout=1)
84
83
 
85
- async def complete(self, timeout: Optional[float] = None) -> None:
84
+ async def complete(self, timeout: float | None = None) -> None:
86
85
  await wait_for_value(self.pcomp.active, False, timeout=timeout)
87
86
 
88
87
  async def stop(self):
@@ -1,7 +1,8 @@
1
- from typing import Any, Dict, Sequence
1
+ from collections.abc import Sequence
2
+ from typing import Any
2
3
 
3
4
 
4
- def phase_sorter(panda_signal_values: Dict[str, Any]) -> Sequence[Dict[str, Any]]:
5
+ def phase_sorter(panda_signal_values: dict[str, Any]) -> Sequence[dict[str, Any]]:
5
6
  # Panda has two load phases. If the signal name ends in the string "UNITS",
6
7
  # it needs to be loaded first so put in first phase
7
8
  phase_1, phase_2 = {}, {}
@@ -1,8 +1,9 @@
1
1
  import asyncio
2
+ from collections.abc import AsyncGenerator, AsyncIterator
2
3
  from pathlib import Path
3
- from typing import AsyncGenerator, AsyncIterator, Dict, List, Optional
4
4
 
5
- from bluesky.protocols import DataKey, StreamAsset
5
+ from bluesky.protocols import StreamAsset
6
+ from event_model import DataKey
6
7
  from p4p.client.thread import Context
7
8
 
8
9
  from ophyd_async.core import (
@@ -20,7 +21,7 @@ from ._block import DataBlock
20
21
 
21
22
 
22
23
  class PandaHDFWriter(DetectorWriter):
23
- _ctxt: Optional[Context] = None
24
+ _ctxt: Context | None = None
24
25
 
25
26
  def __init__(
26
27
  self,
@@ -33,12 +34,12 @@ class PandaHDFWriter(DetectorWriter):
33
34
  self._prefix = prefix
34
35
  self._path_provider = path_provider
35
36
  self._name_provider = name_provider
36
- self._datasets: List[HDFDataset] = []
37
- self._file: Optional[HDFFile] = None
37
+ self._datasets: list[HDFDataset] = []
38
+ self._file: HDFFile | None = None
38
39
  self._multiplier = 1
39
40
 
40
41
  # Triggered on PCAP arm
41
- async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
42
+ async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
42
43
  """Retrieve and get descriptor of all PandA signals marked for capture"""
43
44
 
44
45
  # Ensure flushes are immediate
@@ -76,7 +77,7 @@ class PandaHDFWriter(DetectorWriter):
76
77
 
77
78
  return await self._describe()
78
79
 
79
- async def _describe(self) -> Dict[str, DataKey]:
80
+ async def _describe(self) -> dict[str, DataKey]:
80
81
  """
81
82
  Return a describe based on the datasets PV
82
83
  """
@@ -85,9 +86,11 @@ class PandaHDFWriter(DetectorWriter):
85
86
  describe = {
86
87
  ds.data_key: DataKey(
87
88
  source=self.panda_data_block.hdf_directory.source,
88
- shape=ds.shape,
89
+ shape=list(ds.shape),
89
90
  dtype="array" if ds.shape != [1] else "number",
90
- dtype_numpy="<f8", # PandA data should always be written as Float64
91
+ # PandA data should always be written as Float64
92
+ # Ignore type check until https://github.com/bluesky/event-model/issues/308
93
+ dtype_numpy="<f8", # type: ignore
91
94
  external="STREAM:",
92
95
  )
93
96
  for ds in self._datasets
@@ -102,15 +105,27 @@ class PandaHDFWriter(DetectorWriter):
102
105
 
103
106
  capture_table = await self.panda_data_block.datasets.get_value()
104
107
  self._datasets = [
105
- HDFDataset(dataset_name, "/" + dataset_name, [1], multiplier=1)
108
+ # TODO: Update chunk size to read signal once available in IOC
109
+ # Currently PandA IOC sets chunk size to 1024 points per chunk
110
+ HDFDataset(
111
+ dataset_name, "/" + dataset_name, [1], multiplier=1, chunk_shape=(1024,)
112
+ )
106
113
  for dataset_name in capture_table["name"]
107
114
  ]
108
115
 
116
+ # Warn user if dataset table is empty in PandA
117
+ # i.e. no stream resources will be generated
118
+ if len(self._datasets) == 0:
119
+ self.panda_data_block.log.warning(
120
+ f"PandA {self._name_provider()} DATASETS table is empty! "
121
+ "No stream resource docs will be generated. "
122
+ "Make sure captured positions have their corresponding "
123
+ "*:DATASET PV set to a scientifically relevant name."
124
+ )
125
+
109
126
  # Next few functions are exactly the same as AD writer. Could move as default
110
127
  # StandardDetector behavior
111
- async def wait_for_index(
112
- self, index: int, timeout: Optional[float] = DEFAULT_TIMEOUT
113
- ):
128
+ async def wait_for_index(self, index: int, timeout: float | None = DEFAULT_TIMEOUT):
114
129
  def matcher(value: int) -> bool:
115
130
  return value >= index
116
131
 
@@ -1,5 +1,3 @@
1
- from typing import List, Optional
2
-
3
1
  import bluesky.plan_stubs as bps
4
2
  from bluesky.utils import short_uid
5
3
 
@@ -15,14 +13,12 @@ from ophyd_async.fastcs.panda import (
15
13
  PcompInfo,
16
14
  SeqTable,
17
15
  SeqTableInfo,
18
- SeqTableRow,
19
- seq_table_from_rows,
20
16
  )
21
17
 
22
18
 
23
19
  def prepare_static_pcomp_flyer_and_detectors(
24
20
  flyer: StandardFlyer[PcompInfo],
25
- detectors: List[StandardDetector],
21
+ detectors: list[StandardDetector],
26
22
  pcomp_info: PcompInfo,
27
23
  trigger_info: TriggerInfo,
28
24
  ):
@@ -41,13 +37,14 @@ def prepare_static_pcomp_flyer_and_detectors(
41
37
 
42
38
  def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
43
39
  flyer: StandardFlyer[SeqTableInfo],
44
- detectors: List[StandardDetector],
40
+ detectors: list[StandardDetector],
45
41
  number_of_frames: int,
46
42
  exposure: float,
47
43
  shutter_time: float,
48
44
  repeats: int = 1,
49
45
  period: float = 0.0,
50
- frame_timeout: Optional[float] = None,
46
+ frame_timeout: float | None = None,
47
+ iteration: int = 1,
51
48
  ):
52
49
  """Prepare a hardware triggered flyable and one or more detectors.
53
50
 
@@ -70,28 +67,31 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
70
67
  deadtime=deadtime,
71
68
  livetime=exposure,
72
69
  frame_timeout=frame_timeout,
70
+ iteration=iteration,
73
71
  )
74
72
  trigger_time = number_of_frames * (exposure + deadtime)
75
73
  pre_delay = max(period - 2 * shutter_time - trigger_time, 0)
76
74
 
77
- table: SeqTable = seq_table_from_rows(
75
+ table = (
78
76
  # Wait for pre-delay then open shutter
79
- SeqTableRow(
77
+ SeqTable.row(
80
78
  time1=in_micros(pre_delay),
81
79
  time2=in_micros(shutter_time),
82
80
  outa2=True,
83
- ),
81
+ )
82
+ +
84
83
  # Keeping shutter open, do N triggers
85
- SeqTableRow(
84
+ SeqTable.row(
86
85
  repeats=number_of_frames,
87
86
  time1=in_micros(exposure),
88
87
  outa1=True,
89
88
  outb1=True,
90
89
  time2=in_micros(deadtime),
91
90
  outa2=True,
92
- ),
91
+ )
92
+ +
93
93
  # Add the shutter close
94
- SeqTableRow(time2=in_micros(shutter_time)),
94
+ SeqTable.row(time2=in_micros(shutter_time))
95
95
  )
96
96
 
97
97
  table_info = SeqTableInfo(sequence_table=table, repeats=repeats)
@@ -105,7 +105,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
105
105
  def fly_and_collect(
106
106
  stream_name: str,
107
107
  flyer: StandardFlyer[SeqTableInfo] | StandardFlyer[PcompInfo],
108
- detectors: List[StandardDetector],
108
+ detectors: list[StandardDetector],
109
109
  ):
110
110
  """Kickoff, complete and collect with a flyer and multiple detectors.
111
111
 
@@ -145,7 +145,7 @@ def fly_and_collect(
145
145
  def fly_and_collect_with_static_pcomp(
146
146
  stream_name: str,
147
147
  flyer: StandardFlyer[PcompInfo],
148
- detectors: List[StandardDetector],
148
+ detectors: list[StandardDetector],
149
149
  number_of_pulses: int,
150
150
  pulse_width: int,
151
151
  rising_edge_step: int,
@@ -171,7 +171,7 @@ def fly_and_collect_with_static_pcomp(
171
171
  def time_resolved_fly_and_collect_with_static_seq_table(
172
172
  stream_name: str,
173
173
  flyer: StandardFlyer[SeqTableInfo],
174
- detectors: List[StandardDetector],
174
+ detectors: list[StandardDetector],
175
175
  number_of_frames: int,
176
176
  exposure: float,
177
177
  shutter_time: float,
@@ -1,14 +1,15 @@
1
- from typing import Sequence
2
- from xml.etree import cElementTree as ET
1
+ from collections.abc import Sequence
2
+ from xml.etree import ElementTree as ET
3
3
 
4
4
  import bluesky.plan_stubs as bps
5
5
 
6
- from ophyd_async.core._device import Device
7
- from ophyd_async.epics.adcore._core_io import NDArrayBaseIO
8
- from ophyd_async.epics.adcore._utils import (
6
+ from ophyd_async.core import Device
7
+ from ophyd_async.epics.adcore import (
8
+ NDArrayBaseIO,
9
9
  NDAttributeDataType,
10
10
  NDAttributeParam,
11
11
  NDAttributePv,
12
+ NDFileHDFIO,
12
13
  )
13
14
 
14
15
 
@@ -48,9 +49,14 @@ def setup_ndattributes(
48
49
 
49
50
 
50
51
  def setup_ndstats_sum(detector: Device):
52
+ hdf = getattr(detector, "hdf", None)
53
+ assert isinstance(hdf, NDFileHDFIO), (
54
+ f"Expected {detector.name} to have 'hdf' attribute that is an NDFilHDFIO, "
55
+ f"got {hdf}"
56
+ )
51
57
  yield from (
52
58
  setup_ndattributes(
53
- detector.hdf,
59
+ hdf,
54
60
  [
55
61
  NDAttributeParam(
56
62
  name=f"{detector.name}-sum",
@@ -1,10 +1,10 @@
1
+ from collections.abc import Sequence
1
2
  from pathlib import Path
2
- from typing import Sequence
3
3
 
4
4
  from ophyd_async.core import (
5
- AsyncReadable,
6
5
  FilenameProvider,
7
6
  PathProvider,
7
+ SignalR,
8
8
  StandardDetector,
9
9
  StaticFilenameProvider,
10
10
  StaticPathProvider,
@@ -19,7 +19,7 @@ class PatternDetector(StandardDetector):
19
19
  def __init__(
20
20
  self,
21
21
  path: Path,
22
- config_sigs: Sequence[AsyncReadable] = [],
22
+ config_sigs: Sequence[SignalR] = (),
23
23
  name: str = "",
24
24
  ) -> None:
25
25
  fp: FilenameProvider = StaticFilenameProvider(name)
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
- from typing import Optional
3
2
 
4
- from ophyd_async.core import AsyncStatus, DetectorControl, DetectorTrigger, PathProvider
3
+ from ophyd_async.core import DetectorControl, PathProvider
4
+ from ophyd_async.core._detector import TriggerInfo
5
5
 
6
6
  from ._pattern_generator import PatternGenerator
7
7
 
@@ -14,36 +14,40 @@ class PatternDetectorController(DetectorControl):
14
14
  exposure: float = 0.1,
15
15
  ) -> None:
16
16
  self.pattern_generator: PatternGenerator = pattern_generator
17
- if exposure is None:
18
- exposure = 0.1
19
17
  self.pattern_generator.set_exposure(exposure)
20
18
  self.path_provider: PathProvider = path_provider
21
- self.task: Optional[asyncio.Task] = None
19
+ self.task: asyncio.Task | None = None
22
20
  super().__init__()
23
21
 
24
- async def arm(
25
- self,
26
- num: int,
27
- trigger: DetectorTrigger = DetectorTrigger.internal,
28
- exposure: Optional[float] = 0.01,
29
- ) -> AsyncStatus:
30
- if exposure is None:
31
- exposure = 0.1
32
- period: float = exposure + self.get_deadtime(exposure)
33
- task = asyncio.create_task(
34
- self._coroutine_for_image_writing(exposure, period, num)
22
+ async def prepare(self, trigger_info: TriggerInfo):
23
+ self._trigger_info = trigger_info
24
+ if self._trigger_info.livetime is None:
25
+ self._trigger_info.livetime = 0.01
26
+ self.period: float = self._trigger_info.livetime + self.get_deadtime(
27
+ trigger_info.livetime
35
28
  )
36
- self.task = task
37
- return AsyncStatus(task)
38
29
 
39
- async def disarm(self):
30
+ async def arm(self):
31
+ assert self._trigger_info.livetime
32
+ assert self.period
33
+ self.task = asyncio.create_task(
34
+ self._coroutine_for_image_writing(
35
+ self._trigger_info.livetime, self.period, self._trigger_info.number
36
+ )
37
+ )
38
+
39
+ async def wait_for_idle(self):
40
40
  if self.task:
41
+ await self.task
42
+
43
+ async def disarm(self):
44
+ if self.task and not self.task.done():
41
45
  self.task.cancel()
42
46
  try:
43
47
  await self.task
44
48
  except asyncio.CancelledError:
45
49
  pass
46
- self.task = None
50
+ self.task = None
47
51
 
48
52
  def get_deadtime(self, exposure: float | None) -> float:
49
53
  return 0.001
@@ -1,8 +1,8 @@
1
- from typing import AsyncGenerator, AsyncIterator, Dict
1
+ from collections.abc import AsyncGenerator, AsyncIterator
2
2
 
3
- from bluesky.protocols import DataKey
3
+ from event_model import DataKey
4
4
 
5
- from ophyd_async.core import DetectorWriter, NameProvider, PathProvider
5
+ from ophyd_async.core import DEFAULT_TIMEOUT, DetectorWriter, NameProvider, PathProvider
6
6
 
7
7
  from ._pattern_generator import PatternGenerator
8
8
 
@@ -20,7 +20,7 @@ class PatternDetectorWriter(DetectorWriter):
20
20
  self.path_provider = path_provider
21
21
  self.name_provider = name_provider
22
22
 
23
- async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
23
+ async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
24
24
  return await self.pattern_generator.open_file(
25
25
  self.path_provider, self.name_provider(), multiplier
26
26
  )
@@ -31,8 +31,11 @@ class PatternDetectorWriter(DetectorWriter):
31
31
  def collect_stream_docs(self, indices_written: int) -> AsyncIterator:
32
32
  return self.pattern_generator.collect_stream_docs(indices_written)
33
33
 
34
- def observe_indices_written(self, timeout=...) -> AsyncGenerator[int, None]:
35
- return self.pattern_generator.observe_indices_written()
34
+ async def observe_indices_written(
35
+ self, timeout=DEFAULT_TIMEOUT
36
+ ) -> AsyncGenerator[int, None]:
37
+ async for index in self.pattern_generator.observe_indices_written(timeout):
38
+ yield index
36
39
 
37
40
  async def get_indices_written(self) -> int:
38
41
  return self.pattern_generator.image_counter