ophyd-async 0.3.4__py3-none-any.whl → 0.4.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 (44) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +20 -8
  3. ophyd_async/core/_providers.py +186 -24
  4. ophyd_async/core/detector.py +14 -15
  5. ophyd_async/core/device.py +18 -6
  6. ophyd_async/core/signal.py +32 -8
  7. ophyd_async/core/soft_signal_backend.py +20 -2
  8. ophyd_async/epics/_backend/_aioca.py +3 -0
  9. ophyd_async/epics/_backend/_p4p.py +50 -2
  10. ophyd_async/epics/_backend/common.py +3 -1
  11. ophyd_async/epics/areadetector/aravis.py +3 -3
  12. ophyd_async/epics/areadetector/controllers/aravis_controller.py +1 -0
  13. ophyd_async/epics/areadetector/drivers/ad_base.py +3 -2
  14. ophyd_async/epics/areadetector/kinetix.py +3 -3
  15. ophyd_async/epics/areadetector/pilatus.py +3 -3
  16. ophyd_async/epics/areadetector/vimba.py +3 -3
  17. ophyd_async/epics/areadetector/writers/__init__.py +2 -2
  18. ophyd_async/epics/areadetector/writers/general_hdffile.py +97 -0
  19. ophyd_async/epics/areadetector/writers/hdf_writer.py +27 -10
  20. ophyd_async/epics/areadetector/writers/nd_file_hdf.py +3 -0
  21. ophyd_async/epics/areadetector/writers/nd_plugin.py +30 -0
  22. ophyd_async/epics/demo/demo_ad_sim_detector.py +3 -3
  23. ophyd_async/epics/motion/motor.py +132 -2
  24. ophyd_async/panda/__init__.py +15 -1
  25. ophyd_async/panda/_common_blocks.py +22 -1
  26. ophyd_async/panda/_hdf_panda.py +5 -3
  27. ophyd_async/panda/_table.py +20 -18
  28. ophyd_async/panda/_trigger.py +62 -7
  29. ophyd_async/panda/writers/_hdf_writer.py +17 -8
  30. ophyd_async/plan_stubs/ensure_connected.py +7 -2
  31. ophyd_async/plan_stubs/fly.py +58 -7
  32. ophyd_async/sim/pattern_generator.py +71 -182
  33. ophyd_async/sim/sim_pattern_detector_control.py +3 -3
  34. ophyd_async/sim/sim_pattern_detector_writer.py +9 -5
  35. ophyd_async/sim/sim_pattern_generator.py +12 -5
  36. {ophyd_async-0.3.4.dist-info → ophyd_async-0.4.0.dist-info}/METADATA +7 -2
  37. {ophyd_async-0.3.4.dist-info → ophyd_async-0.4.0.dist-info}/RECORD +41 -43
  38. {ophyd_async-0.3.4.dist-info → ophyd_async-0.4.0.dist-info}/WHEEL +1 -1
  39. ophyd_async/epics/areadetector/writers/_hdfdataset.py +0 -10
  40. ophyd_async/epics/areadetector/writers/_hdffile.py +0 -54
  41. ophyd_async/panda/writers/_panda_hdf_file.py +0 -54
  42. {ophyd_async-0.3.4.dist-info → ophyd_async-0.4.0.dist-info}/LICENSE +0 -0
  43. {ophyd_async-0.3.4.dist-info → ophyd_async-0.4.0.dist-info}/entry_points.txt +0 -0
  44. {ophyd_async-0.3.4.dist-info → ophyd_async-0.4.0.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from enum import Enum
4
4
 
5
5
  from ophyd_async.core import Device, DeviceVector, SignalR, SignalRW
6
+ from ophyd_async.core.signal_backend import SubsetEnum
6
7
  from ophyd_async.panda._table import DatasetTable, SeqTable
7
8
 
8
9
 
@@ -22,6 +23,25 @@ class PulseBlock(Device):
22
23
  width: SignalRW[float]
23
24
 
24
25
 
26
+ class PcompDirectionOptions(str, Enum):
27
+ positive = "Positive"
28
+ negative = "Negative"
29
+ either = "Either"
30
+
31
+
32
+ EnableDisableOptions = SubsetEnum["ZERO", "ONE"]
33
+
34
+
35
+ class PcompBlock(Device):
36
+ active: SignalR[bool]
37
+ dir: SignalRW[PcompDirectionOptions]
38
+ enable: SignalRW[EnableDisableOptions]
39
+ pulses: SignalRW[int]
40
+ start: SignalRW[int]
41
+ step: SignalRW[int]
42
+ width: SignalRW[int]
43
+
44
+
25
45
  class TimeUnits(str, Enum):
26
46
  min = "min"
27
47
  s = "s"
@@ -35,7 +55,7 @@ class SeqBlock(Device):
35
55
  repeats: SignalRW[int]
36
56
  prescale: SignalRW[float]
37
57
  prescale_units: SignalRW[TimeUnits]
38
- enable: SignalRW[str]
58
+ enable: SignalRW[EnableDisableOptions]
39
59
 
40
60
 
41
61
  class PcapBlock(Device):
@@ -46,5 +66,6 @@ class PcapBlock(Device):
46
66
  class CommonPandaBlocks(Device):
47
67
  pulse: DeviceVector[PulseBlock]
48
68
  seq: DeviceVector[SeqBlock]
69
+ pcomp: DeviceVector[PcompBlock]
49
70
  pcap: PcapBlock
50
71
  data: DataBlock
@@ -4,7 +4,7 @@ from typing import Sequence
4
4
 
5
5
  from ophyd_async.core import (
6
6
  DEFAULT_TIMEOUT,
7
- DirectoryProvider,
7
+ PathProvider,
8
8
  SignalR,
9
9
  StandardDetector,
10
10
  )
@@ -19,7 +19,7 @@ class HDFPanda(CommonPandaBlocks, StandardDetector):
19
19
  def __init__(
20
20
  self,
21
21
  prefix: str,
22
- directory_provider: DirectoryProvider,
22
+ path_provider: PathProvider,
23
23
  config_sigs: Sequence[SignalR] = (),
24
24
  name: str = "",
25
25
  ):
@@ -28,7 +28,9 @@ class HDFPanda(CommonPandaBlocks, StandardDetector):
28
28
  create_children_from_annotations(self)
29
29
  controller = PandaPcapController(pcap=self.pcap)
30
30
  writer = PandaHDFWriter(
31
- directory_provider=directory_provider,
31
+ prefix=prefix,
32
+ path_provider=path_provider,
33
+ name_provider=lambda: name,
32
34
  panda_device=self,
33
35
  )
34
36
  super().__init__(
@@ -1,9 +1,11 @@
1
1
  from dataclasses import dataclass
2
2
  from enum import Enum
3
- from typing import Optional, Sequence, Type, TypedDict, TypeVar
3
+ from typing import Optional, Sequence, Type, TypeVar
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
9
 
8
10
 
9
11
  class PandaHdf5DatasetType(str, Enum):
@@ -54,23 +56,23 @@ class SeqTableRow:
54
56
 
55
57
 
56
58
  class SeqTable(TypedDict):
57
- repeats: npt.NDArray[np.uint16]
58
- trigger: Sequence[SeqTrigger]
59
- position: npt.NDArray[np.int32]
60
- time1: npt.NDArray[np.uint32]
61
- outa1: npt.NDArray[np.bool_]
62
- outb1: npt.NDArray[np.bool_]
63
- outc1: npt.NDArray[np.bool_]
64
- outd1: npt.NDArray[np.bool_]
65
- oute1: npt.NDArray[np.bool_]
66
- outf1: npt.NDArray[np.bool_]
67
- time2: npt.NDArray[np.uint32]
68
- outa2: npt.NDArray[np.bool_]
69
- outb2: npt.NDArray[np.bool_]
70
- outc2: npt.NDArray[np.bool_]
71
- outd2: npt.NDArray[np.bool_]
72
- oute2: npt.NDArray[np.bool_]
73
- outf2: npt.NDArray[np.bool_]
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]
74
76
 
75
77
 
76
78
  def seq_table_from_rows(*rows: SeqTableRow):
@@ -1,15 +1,22 @@
1
1
  import asyncio
2
- from dataclasses import dataclass
2
+ from typing import Optional
3
+
4
+ from pydantic import BaseModel, Field
3
5
 
4
6
  from ophyd_async.core import TriggerLogic, wait_for_value
5
- from ophyd_async.panda import SeqBlock, SeqTable, TimeUnits
7
+ from ophyd_async.panda import (
8
+ PcompBlock,
9
+ PcompDirectionOptions,
10
+ SeqBlock,
11
+ SeqTable,
12
+ TimeUnits,
13
+ )
6
14
 
7
15
 
8
- @dataclass
9
- class SeqTableInfo:
10
- sequence_table: SeqTable
11
- repeats: int
12
- prescale_as_us: float = 1 # microseconds
16
+ class SeqTableInfo(BaseModel):
17
+ sequence_table: SeqTable = Field(strict=True)
18
+ repeats: int = Field(ge=0)
19
+ prescale_as_us: float = Field(default=1, ge=0) # microseconds
13
20
 
14
21
 
15
22
  class StaticSeqTableTriggerLogic(TriggerLogic[SeqTableInfo]):
@@ -37,3 +44,51 @@ class StaticSeqTableTriggerLogic(TriggerLogic[SeqTableInfo]):
37
44
  async def stop(self):
38
45
  await self.seq.enable.set("ZERO")
39
46
  await wait_for_value(self.seq.active, False, timeout=1)
47
+
48
+
49
+ class PcompInfo(BaseModel):
50
+ start_postion: int = Field(description="start position in counts")
51
+ pulse_width: int = Field(description="width of a single pulse in counts", gt=0)
52
+ rising_edge_step: int = Field(
53
+ description="step between rising edges of pulses in counts", gt=0
54
+ ) #
55
+ number_of_pulses: int = Field(
56
+ description=(
57
+ "Number of pulses to send before the PCOMP block is disarmed. "
58
+ "0 means infinite."
59
+ ),
60
+ ge=0,
61
+ )
62
+ direction: PcompDirectionOptions = Field(
63
+ description=(
64
+ "Specifies which direction the motor counts should be "
65
+ "moving. Pulses won't be sent unless the values are moving in "
66
+ "this direction."
67
+ )
68
+ )
69
+
70
+
71
+ class StaticPcompTriggerLogic(TriggerLogic[PcompInfo]):
72
+ def __init__(self, pcomp: PcompBlock) -> None:
73
+ self.pcomp = pcomp
74
+
75
+ async def prepare(self, value: PcompInfo):
76
+ await self.pcomp.enable.set("ZERO")
77
+ await asyncio.gather(
78
+ self.pcomp.start.set(value.start_postion),
79
+ self.pcomp.width.set(value.pulse_width),
80
+ self.pcomp.step.set(value.rising_edge_step),
81
+ self.pcomp.pulses.set(value.number_of_pulses),
82
+ self.pcomp.dir.set(value.direction),
83
+ )
84
+
85
+ async def kickoff(self) -> None:
86
+ await self.pcomp.enable.set("ONE")
87
+ await wait_for_value(self.pcomp.active, True, timeout=1)
88
+
89
+ async def complete(self, timeout: Optional[float] = None) -> None:
90
+ await wait_for_value(self.pcomp.active, False, timeout=timeout)
91
+
92
+ async def stop(self):
93
+ await self.pcomp.enable.set("ZERO")
94
+ await wait_for_value(self.pcomp.active, False, timeout=1)
@@ -8,13 +8,14 @@ from p4p.client.thread import Context
8
8
  from ophyd_async.core import (
9
9
  DEFAULT_TIMEOUT,
10
10
  DetectorWriter,
11
- DirectoryProvider,
11
+ NameProvider,
12
+ PathProvider,
12
13
  wait_for_value,
13
14
  )
14
15
  from ophyd_async.core.signal import observe_value
16
+ from ophyd_async.epics.areadetector.writers.general_hdffile import _HDFDataset, _HDFFile
15
17
 
16
18
  from .._common_blocks import CommonPandaBlocks
17
- from ._panda_hdf_file import _HDFDataset, _HDFFile
18
19
 
19
20
 
20
21
  class PandaHDFWriter(DetectorWriter):
@@ -22,11 +23,15 @@ class PandaHDFWriter(DetectorWriter):
22
23
 
23
24
  def __init__(
24
25
  self,
25
- directory_provider: DirectoryProvider,
26
+ prefix: str,
27
+ path_provider: PathProvider,
28
+ name_provider: NameProvider,
26
29
  panda_device: CommonPandaBlocks,
27
30
  ) -> None:
28
31
  self.panda_device = panda_device
29
- self._directory_provider = directory_provider
32
+ self._prefix = prefix
33
+ self._path_provider = path_provider
34
+ self._name_provider = name_provider
30
35
  self._datasets: List[_HDFDataset] = []
31
36
  self._file: Optional[_HDFFile] = None
32
37
  self._multiplier = 1
@@ -39,16 +44,18 @@ class PandaHDFWriter(DetectorWriter):
39
44
  await self.panda_device.data.flush_period.set(0)
40
45
 
41
46
  self._file = None
42
- info = self._directory_provider()
47
+ info = self._path_provider(device_name=self.panda_device.name)
43
48
  # Set the initial values
44
49
  await asyncio.gather(
45
50
  self.panda_device.data.hdf_directory.set(
46
51
  str(info.root / info.resource_dir)
47
52
  ),
48
53
  self.panda_device.data.hdf_file_name.set(
49
- f"{info.prefix}{self.panda_device.name}{info.suffix}.h5",
54
+ f"{info.filename}.h5",
50
55
  ),
51
56
  self.panda_device.data.num_capture.set(0),
57
+ # TODO: Set create_dir_depth once available
58
+ # https://github.com/bluesky/ophyd-async/issues/317
52
59
  )
53
60
 
54
61
  # Wait for it to start, stashing the status that tells us when it finishes
@@ -71,6 +78,7 @@ class PandaHDFWriter(DetectorWriter):
71
78
  source=self.panda_device.data.hdf_directory.source,
72
79
  shape=ds.shape,
73
80
  dtype="array" if ds.shape != [1] else "number",
81
+ dtype_numpy="<f8", # PandA data should always be written as Float64
74
82
  external="STREAM:",
75
83
  )
76
84
  for ds in self._datasets
@@ -121,8 +129,9 @@ class PandaHDFWriter(DetectorWriter):
121
129
  if indices_written:
122
130
  if not self._file:
123
131
  self._file = _HDFFile(
124
- self._directory_provider(),
125
- Path(await self.panda_device.data.hdf_file_name.get_value()),
132
+ self._path_provider(),
133
+ Path(await self.panda_device.data.hdf_directory.get_value())
134
+ / Path(await self.panda_device.data.hdf_file_name.get_value()),
126
135
  self._datasets,
127
136
  )
128
137
  for doc in self._file.stream_resources():
@@ -10,13 +10,18 @@ def ensure_connected(
10
10
  timeout: float = DEFAULT_TIMEOUT,
11
11
  force_reconnect=False,
12
12
  ):
13
- yield from bps.wait_for(
13
+ (connect_task,) = yield from bps.wait_for(
14
14
  [
15
15
  lambda: wait_for_connection(
16
16
  **{
17
- device.name: device.connect(mock, timeout, force_reconnect)
17
+ device.name: device.connect(
18
+ mock=mock, timeout=timeout, force_reconnect=force_reconnect
19
+ )
18
20
  for device in devices
19
21
  }
20
22
  )
21
23
  ]
22
24
  )
25
+
26
+ if connect_task and connect_task.exception() is not None:
27
+ raise connect_task.exception()
@@ -1,4 +1,4 @@
1
- from typing import List
1
+ from typing import List, Optional
2
2
 
3
3
  import bluesky.plan_stubs as bps
4
4
  from bluesky.utils import short_uid
@@ -6,8 +6,33 @@ from bluesky.utils import short_uid
6
6
  from ophyd_async.core.detector import DetectorTrigger, StandardDetector, TriggerInfo
7
7
  from ophyd_async.core.flyer import HardwareTriggeredFlyable
8
8
  from ophyd_async.core.utils import in_micros
9
- from ophyd_async.panda._table import SeqTable, SeqTableRow, seq_table_from_rows
10
- from ophyd_async.panda._trigger import SeqTableInfo
9
+ from ophyd_async.panda import (
10
+ PcompDirectionOptions,
11
+ PcompInfo,
12
+ SeqTable,
13
+ SeqTableInfo,
14
+ SeqTableRow,
15
+ seq_table_from_rows,
16
+ )
17
+
18
+
19
+ def prepare_static_pcomp_flyer_and_detectors(
20
+ flyer: HardwareTriggeredFlyable[PcompInfo],
21
+ detectors: List[StandardDetector],
22
+ pcomp_info: PcompInfo,
23
+ trigger_info: TriggerInfo,
24
+ ):
25
+ """Prepare a hardware triggered flyable and one or more detectors.
26
+
27
+ Prepare a hardware triggered flyable and one or more detectors with the
28
+ same trigger.
29
+
30
+ """
31
+
32
+ for det in detectors:
33
+ yield from bps.prepare(det, trigger_info, wait=False, group="prep")
34
+ yield from bps.prepare(flyer, pcomp_info, wait=False, group="prep")
35
+ yield from bps.wait(group="prep")
11
36
 
12
37
 
13
38
  def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
@@ -18,7 +43,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
18
43
  shutter_time: float,
19
44
  repeats: int = 1,
20
45
  period: float = 0.0,
21
- frame_timeout: float | None = None,
46
+ frame_timeout: Optional[float] = None,
22
47
  ):
23
48
  """Prepare a hardware triggered flyable and one or more detectors.
24
49
 
@@ -36,7 +61,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
36
61
  deadtime = max(det.controller.get_deadtime(exposure) for det in detectors)
37
62
 
38
63
  trigger_info = TriggerInfo(
39
- num=number_of_frames * repeats,
64
+ number=number_of_frames * repeats,
40
65
  trigger=DetectorTrigger.constant_gate,
41
66
  deadtime=deadtime,
42
67
  livetime=exposure,
@@ -65,7 +90,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
65
90
  SeqTableRow(time2=in_micros(shutter_time)),
66
91
  )
67
92
 
68
- table_info = SeqTableInfo(table, repeats)
93
+ table_info = SeqTableInfo(sequence_table=table, repeats=repeats)
69
94
 
70
95
  for det in detectors:
71
96
  yield from bps.prepare(det, trigger_info, wait=False, group="prep")
@@ -75,7 +100,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
75
100
 
76
101
  def fly_and_collect(
77
102
  stream_name: str,
78
- flyer: HardwareTriggeredFlyable[SeqTableInfo],
103
+ flyer: HardwareTriggeredFlyable[SeqTableInfo] | HardwareTriggeredFlyable[PcompInfo],
79
104
  detectors: List[StandardDetector],
80
105
  ):
81
106
  """Kickoff, complete and collect with a flyer and multiple detectors.
@@ -113,6 +138,32 @@ def fly_and_collect(
113
138
  yield from bps.wait(group=group)
114
139
 
115
140
 
141
+ def fly_and_collect_with_static_pcomp(
142
+ stream_name: str,
143
+ flyer: HardwareTriggeredFlyable[PcompInfo],
144
+ detectors: List[StandardDetector],
145
+ number_of_pulses: int,
146
+ pulse_width: int,
147
+ rising_edge_step: int,
148
+ direction: PcompDirectionOptions,
149
+ trigger_info: TriggerInfo,
150
+ ):
151
+ # Set up scan and prepare trigger
152
+ pcomp_info = PcompInfo(
153
+ start_postion=0,
154
+ pulse_width=pulse_width,
155
+ rising_edge_step=rising_edge_step,
156
+ number_of_pulses=number_of_pulses,
157
+ direction=direction,
158
+ )
159
+ yield from prepare_static_pcomp_flyer_and_detectors(
160
+ flyer, detectors, pcomp_info, trigger_info
161
+ )
162
+
163
+ # Run the fly scan
164
+ yield from fly_and_collect(stream_name, flyer, detectors)
165
+
166
+
116
167
  def time_resolved_fly_and_collect_with_static_seq_table(
117
168
  stream_name: str,
118
169
  flyer: HardwareTriggeredFlyable[SeqTableInfo],