ophyd-async 0.5.0__py3-none-any.whl → 0.5.2__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 (48) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +6 -3
  3. ophyd_async/core/_detector.py +38 -28
  4. ophyd_async/core/_hdf_dataset.py +1 -5
  5. ophyd_async/core/_mock_signal_utils.py +4 -3
  6. ophyd_async/core/_providers.py +30 -39
  7. ophyd_async/core/_signal.py +73 -28
  8. ophyd_async/core/_status.py +17 -1
  9. ophyd_async/epics/adaravis/_aravis.py +1 -1
  10. ophyd_async/epics/adcore/__init__.py +16 -5
  11. ophyd_async/epics/adcore/_core_io.py +29 -5
  12. ophyd_async/epics/adcore/_core_logic.py +7 -4
  13. ophyd_async/epics/adcore/_hdf_writer.py +51 -33
  14. ophyd_async/epics/adcore/_utils.py +69 -70
  15. ophyd_async/epics/adkinetix/_kinetix.py +1 -1
  16. ophyd_async/epics/adkinetix/_kinetix_io.py +4 -1
  17. ophyd_async/epics/adpilatus/_pilatus.py +1 -1
  18. ophyd_async/epics/adpilatus/_pilatus_controller.py +1 -1
  19. ophyd_async/epics/adpilatus/_pilatus_io.py +1 -1
  20. ophyd_async/epics/adsimdetector/_sim.py +1 -1
  21. ophyd_async/epics/advimba/_vimba.py +1 -1
  22. ophyd_async/epics/advimba/_vimba_controller.py +3 -3
  23. ophyd_async/epics/advimba/_vimba_io.py +6 -4
  24. ophyd_async/epics/eiger/__init__.py +5 -0
  25. ophyd_async/epics/eiger/_eiger.py +43 -0
  26. ophyd_async/epics/eiger/_eiger_controller.py +66 -0
  27. ophyd_async/epics/eiger/_eiger_io.py +42 -0
  28. ophyd_async/epics/eiger/_odin_io.py +125 -0
  29. ophyd_async/epics/motor.py +16 -3
  30. ophyd_async/epics/signal/_aioca.py +12 -5
  31. ophyd_async/epics/signal/_common.py +1 -1
  32. ophyd_async/epics/signal/_p4p.py +14 -11
  33. ophyd_async/fastcs/panda/__init__.py +3 -3
  34. ophyd_async/fastcs/panda/{_common_blocks.py → _block.py} +2 -0
  35. ophyd_async/fastcs/panda/{_panda_controller.py → _control.py} +1 -1
  36. ophyd_async/fastcs/panda/_hdf_panda.py +4 -4
  37. ophyd_async/fastcs/panda/_trigger.py +1 -1
  38. ophyd_async/fastcs/panda/{_hdf_writer.py → _writer.py} +29 -22
  39. ophyd_async/plan_stubs/__init__.py +3 -0
  40. ophyd_async/plan_stubs/_nd_attributes.py +63 -0
  41. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +5 -2
  42. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +1 -3
  43. {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/METADATA +46 -44
  44. {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/RECORD +48 -42
  45. {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/WHEEL +1 -1
  46. {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/LICENSE +0 -0
  47. {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/entry_points.txt +0 -0
  48. {ophyd_async-0.5.0.dist-info → ophyd_async-0.5.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,42 @@
1
+ from enum import Enum
2
+
3
+ from ophyd_async.core import Device
4
+ from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw_rbv, epics_signal_w
5
+
6
+
7
+ class EigerTriggerMode(str, Enum):
8
+ internal = "ints"
9
+ edge = "exts"
10
+ gate = "exte"
11
+
12
+
13
+ class EigerDriverIO(Device):
14
+ def __init__(self, prefix: str, name: str = "") -> None:
15
+ self.bit_depth = epics_signal_r(int, f"{prefix}BitDepthReadout")
16
+ self.stale_parameters = epics_signal_r(bool, f"{prefix}StaleParameters")
17
+ self.state = epics_signal_r(str, f"{prefix}DetectorState")
18
+ self.roi_mode = epics_signal_rw_rbv(str, f"{prefix}RoiMode")
19
+
20
+ self.acquire_time = epics_signal_rw_rbv(float, f"{prefix}CountTime")
21
+ self.acquire_period = epics_signal_rw_rbv(float, f"{prefix}FrameTime")
22
+
23
+ self.num_images = epics_signal_rw_rbv(int, f"{prefix}Nimages")
24
+ self.num_triggers = epics_signal_rw_rbv(int, f"{prefix}Ntrigger")
25
+
26
+ # TODO: Should be EigerTriggerMode enum, see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
27
+ self.trigger_mode = epics_signal_rw_rbv(str, f"{prefix}TriggerMode")
28
+
29
+ self.arm = epics_signal_w(int, f"{prefix}Arm")
30
+ self.disarm = epics_signal_w(int, f"{prefix}Disarm")
31
+ self.abort = epics_signal_w(int, f"{prefix}Abort")
32
+
33
+ self.beam_centre_x = epics_signal_rw_rbv(float, f"{prefix}BeamCenterX")
34
+ self.beam_centre_y = epics_signal_rw_rbv(float, f"{prefix}BeamCenterY")
35
+
36
+ self.det_distance = epics_signal_rw_rbv(float, f"{prefix}DetectorDistance")
37
+ self.omega_start = epics_signal_rw_rbv(float, f"{prefix}OmegaStart")
38
+ self.omega_increment = epics_signal_rw_rbv(float, f"{prefix}OmegaIncrement")
39
+
40
+ self.photon_energy = epics_signal_rw_rbv(float, f"{prefix}PhotonEnergy")
41
+
42
+ super().__init__(name)
@@ -0,0 +1,125 @@
1
+ import asyncio
2
+ from enum import Enum
3
+ from typing import AsyncGenerator, AsyncIterator, Dict
4
+
5
+ from bluesky.protocols import StreamAsset
6
+ from event_model.documents.event_descriptor import DataKey
7
+
8
+ from ophyd_async.core import (
9
+ DEFAULT_TIMEOUT,
10
+ DetectorWriter,
11
+ Device,
12
+ DeviceVector,
13
+ NameProvider,
14
+ PathProvider,
15
+ observe_value,
16
+ set_and_wait_for_value,
17
+ )
18
+ from ophyd_async.epics.signal import (
19
+ epics_signal_r,
20
+ epics_signal_rw,
21
+ epics_signal_rw_rbv,
22
+ )
23
+
24
+
25
+ class Writing(str, Enum):
26
+ ON = "ON"
27
+ OFF = "OFF"
28
+
29
+
30
+ class OdinNode(Device):
31
+ def __init__(self, prefix: str, name: str = "") -> None:
32
+ self.writing = epics_signal_r(Writing, f"{prefix}HDF:Writing")
33
+ self.connected = epics_signal_r(bool, f"{prefix}Connected")
34
+
35
+ super().__init__(name)
36
+
37
+
38
+ class Odin(Device):
39
+ def __init__(self, prefix: str, name: str = "") -> None:
40
+ self.nodes = DeviceVector({i: OdinNode(f"{prefix}FP{i}:") for i in range(4)})
41
+
42
+ self.capture = epics_signal_rw(
43
+ Writing, f"{prefix}Writing", f"{prefix}ConfigHdfWrite"
44
+ )
45
+ self.num_captured = epics_signal_r(int, f"{prefix}FramesWritten")
46
+ self.num_to_capture = epics_signal_rw_rbv(int, f"{prefix}ConfigHdfFrames")
47
+
48
+ self.start_timeout = epics_signal_rw_rbv(int, f"{prefix}TimeoutTimerPeriod")
49
+
50
+ self.image_height = epics_signal_rw_rbv(int, f"{prefix}DatasetDataDims0")
51
+ self.image_width = epics_signal_rw_rbv(int, f"{prefix}DatasetDataDims1")
52
+
53
+ self.num_row_chunks = epics_signal_rw_rbv(int, f"{prefix}DatasetDataChunks1")
54
+ self.num_col_chunks = epics_signal_rw_rbv(int, f"{prefix}DatasetDataChunks2")
55
+
56
+ self.file_path = epics_signal_rw_rbv(str, f"{prefix}ConfigHdfFilePath")
57
+ self.file_name = epics_signal_rw_rbv(str, f"{prefix}ConfigHdfFilePrefix")
58
+
59
+ self.acquisition_id = epics_signal_rw_rbv(
60
+ str, f"{prefix}ConfigHdfAcquisitionId"
61
+ )
62
+
63
+ self.data_type = epics_signal_rw_rbv(str, f"{prefix}DatasetDataDatatype")
64
+
65
+ super().__init__(name)
66
+
67
+
68
+ class OdinWriter(DetectorWriter):
69
+ def __init__(
70
+ self,
71
+ path_provider: PathProvider,
72
+ name_provider: NameProvider,
73
+ odin_driver: Odin,
74
+ ) -> None:
75
+ self._drv = odin_driver
76
+ self._path_provider = path_provider
77
+ self._name_provider = name_provider
78
+ super().__init__()
79
+
80
+ async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
81
+ info = self._path_provider(device_name=self._name_provider())
82
+
83
+ await asyncio.gather(
84
+ self._drv.file_path.set(str(info.directory_path)),
85
+ self._drv.file_name.set(info.filename),
86
+ self._drv.data_type.set(
87
+ "uint16"
88
+ ), # TODO: Get from eiger https://github.com/bluesky/ophyd-async/issues/529
89
+ self._drv.num_to_capture.set(0),
90
+ )
91
+
92
+ await self._drv.capture.set(Writing.ON)
93
+
94
+ return await self._describe()
95
+
96
+ async def _describe(self) -> Dict[str, DataKey]:
97
+ data_shape = await asyncio.gather(
98
+ self._drv.image_height.get_value(), self._drv.image_width.get_value()
99
+ )
100
+
101
+ return {
102
+ "data": DataKey(
103
+ source=self._drv.file_name.source,
104
+ shape=data_shape,
105
+ dtype="array",
106
+ dtype_numpy="<u2", # TODO: Use correct type based on eiger https://github.com/bluesky/ophyd-async/issues/529
107
+ external="STREAM:",
108
+ )
109
+ }
110
+
111
+ async def observe_indices_written(
112
+ self, timeout=DEFAULT_TIMEOUT
113
+ ) -> AsyncGenerator[int, None]:
114
+ async for num_captured in observe_value(self._drv.num_captured, timeout):
115
+ yield num_captured
116
+
117
+ async def get_indices_written(self) -> int:
118
+ return await self._drv.num_captured.get_value()
119
+
120
+ def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
121
+ # TODO: Correctly return stream https://github.com/bluesky/ophyd-async/issues/530
122
+ raise NotImplementedError()
123
+
124
+ async def close(self) -> None:
125
+ await set_and_wait_for_value(self._drv.capture, Writing.OFF)
@@ -1,7 +1,13 @@
1
1
  import asyncio
2
2
  from typing import Optional
3
3
 
4
- from bluesky.protocols import Flyable, Movable, Preparable, Stoppable
4
+ from bluesky.protocols import (
5
+ Flyable,
6
+ Locatable,
7
+ Location,
8
+ Preparable,
9
+ Stoppable,
10
+ )
5
11
  from pydantic import BaseModel, Field
6
12
 
7
13
  from ophyd_async.core import (
@@ -51,7 +57,7 @@ class FlyMotorInfo(BaseModel):
51
57
  timeout: CalculatableTimeout = Field(frozen=True, default=CalculateTimeout)
52
58
 
53
59
 
54
- class Motor(StandardReadable, Movable, Stoppable, Flyable, Preparable):
60
+ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
55
61
  """Device that moves a motor record"""
56
62
 
57
63
  def __init__(self, prefix: str, name="") -> None:
@@ -113,7 +119,7 @@ class Motor(StandardReadable, Movable, Stoppable, Flyable, Preparable):
113
119
  )
114
120
 
115
121
  await self.set(fly_prepared_position)
116
- await self.velocity.set(fly_velocity)
122
+ await self.velocity.set(abs(fly_velocity))
117
123
 
118
124
  @AsyncStatus.wrap
119
125
  async def kickoff(self):
@@ -193,6 +199,13 @@ class Motor(StandardReadable, Movable, Stoppable, Flyable, Preparable):
193
199
  await self.velocity.set(abs(max_speed))
194
200
  return fly_velocity
195
201
 
202
+ async def locate(self) -> Location[float]:
203
+ location: Location = {
204
+ "setpoint": await self.user_setpoint.get_value(),
205
+ "readback": await self.user_readback.get_value(),
206
+ }
207
+ return location
208
+
196
209
  async def _prepare_motor_path(
197
210
  self, fly_velocity: float, start_position: float, end_position: float
198
211
  ) -> float:
@@ -47,7 +47,7 @@ def _data_key_from_augmented_value(
47
47
  value: AugmentedValue,
48
48
  *,
49
49
  choices: Optional[List[str]] = None,
50
- dtype: Optional[str] = None,
50
+ dtype: Optional[Dtype] = None,
51
51
  ) -> DataKey:
52
52
  """Use the return value of get with FORMAT_CTRL to construct a DataKey
53
53
  describing the signal. See docstring of AugmentedValue for expected
@@ -175,7 +175,7 @@ class CaBoolConverter(CaConverter):
175
175
  return bool(value)
176
176
 
177
177
  def get_datakey(self, value: AugmentedValue) -> DataKey:
178
- return _data_key_from_augmented_value(value, dtype="bool")
178
+ return _data_key_from_augmented_value(value, dtype="boolean")
179
179
 
180
180
 
181
181
  class DisconnectedCaConverter(CaConverter):
@@ -229,10 +229,17 @@ def make_converter(
229
229
  value = list(values.values())[0]
230
230
  # Done the dbr check, so enough to check one of the values
231
231
  if datatype and not isinstance(value, datatype):
232
- raise TypeError(
233
- f"{pv} has type {type(value).__name__.replace('ca_', '')} "
234
- + f"not {datatype.__name__}"
232
+ # Allow int signals to represent float records when prec is 0
233
+ is_prec_zero_float = (
234
+ isinstance(value, float)
235
+ and get_unique({k: v.precision for k, v in values.items()}, "precision")
236
+ == 0
235
237
  )
238
+ if not (datatype is int and is_prec_zero_float):
239
+ raise TypeError(
240
+ f"{pv} has type {type(value).__name__.replace('ca_', '')} "
241
+ + f"not {datatype.__name__}"
242
+ )
236
243
  return CaConverter(pv_dbr, None)
237
244
 
238
245
 
@@ -55,7 +55,7 @@ def get_supported_values(
55
55
  f"which do not match {datatype}, which has {choices}."
56
56
  )
57
57
  return {x: datatype(x) if x else "_" for x in pv_choices}
58
- elif datatype is None:
58
+ elif datatype is None or datatype is str:
59
59
  return {x: x or "_" for x in pv_choices}
60
60
 
61
61
  raise TypeError(
@@ -64,7 +64,7 @@ def _data_key_from_value(
64
64
  *,
65
65
  shape: Optional[list[int]] = None,
66
66
  choices: Optional[list[str]] = None,
67
- dtype: Optional[str] = None,
67
+ dtype: Optional[Dtype] = None,
68
68
  ) -> DataKey:
69
69
  """
70
70
  Args:
@@ -85,7 +85,7 @@ def _data_key_from_value(
85
85
  if isinstance(type_code, tuple):
86
86
  dtype_numpy = ""
87
87
  if type_code[1] == "enum_t":
88
- if dtype == "bool":
88
+ if dtype == "boolean":
89
89
  dtype_numpy = "<i2"
90
90
  else:
91
91
  for item in type_code[2]:
@@ -241,7 +241,7 @@ class PvaEmumBoolConverter(PvaConverter):
241
241
  return bool(value["value"]["index"])
242
242
 
243
243
  def get_datakey(self, source: str, value) -> DataKey:
244
- return _data_key_from_value(source, value, dtype="bool")
244
+ return _data_key_from_value(source, value, dtype="boolean")
245
245
 
246
246
 
247
247
  class PvaTableConverter(PvaConverter):
@@ -335,14 +335,17 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
335
335
  return PvaEnumConverter(
336
336
  get_supported_values(pv, datatype, datatype.choices)
337
337
  )
338
- elif (
339
- datatype
340
- and not issubclass(typ, datatype)
341
- and not (
342
- typ is float and datatype is int
343
- ) # Allow float -> int since prec can be 0
344
- ):
345
- raise TypeError(f"{pv} has type {typ.__name__} not {datatype.__name__}")
338
+ elif datatype and not issubclass(typ, datatype):
339
+ # Allow int signals to represent float records when prec is 0
340
+ is_prec_zero_float = typ is float and (
341
+ get_unique(
342
+ {k: v["display"]["precision"] for k, v in values.items()},
343
+ "precision",
344
+ )
345
+ == 0
346
+ )
347
+ if not (datatype is int and is_prec_zero_float):
348
+ raise TypeError(f"{pv} has type {typ.__name__} not {datatype.__name__}")
346
349
  return PvaConverter()
347
350
  elif "NTTable" in typeid:
348
351
  return PvaTableConverter()
@@ -1,4 +1,4 @@
1
- from ._common_blocks import (
1
+ from ._block import (
2
2
  CommonPandaBlocks,
3
3
  DataBlock,
4
4
  EnableDisableOptions,
@@ -9,9 +9,8 @@ from ._common_blocks import (
9
9
  SeqBlock,
10
10
  TimeUnits,
11
11
  )
12
+ from ._control import PandaPcapController
12
13
  from ._hdf_panda import HDFPanda
13
- from ._hdf_writer import PandaHDFWriter
14
- from ._panda_controller import PandaPcapController
15
14
  from ._table import (
16
15
  DatasetTable,
17
16
  PandaHdf5DatasetType,
@@ -28,6 +27,7 @@ from ._trigger import (
28
27
  StaticSeqTableTriggerLogic,
29
28
  )
30
29
  from ._utils import phase_sorter
30
+ from ._writer import PandaHDFWriter
31
31
 
32
32
  __all__ = [
33
33
  "CommonPandaBlocks",
@@ -13,6 +13,8 @@ class DataBlock(Device):
13
13
  hdf_file_name: SignalRW[str]
14
14
  num_capture: SignalRW[int]
15
15
  num_captured: SignalR[int]
16
+ create_directory: SignalRW[int]
17
+ directory_exists: SignalR[bool]
16
18
  capture: SignalRW[bool]
17
19
  flush_period: SignalRW[float]
18
20
  datasets: SignalR[DatasetTable]
@@ -8,7 +8,7 @@ from ophyd_async.core import (
8
8
  wait_for_value,
9
9
  )
10
10
 
11
- from ._common_blocks import PcapBlock
11
+ from ._block import PcapBlock
12
12
 
13
13
 
14
14
  class PandaPcapController(DetectorControl):
@@ -5,9 +5,9 @@ from typing import Sequence
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
7
7
 
8
- from ._common_blocks import CommonPandaBlocks
9
- from ._hdf_writer import PandaHDFWriter
10
- from ._panda_controller import PandaPcapController
8
+ from ._block import CommonPandaBlocks
9
+ from ._control import PandaPcapController
10
+ from ._writer import PandaHDFWriter
11
11
 
12
12
 
13
13
  class HDFPanda(CommonPandaBlocks, StandardDetector):
@@ -26,7 +26,7 @@ class HDFPanda(CommonPandaBlocks, StandardDetector):
26
26
  prefix=prefix,
27
27
  path_provider=path_provider,
28
28
  name_provider=lambda: name,
29
- panda_device=self,
29
+ panda_data_block=self.data,
30
30
  )
31
31
  super().__init__(
32
32
  controller=controller,
@@ -5,7 +5,7 @@ from pydantic import BaseModel, Field
5
5
 
6
6
  from ophyd_async.core import TriggerLogic, wait_for_value
7
7
 
8
- from ._common_blocks import PcompBlock, PcompDirectionOptions, SeqBlock, TimeUnits
8
+ from ._block import PcompBlock, PcompDirectionOptions, SeqBlock, TimeUnits
9
9
  from ._table import SeqTable
10
10
 
11
11
 
@@ -16,7 +16,7 @@ from ophyd_async.core import (
16
16
  wait_for_value,
17
17
  )
18
18
 
19
- from ._common_blocks import CommonPandaBlocks
19
+ from ._block import DataBlock
20
20
 
21
21
 
22
22
  class PandaHDFWriter(DetectorWriter):
@@ -27,9 +27,9 @@ class PandaHDFWriter(DetectorWriter):
27
27
  prefix: str,
28
28
  path_provider: PathProvider,
29
29
  name_provider: NameProvider,
30
- panda_device: CommonPandaBlocks,
30
+ panda_data_block: DataBlock,
31
31
  ) -> None:
32
- self.panda_device = panda_device
32
+ self.panda_data_block = panda_data_block
33
33
  self._prefix = prefix
34
34
  self._path_provider = path_provider
35
35
  self._name_provider = name_provider
@@ -42,25 +42,33 @@ class PandaHDFWriter(DetectorWriter):
42
42
  """Retrieve and get descriptor of all PandA signals marked for capture"""
43
43
 
44
44
  # Ensure flushes are immediate
45
- await self.panda_device.data.flush_period.set(0)
45
+ await self.panda_data_block.flush_period.set(0)
46
46
 
47
47
  self._file = None
48
- info = self._path_provider(device_name=self.panda_device.name)
48
+ info = self._path_provider(device_name=self._name_provider())
49
+
50
+ # Set create dir depth first to guarantee that callback when setting
51
+ # directory path has correct value
52
+ await self.panda_data_block.create_directory.set(info.create_dir_depth)
53
+
49
54
  # Set the initial values
50
55
  await asyncio.gather(
51
- self.panda_device.data.hdf_directory.set(
52
- str(info.root / info.resource_dir)
53
- ),
54
- self.panda_device.data.hdf_file_name.set(
56
+ self.panda_data_block.hdf_directory.set(str(info.directory_path)),
57
+ self.panda_data_block.hdf_file_name.set(
55
58
  f"{info.filename}.h5",
56
59
  ),
57
- self.panda_device.data.num_capture.set(0),
58
- # TODO: Set create_dir_depth once available
59
- # https://github.com/bluesky/ophyd-async/issues/317
60
+ self.panda_data_block.num_capture.set(0),
60
61
  )
61
62
 
63
+ # Make sure that directory exists or has been created.
64
+ if not await self.panda_data_block.directory_exists.get_value() == 1:
65
+ raise OSError(
66
+ f"Directory {info.directory_path} does not exist or "
67
+ "is not writable by the PandABlocks-ioc!"
68
+ )
69
+
62
70
  # Wait for it to start, stashing the status that tells us when it finishes
63
- await self.panda_device.data.capture.set(True)
71
+ await self.panda_data_block.capture.set(True)
64
72
  if multiplier > 1:
65
73
  raise ValueError(
66
74
  "All PandA datasets should be scalar, multiplier should be 1"
@@ -76,7 +84,7 @@ class PandaHDFWriter(DetectorWriter):
76
84
  await self._update_datasets()
77
85
  describe = {
78
86
  ds.data_key: DataKey(
79
- source=self.panda_device.data.hdf_directory.source,
87
+ source=self.panda_data_block.hdf_directory.source,
80
88
  shape=ds.shape,
81
89
  dtype="array" if ds.shape != [1] else "number",
82
90
  dtype_numpy="<f8", # PandA data should always be written as Float64
@@ -92,7 +100,7 @@ class PandaHDFWriter(DetectorWriter):
92
100
  representation of datasets that the panda will write.
93
101
  """
94
102
 
95
- capture_table = await self.panda_device.data.datasets.get_value()
103
+ capture_table = await self.panda_data_block.datasets.get_value()
96
104
  self._datasets = [
97
105
  HDFDataset(dataset_name, "/" + dataset_name, [1], multiplier=1)
98
106
  for dataset_name in capture_table["name"]
@@ -108,18 +116,18 @@ class PandaHDFWriter(DetectorWriter):
108
116
 
109
117
  matcher.__name__ = f"index_at_least_{index}"
110
118
  await wait_for_value(
111
- self.panda_device.data.num_captured, matcher, timeout=timeout
119
+ self.panda_data_block.num_captured, matcher, timeout=timeout
112
120
  )
113
121
 
114
122
  async def get_indices_written(self) -> int:
115
- return await self.panda_device.data.num_captured.get_value()
123
+ return await self.panda_data_block.num_captured.get_value()
116
124
 
117
125
  async def observe_indices_written(
118
126
  self, timeout=DEFAULT_TIMEOUT
119
127
  ) -> AsyncGenerator[int, None]:
120
128
  """Wait until a specific index is ready to be collected"""
121
129
  async for num_captured in observe_value(
122
- self.panda_device.data.num_captured, timeout
130
+ self.panda_data_block.num_captured, timeout
123
131
  ):
124
132
  yield num_captured // self._multiplier
125
133
 
@@ -130,9 +138,8 @@ class PandaHDFWriter(DetectorWriter):
130
138
  if indices_written:
131
139
  if not self._file:
132
140
  self._file = HDFFile(
133
- self._path_provider(),
134
- Path(await self.panda_device.data.hdf_directory.get_value())
135
- / Path(await self.panda_device.data.hdf_file_name.get_value()),
141
+ Path(await self.panda_data_block.hdf_directory.get_value())
142
+ / Path(await self.panda_data_block.hdf_file_name.get_value()),
136
143
  self._datasets,
137
144
  )
138
145
  for doc in self._file.stream_resources():
@@ -142,6 +149,6 @@ class PandaHDFWriter(DetectorWriter):
142
149
 
143
150
  # Could put this function as default for StandardDetector
144
151
  async def close(self):
145
- await self.panda_device.data.capture.set(
152
+ await self.panda_data_block.capture.set(
146
153
  False, wait=True, timeout=DEFAULT_TIMEOUT
147
154
  )
@@ -4,10 +4,13 @@ from ._fly import (
4
4
  prepare_static_seq_table_flyer_and_detectors_with_same_trigger,
5
5
  time_resolved_fly_and_collect_with_static_seq_table,
6
6
  )
7
+ from ._nd_attributes import setup_ndattributes, setup_ndstats_sum
7
8
 
8
9
  __all__ = [
9
10
  "fly_and_collect",
10
11
  "prepare_static_seq_table_flyer_and_detectors_with_same_trigger",
11
12
  "time_resolved_fly_and_collect_with_static_seq_table",
12
13
  "ensure_connected",
14
+ "setup_ndattributes",
15
+ "setup_ndstats_sum",
13
16
  ]
@@ -0,0 +1,63 @@
1
+ from typing import Sequence
2
+ from xml.etree import cElementTree as ET
3
+
4
+ import bluesky.plan_stubs as bps
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 (
9
+ NDAttributeDataType,
10
+ NDAttributeParam,
11
+ NDAttributePv,
12
+ )
13
+
14
+
15
+ def setup_ndattributes(
16
+ device: NDArrayBaseIO, ndattributes: Sequence[NDAttributePv | NDAttributeParam]
17
+ ):
18
+ xml_text = ET.Element("Attributes")
19
+
20
+ for ndattribute in ndattributes:
21
+ if isinstance(ndattribute, NDAttributeParam):
22
+ ET.SubElement(
23
+ xml_text,
24
+ "Attribute",
25
+ name=ndattribute.name,
26
+ type="PARAM",
27
+ source=ndattribute.param,
28
+ addr=str(ndattribute.addr),
29
+ datatype=ndattribute.datatype.value,
30
+ description=ndattribute.description,
31
+ )
32
+ elif isinstance(ndattribute, NDAttributePv):
33
+ ET.SubElement(
34
+ xml_text,
35
+ "Attribute",
36
+ name=ndattribute.name,
37
+ type="EPICS_PV",
38
+ source=ndattribute.signal.source.split("ca://")[-1],
39
+ dbrtype=ndattribute.dbrtype.value,
40
+ description=ndattribute.description,
41
+ )
42
+ else:
43
+ raise ValueError(
44
+ f"Invalid type for ndattributes: {type(ndattribute)}. "
45
+ "Expected NDAttributePv or NDAttributeParam."
46
+ )
47
+ yield from bps.mv(device.nd_attributes_file, xml_text)
48
+
49
+
50
+ def setup_ndstats_sum(detector: Device):
51
+ yield from (
52
+ setup_ndattributes(
53
+ detector.hdf,
54
+ [
55
+ NDAttributeParam(
56
+ name=f"{detector.name}-sum",
57
+ param="NDPluginStatsTotal",
58
+ datatype=NDAttributeDataType.DOUBLE,
59
+ description="Sum of the array",
60
+ )
61
+ ],
62
+ )
63
+ )
@@ -14,6 +14,8 @@ 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
17
19
  self.pattern_generator.set_exposure(exposure)
18
20
  self.path_provider: PathProvider = path_provider
19
21
  self.task: Optional[asyncio.Task] = None
@@ -25,7 +27,8 @@ class PatternDetectorController(DetectorControl):
25
27
  trigger: DetectorTrigger = DetectorTrigger.internal,
26
28
  exposure: Optional[float] = 0.01,
27
29
  ) -> AsyncStatus:
28
- assert exposure is not None
30
+ if exposure is None:
31
+ exposure = 0.1
29
32
  period: float = exposure + self.get_deadtime(exposure)
30
33
  task = asyncio.create_task(
31
34
  self._coroutine_for_image_writing(exposure, period, num)
@@ -42,7 +45,7 @@ class PatternDetectorController(DetectorControl):
42
45
  pass
43
46
  self.task = None
44
47
 
45
- def get_deadtime(self, exposure: float) -> float:
48
+ def get_deadtime(self, exposure: float | None) -> float:
46
49
  return 0.001
47
50
 
48
51
  async def _coroutine_for_image_writing(
@@ -166,8 +166,7 @@ class PatternGenerator:
166
166
 
167
167
  def _get_new_path(self, path_provider: PathProvider) -> Path:
168
168
  info = path_provider(device_name="pattern")
169
- filename = info.filename
170
- new_path: Path = info.root / info.resource_dir / filename
169
+ new_path: Path = info.directory_path / info.filename
171
170
  return new_path
172
171
 
173
172
  async def collect_stream_docs(
@@ -188,7 +187,6 @@ class PatternGenerator:
188
187
  if not self._hdf_stream_provider:
189
188
  assert self.target_path, "open file has not been called"
190
189
  self._hdf_stream_provider = HDFFile(
191
- self._path_provider(),
192
190
  self.target_path,
193
191
  self._datasets,
194
192
  )