ophyd-async 0.5.2__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.
Files changed (80) 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 +15 -7
  5. ophyd_async/core/_detector.py +133 -87
  6. ophyd_async/core/_device.py +19 -16
  7. ophyd_async/core/_device_save_loader.py +30 -19
  8. ophyd_async/core/_flyer.py +6 -19
  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 +146 -0
  21. ophyd_async/core/_utils.py +24 -28
  22. ophyd_async/epics/adaravis/_aravis_controller.py +20 -19
  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 +4 -5
  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 +20 -15
  30. ophyd_async/epics/adpilatus/_pilatus_controller.py +22 -18
  31. ophyd_async/epics/adsimdetector/_sim.py +7 -6
  32. ophyd_async/epics/adsimdetector/_sim_controller.py +22 -17
  33. ophyd_async/epics/advimba/_vimba_controller.py +22 -17
  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 +18 -18
  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/_block.py +7 -0
  48. ophyd_async/fastcs/panda/_control.py +16 -17
  49. ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
  50. ophyd_async/fastcs/panda/_table.py +77 -138
  51. ophyd_async/fastcs/panda/_trigger.py +4 -5
  52. ophyd_async/fastcs/panda/_utils.py +3 -2
  53. ophyd_async/fastcs/panda/_writer.py +30 -15
  54. ophyd_async/plan_stubs/_fly.py +15 -17
  55. ophyd_async/plan_stubs/_nd_attributes.py +12 -6
  56. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
  57. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +27 -21
  58. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
  59. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
  60. ophyd_async/sim/demo/_sim_motor.py +2 -1
  61. ophyd_async/tango/__init__.py +45 -0
  62. ophyd_async/tango/base_devices/__init__.py +4 -0
  63. ophyd_async/tango/base_devices/_base_device.py +225 -0
  64. ophyd_async/tango/base_devices/_tango_readable.py +33 -0
  65. ophyd_async/tango/demo/__init__.py +12 -0
  66. ophyd_async/tango/demo/_counter.py +37 -0
  67. ophyd_async/tango/demo/_detector.py +42 -0
  68. ophyd_async/tango/demo/_mover.py +77 -0
  69. ophyd_async/tango/demo/_tango/__init__.py +3 -0
  70. ophyd_async/tango/demo/_tango/_servers.py +108 -0
  71. ophyd_async/tango/signal/__init__.py +39 -0
  72. ophyd_async/tango/signal/_signal.py +223 -0
  73. ophyd_async/tango/signal/_tango_transport.py +764 -0
  74. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/METADATA +50 -45
  75. ophyd_async-0.7.0.dist-info/RECORD +108 -0
  76. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/WHEEL +1 -1
  77. ophyd_async-0.5.2.dist-info/RECORD +0 -95
  78. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/LICENSE +0 -0
  79. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/entry_points.txt +0 -0
  80. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/top_level.txt +0 -0
@@ -1,38 +1,37 @@
1
1
  import asyncio
2
- from typing import Optional
3
2
 
4
3
  from ophyd_async.core import (
5
- AsyncStatus,
6
- DetectorControl,
4
+ DetectorController,
7
5
  DetectorTrigger,
8
6
  wait_for_value,
9
7
  )
8
+ from ophyd_async.core._detector import TriggerInfo
9
+ from ophyd_async.core._status import AsyncStatus
10
10
 
11
11
  from ._block import PcapBlock
12
12
 
13
13
 
14
- class PandaPcapController(DetectorControl):
14
+ class PandaPcapController(DetectorController):
15
15
  def __init__(self, pcap: PcapBlock) -> None:
16
16
  self.pcap = pcap
17
+ self._arm_status: AsyncStatus | None = None
17
18
 
18
- def get_deadtime(self, exposure: float) -> float:
19
+ def get_deadtime(self, exposure: float | None) -> float:
19
20
  return 0.000000008
20
21
 
21
- async def arm(
22
- self,
23
- num: int,
24
- trigger: DetectorTrigger = DetectorTrigger.constant_gate,
25
- exposure: Optional[float] = None,
26
- ) -> AsyncStatus:
27
- assert trigger in (
22
+ async def prepare(self, trigger_info: TriggerInfo):
23
+ assert trigger_info.trigger in (
28
24
  DetectorTrigger.constant_gate,
29
- trigger == DetectorTrigger.variable_gate,
25
+ DetectorTrigger.variable_gate,
30
26
  ), "Only constant_gate and variable_gate triggering is supported on the PandA"
31
- await asyncio.gather(self.pcap.arm.set(True))
27
+
28
+ async def arm(self):
29
+ self._arm_status = self.pcap.arm.set(True)
32
30
  await wait_for_value(self.pcap.active, True, timeout=1)
33
- return AsyncStatus(wait_for_value(self.pcap.active, False, timeout=None))
34
31
 
35
- async def disarm(self) -> AsyncStatus:
32
+ async def wait_for_idle(self):
33
+ pass
34
+
35
+ async def disarm(self):
36
36
  await asyncio.gather(self.pcap.arm.set(False))
37
37
  await wait_for_value(self.pcap.active, False, timeout=1)
38
- return AsyncStatus(wait_for_value(self.pcap.active, False, timeout=None))
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Sequence
3
+ from collections.abc import Sequence
4
4
 
5
5
  from ophyd_async.core import DEFAULT_TIMEOUT, PathProvider, SignalR, StandardDetector
6
6
  from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries
@@ -36,7 +36,14 @@ class HDFPanda(CommonPandaBlocks, StandardDetector):
36
36
  )
37
37
 
38
38
  async def connect(
39
- self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT
40
- ) -> None:
39
+ self,
40
+ mock: bool = False,
41
+ timeout: float = DEFAULT_TIMEOUT,
42
+ force_reconnect: bool = False,
43
+ ):
44
+ # TODO: this doesn't support caching
45
+ # https://github.com/bluesky/ophyd-async/issues/472
41
46
  await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, mock=mock)
42
- await super().connect(mock=mock, timeout=timeout)
47
+ await super().connect(
48
+ mock=mock, timeout=timeout, force_reconnect=force_reconnect
49
+ )
@@ -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, 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,73 @@ 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[Sequence[SeqTrigger], Field(default_factory=list)]
55
+
56
+
57
+ class SeqTable(Table):
58
+ repeats: PydanticNp1DArrayInt32
59
+ trigger: TriggerStr
60
+ position: PydanticNp1DArrayInt32
61
+ time1: PydanticNp1DArrayInt32
62
+ outa1: PydanticNp1DArrayBool
63
+ outb1: PydanticNp1DArrayBool
64
+ outc1: PydanticNp1DArrayBool
65
+ outd1: PydanticNp1DArrayBool
66
+ oute1: PydanticNp1DArrayBool
67
+ outf1: PydanticNp1DArrayBool
68
+ time2: PydanticNp1DArrayInt32
69
+ outa2: PydanticNp1DArrayBool
70
+ outb2: PydanticNp1DArrayBool
71
+ outc2: PydanticNp1DArrayBool
72
+ outd2: PydanticNp1DArrayBool
73
+ oute2: PydanticNp1DArrayBool
74
+ outf2: PydanticNp1DArrayBool
75
+
76
+ @classmethod
77
+ def row( # type: ignore
78
+ cls,
79
+ *,
80
+ repeats: int = 1,
81
+ trigger: str = SeqTrigger.IMMEDIATE,
82
+ position: int = 0,
83
+ time1: int = 0,
84
+ outa1: bool = False,
85
+ outb1: bool = False,
86
+ outc1: bool = False,
87
+ outd1: bool = False,
88
+ oute1: bool = False,
89
+ outf1: bool = False,
90
+ time2: int = 0,
91
+ outa2: bool = False,
92
+ outb2: bool = False,
93
+ outc2: bool = False,
94
+ outd2: bool = False,
95
+ oute2: bool = False,
96
+ outf2: bool = False,
97
+ ) -> "SeqTable":
98
+ return Table.row(**locals())
99
+
100
+ @model_validator(mode="after")
101
+ def validate_max_length(self) -> "SeqTable":
102
+ """
103
+ Used to check max_length. Unfortunately trying the `max_length` arg in
104
+ the pydantic field doesn't work
105
+ """
106
+
107
+ first_length = len(next(iter(self))[1])
108
+ assert 0 <= first_length < 4096, f"Length {first_length} not in range."
109
+ return self
@@ -1,9 +1,8 @@
1
1
  import asyncio
2
- from typing import Optional
3
2
 
4
3
  from pydantic import BaseModel, Field
5
4
 
6
- from ophyd_async.core import TriggerLogic, wait_for_value
5
+ from ophyd_async.core import FlyerController, wait_for_value
7
6
 
8
7
  from ._block import PcompBlock, PcompDirectionOptions, SeqBlock, TimeUnits
9
8
  from ._table import SeqTable
@@ -15,7 +14,7 @@ class SeqTableInfo(BaseModel):
15
14
  prescale_as_us: float = Field(default=1, ge=0) # microseconds
16
15
 
17
16
 
18
- class StaticSeqTableTriggerLogic(TriggerLogic[SeqTableInfo]):
17
+ class StaticSeqTableTriggerLogic(FlyerController[SeqTableInfo]):
19
18
  def __init__(self, seq: SeqBlock) -> None:
20
19
  self.seq = seq
21
20
 
@@ -64,7 +63,7 @@ class PcompInfo(BaseModel):
64
63
  )
65
64
 
66
65
 
67
- class StaticPcompTriggerLogic(TriggerLogic[PcompInfo]):
66
+ class StaticPcompTriggerLogic(FlyerController[PcompInfo]):
68
67
  def __init__(self, pcomp: PcompBlock) -> None:
69
68
  self.pcomp = pcomp
70
69
 
@@ -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 (
@@ -16,11 +17,11 @@ from ophyd_async.core import (
16
17
  wait_for_value,
17
18
  )
18
19
 
19
- from ._block import DataBlock
20
+ from ._block import CaptureMode, 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
@@ -57,7 +58,7 @@ class PandaHDFWriter(DetectorWriter):
57
58
  self.panda_data_block.hdf_file_name.set(
58
59
  f"{info.filename}.h5",
59
60
  ),
60
- self.panda_data_block.num_capture.set(0),
61
+ self.panda_data_block.capture_mode.set(CaptureMode.FOREVER),
61
62
  )
62
63
 
63
64
  # Make sure that directory exists or has been created.
@@ -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,13 @@ 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,
51
47
  ):
52
48
  """Prepare a hardware triggered flyable and one or more detectors.
53
49
 
@@ -65,7 +61,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
65
61
  deadtime = max(det.controller.get_deadtime(exposure) for det in detectors)
66
62
 
67
63
  trigger_info = TriggerInfo(
68
- number=number_of_frames * repeats,
64
+ number_of_triggers=number_of_frames * repeats,
69
65
  trigger=DetectorTrigger.constant_gate,
70
66
  deadtime=deadtime,
71
67
  livetime=exposure,
@@ -74,24 +70,26 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
74
70
  trigger_time = number_of_frames * (exposure + deadtime)
75
71
  pre_delay = max(period - 2 * shutter_time - trigger_time, 0)
76
72
 
77
- table: SeqTable = seq_table_from_rows(
73
+ table = (
78
74
  # Wait for pre-delay then open shutter
79
- SeqTableRow(
75
+ SeqTable.row(
80
76
  time1=in_micros(pre_delay),
81
77
  time2=in_micros(shutter_time),
82
78
  outa2=True,
83
- ),
79
+ )
80
+ +
84
81
  # Keeping shutter open, do N triggers
85
- SeqTableRow(
82
+ SeqTable.row(
86
83
  repeats=number_of_frames,
87
84
  time1=in_micros(exposure),
88
85
  outa1=True,
89
86
  outb1=True,
90
87
  time2=in_micros(deadtime),
91
88
  outa2=True,
92
- ),
89
+ )
90
+ +
93
91
  # Add the shutter close
94
- SeqTableRow(time2=in_micros(shutter_time)),
92
+ SeqTable.row(time2=in_micros(shutter_time))
95
93
  )
96
94
 
97
95
  table_info = SeqTableInfo(sequence_table=table, repeats=repeats)
@@ -105,7 +103,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
105
103
  def fly_and_collect(
106
104
  stream_name: str,
107
105
  flyer: StandardFlyer[SeqTableInfo] | StandardFlyer[PcompInfo],
108
- detectors: List[StandardDetector],
106
+ detectors: list[StandardDetector],
109
107
  ):
110
108
  """Kickoff, complete and collect with a flyer and multiple detectors.
111
109
 
@@ -145,7 +143,7 @@ def fly_and_collect(
145
143
  def fly_and_collect_with_static_pcomp(
146
144
  stream_name: str,
147
145
  flyer: StandardFlyer[PcompInfo],
148
- detectors: List[StandardDetector],
146
+ detectors: list[StandardDetector],
149
147
  number_of_pulses: int,
150
148
  pulse_width: int,
151
149
  rising_edge_step: int,
@@ -171,7 +169,7 @@ def fly_and_collect_with_static_pcomp(
171
169
  def time_resolved_fly_and_collect_with_static_seq_table(
172
170
  stream_name: str,
173
171
  flyer: StandardFlyer[SeqTableInfo],
174
- detectors: List[StandardDetector],
172
+ detectors: list[StandardDetector],
175
173
  number_of_frames: int,
176
174
  exposure: float,
177
175
  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)