ophyd-async 0.12.3__py3-none-any.whl → 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ophyd_async/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.12.3'
21
- __version_tuple__ = version_tuple = (0, 12, 3)
20
+ __version__ = version = '0.13.0'
21
+ __version_tuple__ = version_tuple = (0, 13, 0)
@@ -16,6 +16,12 @@ from ._detector import (
16
16
  )
17
17
  from ._device import Device, DeviceConnector, DeviceVector, init_devices
18
18
  from ._device_filler import DeviceFiller
19
+ from ._enums import (
20
+ EnabledDisabled,
21
+ EnableDisable,
22
+ InOut,
23
+ OnOff,
24
+ )
19
25
  from ._flyer import FlyerController, FlyMotorInfo, StandardFlyer
20
26
  from ._hdf_dataset import HDFDatasetDescription, HDFDocumentComposer
21
27
  from ._log import config_ophyd_async_logging
@@ -209,4 +215,9 @@ __all__ = [
209
215
  # Back compat - delete before 1.0
210
216
  "ConfigSignal",
211
217
  "HintedSignal",
218
+ # Standard enums
219
+ "EnabledDisabled",
220
+ "EnableDisable",
221
+ "InOut",
222
+ "OnOff",
212
223
  ]
@@ -104,7 +104,11 @@ class DetectorController(ABC):
104
104
 
105
105
  @abstractmethod
106
106
  def get_deadtime(self, exposure: float | None) -> float:
107
- """For a given exposure, how long should the time between exposures be."""
107
+ """Get state-independent deadtime.
108
+
109
+ For a given exposure, what is the safest minimum time between exposures that
110
+ can be determined without reading signals.
111
+ """
108
112
 
109
113
  @abstractmethod
110
114
  async def prepare(self, trigger_info: TriggerInfo) -> None:
@@ -313,14 +317,7 @@ class StandardDetector(
313
317
  if value.trigger != DetectorTrigger.INTERNAL and not value.deadtime:
314
318
  msg = "Deadtime must be supplied when in externally triggered mode"
315
319
  raise ValueError(msg)
316
- required_deadtime = self._controller.get_deadtime(value.livetime)
317
- if value.deadtime and required_deadtime > value.deadtime:
318
- msg = (
319
- f"Detector {self._controller} needs at least {required_deadtime}s "
320
- f"deadtime, but trigger logic provides only {value.deadtime}s"
321
- )
322
- raise ValueError(msg)
323
- elif not value.deadtime:
320
+ if not value.deadtime:
324
321
  value.deadtime = self._controller.get_deadtime(value.livetime)
325
322
  self._trigger_info = value
326
323
  self._number_of_events_iter = iter(
@@ -0,0 +1,21 @@
1
+ from ._utils import StrictEnum
2
+
3
+
4
+ class OnOff(StrictEnum):
5
+ ON = "On"
6
+ OFF = "Off"
7
+
8
+
9
+ class EnableDisable(StrictEnum):
10
+ ENABLE = "Enable"
11
+ DISABLE = "Disable"
12
+
13
+
14
+ class EnabledDisabled(StrictEnum):
15
+ ENABLED = "Enabled"
16
+ DISABLED = "Disabled"
17
+
18
+
19
+ class InOut(StrictEnum):
20
+ IN = "In"
21
+ OUT = "Out"
@@ -5,12 +5,11 @@ https://github.com/areaDetector/ADAravis
5
5
 
6
6
  from ._aravis import AravisDetector
7
7
  from ._aravis_controller import AravisController
8
- from ._aravis_io import AravisDriverIO, AravisTriggerMode, AravisTriggerSource
8
+ from ._aravis_io import AravisDriverIO, AravisTriggerSource
9
9
 
10
10
  __all__ = [
11
11
  "AravisDetector",
12
12
  "AravisController",
13
13
  "AravisDriverIO",
14
- "AravisTriggerMode",
15
14
  "AravisTriggerSource",
16
15
  ]
@@ -1,9 +1,9 @@
1
1
  import asyncio
2
2
 
3
- from ophyd_async.core import DetectorTrigger, TriggerInfo
3
+ from ophyd_async.core import DetectorTrigger, OnOff, TriggerInfo
4
4
  from ophyd_async.epics import adcore
5
5
 
6
- from ._aravis_io import AravisDriverIO, AravisTriggerMode, AravisTriggerSource
6
+ from ._aravis_io import AravisDriverIO, AravisTriggerSource
7
7
 
8
8
  # The deadtime of an ADaravis controller varies depending on the exact model of camera.
9
9
  # Ideally we would maximize performance by dynamically retrieving the deadtime at
@@ -23,14 +23,14 @@ class AravisController(adcore.ADBaseController[AravisDriverIO]):
23
23
 
24
24
  if trigger_info.trigger is DetectorTrigger.INTERNAL:
25
25
  # Set trigger mode off to ignore the trigger source
26
- await self.driver.trigger_mode.set(AravisTriggerMode.OFF)
26
+ await self.driver.trigger_mode.set(OnOff.OFF)
27
27
  elif trigger_info.trigger in {
28
28
  DetectorTrigger.CONSTANT_GATE,
29
29
  DetectorTrigger.EDGE_TRIGGER,
30
30
  }:
31
31
  # Trigger on the rising edge of Line1
32
32
  # trigger mode must be set first and on it's own!
33
- await self.driver.trigger_mode.set(AravisTriggerMode.ON)
33
+ await self.driver.trigger_mode.set(OnOff.ON)
34
34
  await self.driver.trigger_source.set(AravisTriggerSource.LINE1)
35
35
  else:
36
36
  raise ValueError(f"ADAravis does not support {trigger_info.trigger}")
@@ -1,20 +1,10 @@
1
1
  from typing import Annotated as A
2
2
 
3
- from ophyd_async.core import SignalRW, StrictEnum, SubsetEnum
3
+ from ophyd_async.core import OnOff, SignalRW, SubsetEnum
4
4
  from ophyd_async.epics import adcore
5
5
  from ophyd_async.epics.core import PvSuffix
6
6
 
7
7
 
8
- class AravisTriggerMode(StrictEnum):
9
- """GigEVision GenICAM standard TriggerMode."""
10
-
11
- ON = "On"
12
- """Use TriggerSource to trigger each frame"""
13
-
14
- OFF = "Off"
15
- """Just trigger as fast as you can"""
16
-
17
-
18
8
  class AravisTriggerSource(SubsetEnum):
19
9
  """Which trigger source to use when TriggerMode=On."""
20
10
 
@@ -27,5 +17,5 @@ class AravisDriverIO(adcore.ADBaseIO):
27
17
  This mirrors the interface provided by ADAravis/db/aravisCamera.template.
28
18
  """
29
19
 
30
- trigger_mode: A[SignalRW[AravisTriggerMode], PvSuffix.rbv("TriggerMode")]
20
+ trigger_mode: A[SignalRW[OnOff], PvSuffix.rbv("TriggerMode")]
31
21
  trigger_source: A[SignalRW[AravisTriggerSource], PvSuffix.rbv("TriggerSource")]
@@ -7,7 +7,6 @@ from ._core_detector import AreaDetector, ContAcqAreaDetector
7
7
  from ._core_io import (
8
8
  ADBaseDatasetDescriber,
9
9
  ADBaseIO,
10
- ADCallbacks,
11
10
  ADCompression,
12
11
  ADState,
13
12
  NDArrayBaseIO,
@@ -18,6 +17,7 @@ from ._core_io import (
18
17
  NDPluginBaseIO,
19
18
  NDPluginCBIO,
20
19
  NDPluginStatsIO,
20
+ NDROIStatIO,
21
21
  )
22
22
  from ._core_logic import DEFAULT_GOOD_STATES, ADBaseContAcqController, ADBaseController
23
23
  from ._core_writer import ADWriter
@@ -33,11 +33,11 @@ from ._utils import (
33
33
  NDAttributeParam,
34
34
  NDAttributePv,
35
35
  NDAttributePvDbrType,
36
+ ndattributes_to_xml,
36
37
  )
37
38
 
38
39
  __all__ = [
39
40
  "ADBaseIO",
40
- "ADCallbacks",
41
41
  "ADCompression",
42
42
  "ADBaseContAcqController",
43
43
  "AreaDetector",
@@ -49,6 +49,7 @@ __all__ = [
49
49
  "NDFileHDFIO",
50
50
  "NDPluginBaseIO",
51
51
  "NDPluginStatsIO",
52
+ "NDROIStatIO",
52
53
  "DEFAULT_GOOD_STATES",
53
54
  "ADBaseDatasetDescriber",
54
55
  "ADBaseController",
@@ -66,4 +67,5 @@ __all__ = [
66
67
  "NDAttributePvDbrType",
67
68
  "NDCBFlushOnSoftTrgMode",
68
69
  "NDPluginCBIO",
70
+ "ndattributes_to_xml",
69
71
  ]
@@ -1,17 +1,19 @@
1
1
  import asyncio
2
2
  from typing import Annotated as A
3
3
 
4
- from ophyd_async.core import DatasetDescriber, SignalR, SignalRW, StrictEnum
4
+ from ophyd_async.core import (
5
+ DatasetDescriber,
6
+ DeviceVector,
7
+ EnableDisable,
8
+ SignalR,
9
+ SignalRW,
10
+ StrictEnum,
11
+ )
5
12
  from ophyd_async.epics.core import EpicsDevice, PvSuffix
6
13
 
7
14
  from ._utils import ADBaseDataType, ADFileWriteMode, ADImageMode, convert_ad_dtype_to_np
8
15
 
9
16
 
10
- class ADCallbacks(StrictEnum):
11
- ENABLE = "Enable"
12
- DISABLE = "Disable"
13
-
14
-
15
17
  class NDArrayBaseIO(EpicsDevice):
16
18
  """Class responsible for passing detector data from drivers to pluglins.
17
19
 
@@ -53,7 +55,7 @@ class NDPluginBaseIO(NDArrayBaseIO):
53
55
  """
54
56
 
55
57
  nd_array_port: A[SignalRW[str], PvSuffix.rbv("NDArrayPort")]
56
- enable_callbacks: A[SignalRW[ADCallbacks], PvSuffix.rbv("EnableCallbacks")]
58
+ enable_callbacks: A[SignalRW[EnableDisable], PvSuffix.rbv("EnableCallbacks")]
57
59
  nd_array_address: A[SignalRW[int], PvSuffix.rbv("NDArrayAddress")]
58
60
  array_size0: A[SignalR[int], PvSuffix("ArraySize0_RBV")]
59
61
  array_size1: A[SignalR[int], PvSuffix("ArraySize1_RBV")]
@@ -87,6 +89,56 @@ class NDPluginStatsIO(NDPluginBaseIO):
87
89
  hist_max: A[SignalRW[float], PvSuffix.rbv("HistMax")]
88
90
 
89
91
 
92
+ class NDROIStatIO(NDPluginBaseIO):
93
+ """Plugin for calculating basic statistics for multiple ROIs.
94
+
95
+ Each ROI is implemented as an instance of NDROIStatNIO,
96
+ and the collection of ROIs is held as a DeviceVector.
97
+
98
+ See HTML docs at https://areadetector.github.io/areaDetector/ADCore/NDPluginROIStat.html
99
+ """
100
+
101
+ def __init__(self, prefix, num_channels=8, with_pvi=False, name=""):
102
+ self.channels = DeviceVector(
103
+ {i: NDROIStatNIO(f"{prefix}{i}:") for i in range(1, num_channels + 1)}
104
+ )
105
+ super().__init__(prefix, with_pvi, name)
106
+
107
+
108
+ class NDROIStatNIO(EpicsDevice):
109
+ """Defines the parameters for a single ROI used for statistics calculation.
110
+
111
+ Each instance represents a single ROI, with attributes for its position
112
+ (min_x, min_y) and size (size_x, size_y), as well as a name and use status.
113
+
114
+ See definition in ADApp/pluginSrc/NDPluginROIStat.h in https://github.com/areaDetector/ADCore.
115
+
116
+ Attributes:
117
+ name: The name of the ROI.
118
+ use: Flag indicating whether the ROI is used.
119
+ min_x: The start X-coordinate of the ROI.
120
+ min_y: The start Y-coordinate of the ROI.
121
+ size_x: The width of the ROI.
122
+ size_y: The height of the ROI.
123
+ min_value: Minimum count value in the ROI.
124
+ max_value: Maximum count value in the ROI.
125
+ mean_value: Mean counts value in the ROI.
126
+ total: Total counts in the ROI.
127
+ """
128
+
129
+ name_: A[SignalRW[str], PvSuffix("Name")]
130
+ use: A[SignalRW[bool], PvSuffix.rbv("Use")]
131
+ min_x: A[SignalRW[int], PvSuffix.rbv("MinX")]
132
+ min_y: A[SignalRW[int], PvSuffix.rbv("MinY")]
133
+ size_x: A[SignalRW[int], PvSuffix.rbv("SizeX")]
134
+ size_y: A[SignalRW[int], PvSuffix.rbv("SizeY")]
135
+ # stats
136
+ min_value: A[SignalR[float], PvSuffix("MinValue_RBV")]
137
+ max_value: A[SignalR[float], PvSuffix("MaxValue_RBV")]
138
+ mean_value: A[SignalR[float], PvSuffix("MeanValue_RBV")]
139
+ total: A[SignalR[float], PvSuffix("Total_RBV")]
140
+
141
+
90
142
  class ADState(StrictEnum):
91
143
  """Default set of states of an AreaDetector driver.
92
144
 
@@ -6,6 +6,7 @@ from ophyd_async.core import (
6
6
  AsyncStatus,
7
7
  DetectorController,
8
8
  DetectorTrigger,
9
+ EnableDisable,
9
10
  TriggerInfo,
10
11
  observe_value,
11
12
  set_and_wait_for_value,
@@ -14,7 +15,6 @@ from ophyd_async.epics.core import stop_busy_record
14
15
 
15
16
  from ._core_io import (
16
17
  ADBaseIO,
17
- ADCallbacks,
18
18
  ADState,
19
19
  NDCBFlushOnSoftTrgMode,
20
20
  NDPluginCBIO,
@@ -199,7 +199,7 @@ class ADBaseContAcqController(ADBaseController[ADBaseIO]):
199
199
 
200
200
  # Configure the CB plugin to collect the correct number of triggers
201
201
  await asyncio.gather(
202
- self.cb_plugin.enable_callbacks.set(ADCallbacks.ENABLE),
202
+ self.cb_plugin.enable_callbacks.set(EnableDisable.ENABLE),
203
203
  self.cb_plugin.pre_count.set(0),
204
204
  self.cb_plugin.post_count.set(trigger_info.total_number_of_exposures),
205
205
  self.cb_plugin.preset_trigger_count.set(1),
@@ -12,6 +12,7 @@ from event_model import ( # type: ignore
12
12
  from pydantic import PositiveInt
13
13
 
14
14
  from ophyd_async.core._detector import DetectorWriter
15
+ from ophyd_async.core._enums import EnableDisable
15
16
  from ophyd_async.core._providers import DatasetDescriber, PathInfo, PathProvider
16
17
  from ophyd_async.core._signal import (
17
18
  observe_value,
@@ -24,7 +25,6 @@ from ophyd_async.epics.core import stop_busy_record
24
25
  # from ophyd_async.epics.adcore._core_logic import ADBaseDatasetDescriber
25
26
  from ._core_io import (
26
27
  ADBaseDatasetDescriber,
27
- ADCallbacks,
28
28
  NDArrayBaseIO,
29
29
  NDFileIO,
30
30
  NDFilePluginIO,
@@ -89,7 +89,7 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
89
89
  )
90
90
 
91
91
  if isinstance(self.fileio, NDFilePluginIO):
92
- await self.fileio.enable_callbacks.set(ADCallbacks.ENABLE)
92
+ await self.fileio.enable_callbacks.set(EnableDisable.ENABLE)
93
93
 
94
94
  # Set the directory creation depth first, since dir creation callback happens
95
95
  # when directory path PV is processed.
@@ -99,12 +99,6 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
99
99
  )
100
100
  ]
101
101
 
102
- self._composer = HDFDocumentComposer(
103
- # See https://github.com/bluesky/ophyd-async/issues/122
104
- f"{self._path_info.directory_uri}{self._path_info.filename}{self._file_extension}",
105
- self._datasets,
106
- )
107
-
108
102
  # And all the scalar datasets
109
103
  for plugin in self._plugins.values():
110
104
  maybe_xml = await plugin.nd_attributes_file.get_value()
@@ -136,6 +130,12 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
136
130
  )
137
131
  )
138
132
 
133
+ self._composer = HDFDocumentComposer(
134
+ # See https://github.com/bluesky/ophyd-async/issues/122
135
+ f"{self._path_info.directory_uri}{self._path_info.filename}{self._file_extension}",
136
+ self._datasets,
137
+ )
138
+
139
139
  describe = {
140
140
  ds.data_key: DataKey(
141
141
  source=self.fileio.full_file_name.source,
@@ -1,4 +1,6 @@
1
+ from collections.abc import Sequence
1
2
  from dataclasses import dataclass
3
+ from xml.etree import ElementTree as ET
2
4
 
3
5
  from ophyd_async.core import (
4
6
  SignalR,
@@ -130,3 +132,39 @@ class NDAttributeParam:
130
132
  datatype: NDAttributeDataType # The datatype of the parameter
131
133
  addr: int = 0 # The address as seen in the INP link of the record
132
134
  description: str = "" # A description that appears in the HDF file as an attribute
135
+
136
+
137
+ def ndattributes_to_xml(
138
+ ndattributes: Sequence[NDAttributeParam | NDAttributePv],
139
+ ) -> str:
140
+ """Convert a set of NDAttribute params to XML."""
141
+ root = ET.Element("Attributes")
142
+ for ndattribute in ndattributes:
143
+ if isinstance(ndattribute, NDAttributeParam):
144
+ ET.SubElement(
145
+ root,
146
+ "Attribute",
147
+ name=ndattribute.name,
148
+ type="PARAM",
149
+ source=ndattribute.param,
150
+ addr=str(ndattribute.addr),
151
+ datatype=ndattribute.datatype.value,
152
+ description=ndattribute.description,
153
+ )
154
+ elif isinstance(ndattribute, NDAttributePv):
155
+ ET.SubElement(
156
+ root,
157
+ "Attribute",
158
+ name=ndattribute.name,
159
+ type="EPICS_PV",
160
+ source=ndattribute.signal.source.split("ca://")[-1],
161
+ dbrtype=ndattribute.dbrtype.value,
162
+ description=ndattribute.description,
163
+ )
164
+ else:
165
+ raise ValueError(
166
+ f"Invalid type for ndattributes: {type(ndattribute)}. "
167
+ "Expected NDAttributePv or NDAttributeParam."
168
+ )
169
+ xml_text = ET.tostring(root, encoding="unicode")
170
+ return xml_text
@@ -4,7 +4,6 @@ from ._vimba_io import (
4
4
  VimbaConvertFormat,
5
5
  VimbaDriverIO,
6
6
  VimbaExposeOutMode,
7
- VimbaOnOff,
8
7
  VimbaOverlap,
9
8
  VimbaTriggerSource,
10
9
  )
@@ -14,7 +13,6 @@ __all__ = [
14
13
  "VimbaController",
15
14
  "VimbaDriverIO",
16
15
  "VimbaExposeOutMode",
17
- "VimbaOnOff",
18
16
  "VimbaTriggerSource",
19
17
  "VimbaOverlap",
20
18
  "VimbaConvertFormat",
@@ -1,18 +1,15 @@
1
1
  import asyncio
2
2
 
3
- from ophyd_async.core import (
4
- DetectorTrigger,
5
- TriggerInfo,
6
- )
3
+ from ophyd_async.core import DetectorTrigger, OnOff, TriggerInfo
7
4
  from ophyd_async.epics import adcore
8
5
 
9
- from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource
6
+ from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaTriggerSource
10
7
 
11
8
  TRIGGER_MODE = {
12
- DetectorTrigger.INTERNAL: VimbaOnOff.OFF,
13
- DetectorTrigger.CONSTANT_GATE: VimbaOnOff.ON,
14
- DetectorTrigger.VARIABLE_GATE: VimbaOnOff.ON,
15
- DetectorTrigger.EDGE_TRIGGER: VimbaOnOff.ON,
9
+ DetectorTrigger.INTERNAL: OnOff.OFF,
10
+ DetectorTrigger.CONSTANT_GATE: OnOff.ON,
11
+ DetectorTrigger.VARIABLE_GATE: OnOff.ON,
12
+ DetectorTrigger.EDGE_TRIGGER: OnOff.ON,
16
13
  }
17
14
 
18
15
  EXPOSE_OUT_MODE = {
@@ -1,6 +1,6 @@
1
1
  from typing import Annotated as A
2
2
 
3
- from ophyd_async.core import SignalRW, StrictEnum
3
+ from ophyd_async.core import OnOff, SignalRW, StrictEnum
4
4
  from ophyd_async.epics import adcore
5
5
  from ophyd_async.epics.core import PvSuffix
6
6
 
@@ -30,17 +30,10 @@ class VimbaTriggerSource(StrictEnum):
30
30
  class VimbaOverlap(StrictEnum):
31
31
  """Overlap modes for the Vimba detector."""
32
32
 
33
- OFF = "Off"
33
+ OFF = OnOff.OFF
34
34
  PREV_FRAME = "PreviousFrame"
35
35
 
36
36
 
37
- class VimbaOnOff(StrictEnum):
38
- """On/Off modes on the Vimba detector."""
39
-
40
- ON = "On"
41
- OFF = "Off"
42
-
43
-
44
37
  class VimbaExposeOutMode(StrictEnum):
45
38
  """Exposure control modes for Vimba detectors."""
46
39
 
@@ -55,6 +48,6 @@ class VimbaDriverIO(adcore.ADBaseIO):
55
48
  SignalRW[VimbaConvertFormat], PvSuffix("ConvertPixelFormat")
56
49
  ]
57
50
  trigger_source: A[SignalRW[VimbaTriggerSource], PvSuffix("TriggerSource")]
58
- trigger_mode: A[SignalRW[VimbaOnOff], PvSuffix("TriggerMode")]
51
+ trigger_mode: A[SignalRW[OnOff], PvSuffix("TriggerMode")]
59
52
  trigger_overlap: A[SignalRW[VimbaOverlap], PvSuffix("TriggerOverlap")]
60
53
  exposure_mode: A[SignalRW[VimbaExposeOutMode], PvSuffix("ExposureMode")]
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import os
2
3
  import sys
3
4
  import typing
4
5
  from collections.abc import Mapping, Sequence
@@ -40,6 +41,10 @@ from ._util import EpicsSignalBackend, format_datatype, get_supported_values
40
41
  logger = logging.getLogger("ophyd_async")
41
42
 
42
43
 
44
+ def _all_updates() -> bool:
45
+ return os.environ.get("OPHYD_ASYNC_EPICS_CA_KEEP_ALL_UPDATES", "True") == "True"
46
+
47
+
43
48
  def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
44
49
  def get_limits(limit: str) -> LimitsRange | None:
45
50
  low = getattr(value, f"lower_{limit}_limit", nan)
@@ -250,12 +255,11 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
250
255
  datatype: type[SignalDatatypeT] | None,
251
256
  read_pv: str = "",
252
257
  write_pv: str = "",
253
- all_updates: bool = True,
254
258
  ):
255
259
  self.converter: CaConverter = DisconnectedCaConverter(float, dbr.DBR_DOUBLE)
256
260
  self.initial_values: dict[str, AugmentedValue] = {}
257
261
  self.subscription: Subscription | None = None
258
- self._all_updates = all_updates
262
+ self._all_updates = _all_updates()
259
263
  super().__init__(datatype, read_pv, write_pv)
260
264
 
261
265
  def source(self, name: str, read: bool):
@@ -1,3 +1,8 @@
1
1
  from ._pmac_io import PmacAxisAssignmentIO, PmacCoordIO, PmacIO, PmacTrajectoryIO
2
2
 
3
- __all__ = ["PmacAxisAssignmentIO", "PmacCoordIO", "PmacIO", "PmacTrajectoryIO"]
3
+ __all__ = [
4
+ "PmacAxisAssignmentIO",
5
+ "PmacCoordIO",
6
+ "PmacIO",
7
+ "PmacTrajectoryIO",
8
+ ]
@@ -69,6 +69,7 @@ class PmacCoordIO(Device):
69
69
 
70
70
  def __init__(self, prefix: str, name: str = "") -> None:
71
71
  self.defer_moves = epics_signal_rw(bool, f"{prefix}DeferMoves")
72
+ self.cs_port = epics_signal_r(str, f"{prefix}Port")
72
73
  self.cs_axis_setpoint = DeviceVector(
73
74
  {
74
75
  i + 1: epics_signal_rw(np.float64, f"{prefix}M{i + 1}:DirectDemand")
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Sequence
5
+ from dataclasses import dataclass
6
+
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+ from scanspec.core import Slice
10
+
11
+ from ophyd_async.core import error_if_none, gather_dict
12
+ from ophyd_async.epics.motor import Motor
13
+
14
+ from ._pmac_io import CS_LETTERS, PmacIO
15
+
16
+ # PMAC durations are in milliseconds
17
+ # We must convert from scanspec durations (seconds) to milliseconds
18
+ # PMAC motion program multiples durations by 0.001
19
+ # (see https://github.com/DiamondLightSource/pmac/blob/afe81f8bb9179c3a20eff351f30bc6cfd1539ad9/pmacApp/pmc/trajectory_scan_code_ppmac.pmc#L241)
20
+ # Therefore, we must divide scanspec durations by 10e-6
21
+ TICK_S = 0.000001
22
+
23
+
24
+ @dataclass
25
+ class _Trajectory:
26
+ positions: dict[Motor, np.ndarray]
27
+ velocities: dict[Motor, np.ndarray]
28
+ user_programs: npt.NDArray[np.int32]
29
+ durations: npt.NDArray[np.float64]
30
+
31
+ @classmethod
32
+ def from_slice(cls, slice: Slice[Motor], ramp_up_time: float) -> _Trajectory:
33
+ """Parse a trajectory with no gaps from a slice.
34
+
35
+ :param slice: Information about a series of scan frames along a number of axes
36
+ :param ramp_up_duration: Time required to ramp up to speed
37
+ :param ramp_down: Booleon representing if we ramp down or not
38
+ :returns Trajectory: Data class representing our parsed trajectory
39
+ :raises RuntimeError: Slice must have no gaps and a duration array
40
+ """
41
+ slice_duration = error_if_none(slice.duration, "Slice must have a duration")
42
+
43
+ # Check if any gaps other than initial gap.
44
+ if any(slice.gap[1:]):
45
+ raise RuntimeError(
46
+ f"Cannot parse trajectory with gaps. Slice has gaps: {slice.gap}"
47
+ )
48
+
49
+ scan_size = len(slice)
50
+ motors = slice.axes()
51
+
52
+ positions: dict[Motor, npt.NDArray[np.float64]] = {}
53
+ velocities: dict[Motor, npt.NDArray[np.float64]] = {}
54
+
55
+ # Initialise arrays
56
+ positions = {motor: np.empty(2 * scan_size + 1, float) for motor in motors}
57
+ velocities = {motor: np.empty(2 * scan_size + 1, float) for motor in motors}
58
+ durations: npt.NDArray[np.float64] = np.empty(2 * scan_size + 1, float)
59
+ user_programs: npt.NDArray[np.int32] = np.ones(2 * scan_size + 1, float)
60
+ user_programs[-1] = 8
61
+
62
+ # Ramp up time for start of collection window
63
+ durations[0] = int(ramp_up_time / TICK_S)
64
+ # Half the time per point
65
+ durations[1:] = np.repeat(slice_duration / (2 * TICK_S), 2)
66
+
67
+ # Fill profile assuming no gaps
68
+ # Excluding starting points, we begin at our next frame
69
+ half_durations = slice_duration / 2
70
+ for motor in motors:
71
+ # Set the first position to be lower bound, then
72
+ # alternate mid and upper as the upper and lower
73
+ # bounds of neighbouring points are the same as gap is false
74
+ positions[motor][0] = slice.lower[motor][0]
75
+ positions[motor][1::2] = slice.midpoints[motor]
76
+ positions[motor][2::2] = slice.upper[motor]
77
+ # For velocities we will need the relative distances
78
+ mid_to_upper_velocities = (
79
+ slice.upper[motor] - slice.midpoints[motor]
80
+ ) / half_durations
81
+ lower_to_mid_velocities = (
82
+ slice.midpoints[motor] - slice.lower[motor]
83
+ ) / half_durations
84
+ # First velocity is the lower -> mid of first point
85
+ velocities[motor][0] = lower_to_mid_velocities[0]
86
+ # For the midpoints, we take the average of the
87
+ # lower -> mid and mid-> upper velocities of the same point
88
+ velocities[motor][1::2] = (
89
+ lower_to_mid_velocities + mid_to_upper_velocities
90
+ ) / 2
91
+ # For the upper points, we need to take the average of the
92
+ # mid -> upper velocity of the previous point and
93
+ # lower -> mid velocity of the current point
94
+ velocities[motor][2:-1:2] = (
95
+ mid_to_upper_velocities[:-1] + lower_to_mid_velocities[1:]
96
+ ) / 2
97
+ # For the last velocity take the mid to upper velocity
98
+ velocities[motor][-1] = mid_to_upper_velocities[-1]
99
+
100
+ return cls(
101
+ positions=positions,
102
+ velocities=velocities,
103
+ user_programs=user_programs,
104
+ durations=durations,
105
+ )
106
+
107
+
108
+ @dataclass
109
+ class _PmacMotorInfo:
110
+ cs_port: str
111
+ cs_number: int
112
+ motor_cs_index: dict[Motor, int]
113
+ motor_acceleration_rate: dict[Motor, float]
114
+
115
+ @classmethod
116
+ async def from_motors(cls, pmac: PmacIO, motors: Sequence[Motor]) -> _PmacMotorInfo:
117
+ """Creates a _PmacMotorInfo instance based on a controller and list of motors.
118
+
119
+ :param pmac: The PMAC device
120
+ :param motors: Sequence of motors involved in trajectory
121
+ :raises RuntimeError:
122
+ if motors do not share common CS port or CS number, or if
123
+ motors do not have unique CS index assignments
124
+ :returns:
125
+ _PmacMotorInfo instance with motor's common CS port and CS number, and
126
+ dictionaries of motor's to their unique CS index and accelerate rate
127
+
128
+ """
129
+ assignments = {
130
+ motor: pmac.assignment[pmac.motor_assignment_index[motor]]
131
+ for motor in motors
132
+ }
133
+
134
+ cs_ports, cs_numbers, cs_axes, velocities, accls = await asyncio.gather(
135
+ gather_dict(
136
+ {motor: assignments[motor].cs_port.get_value() for motor in motors}
137
+ ),
138
+ gather_dict(
139
+ {motor: assignments[motor].cs_number.get_value() for motor in motors}
140
+ ),
141
+ gather_dict(
142
+ {
143
+ motor: assignments[motor].cs_axis_letter.get_value()
144
+ for motor in motors
145
+ }
146
+ ),
147
+ gather_dict({motor: motor.max_velocity.get_value() for motor in motors}),
148
+ gather_dict(
149
+ {motor: motor.acceleration_time.get_value() for motor in motors}
150
+ ),
151
+ )
152
+
153
+ # check if the values in cs_port and cs_number are the same
154
+ cs_ports = set(cs_ports.values())
155
+
156
+ if len(cs_ports) != 1:
157
+ raise RuntimeError(
158
+ "Failed to fetch common CS port."
159
+ "Motors passed are assigned to multiple CS ports:"
160
+ f"{list(cs_ports)}"
161
+ )
162
+
163
+ cs_port = cs_ports.pop()
164
+
165
+ cs_numbers = set(cs_numbers.values())
166
+ if len(cs_numbers) != 1:
167
+ raise RuntimeError(
168
+ "Failed to fetch common CS number."
169
+ "Motors passed are assigned to multiple CS numbers:"
170
+ f"{list(cs_numbers)}"
171
+ )
172
+
173
+ cs_number = cs_numbers.pop()
174
+
175
+ motor_cs_index = {}
176
+ for motor in cs_axes:
177
+ try:
178
+ if not cs_axes[motor]:
179
+ raise ValueError
180
+ motor_cs_index[motor] = CS_LETTERS.index(cs_axes[motor])
181
+ except ValueError as err:
182
+ raise ValueError(
183
+ "Failed to get motor CS index. "
184
+ f"Motor {motor.name} assigned to '{cs_axes[motor]}' "
185
+ f"but must be assignmed to '{CS_LETTERS}"
186
+ ) from err
187
+ if len(set(motor_cs_index.values())) != len(motor_cs_index.items()):
188
+ raise RuntimeError(
189
+ "Failed to fetch distinct CS Axes."
190
+ "Motors passed are assigned to the same CS Axis"
191
+ f"{list(motor_cs_index)}"
192
+ )
193
+
194
+ motor_acceleration_rate = {
195
+ motor: float(velocities[motor]) / float(accls[motor])
196
+ for motor in velocities
197
+ }
198
+
199
+ return _PmacMotorInfo(
200
+ cs_port, cs_number, motor_cs_index, motor_acceleration_rate
201
+ )
202
+
203
+
204
+ def calculate_ramp_position_and_duration(
205
+ slice: Slice[Motor], motor_info: _PmacMotorInfo, is_up: bool
206
+ ) -> tuple[dict[Motor, float], float]:
207
+ if slice.duration is None:
208
+ raise ValueError("Slice must have a duration")
209
+
210
+ scan_axes = slice.axes()
211
+ idx = 0 if is_up else -1
212
+
213
+ velocities: dict[Motor, float] = {}
214
+ ramp_times: list[float] = []
215
+ for axis in scan_axes:
216
+ velocity = (slice.upper[axis][idx] - slice.lower[axis][idx]) / slice.duration[
217
+ idx
218
+ ]
219
+ velocities[axis] = velocity
220
+ ramp_times.append(abs(velocity) / motor_info.motor_acceleration_rate[axis])
221
+
222
+ max_ramp_time = max(ramp_times)
223
+
224
+ motor_to_ramp_position = {}
225
+ sign = -1 if is_up else 1
226
+ for axis, v in velocities.items():
227
+ ref_pos = slice.lower[axis][0] if is_up else slice.upper[axis][-1]
228
+ displacement = 0.5 * v * max_ramp_time
229
+ motor_to_ramp_position[axis] = ref_pos + sign * displacement
230
+
231
+ return (motor_to_ramp_position, max_ramp_time)
@@ -1,57 +1,31 @@
1
1
  from collections.abc import Sequence
2
- from xml.etree import ElementTree as ET
3
2
 
4
3
  import bluesky.plan_stubs as bps
5
4
 
6
- from ophyd_async.core import Device
7
5
  from ophyd_async.epics.adcore import (
6
+ AreaDetector,
8
7
  NDArrayBaseIO,
9
8
  NDAttributeDataType,
10
9
  NDAttributeParam,
11
10
  NDAttributePv,
12
11
  NDFileHDFIO,
12
+ ndattributes_to_xml,
13
13
  )
14
14
 
15
15
 
16
16
  def setup_ndattributes(
17
- device: NDArrayBaseIO, ndattributes: Sequence[NDAttributePv | NDAttributeParam]
17
+ device: NDArrayBaseIO, ndattributes: Sequence[NDAttributeParam | NDAttributePv]
18
18
  ):
19
- """Set up attributes on NdArray devices."""
20
- root = ET.Element("Attributes")
21
-
22
- for ndattribute in ndattributes:
23
- if isinstance(ndattribute, NDAttributeParam):
24
- ET.SubElement(
25
- root,
26
- "Attribute",
27
- name=ndattribute.name,
28
- type="PARAM",
29
- source=ndattribute.param,
30
- addr=str(ndattribute.addr),
31
- datatype=ndattribute.datatype.value,
32
- description=ndattribute.description,
33
- )
34
- elif isinstance(ndattribute, NDAttributePv):
35
- ET.SubElement(
36
- root,
37
- "Attribute",
38
- name=ndattribute.name,
39
- type="EPICS_PV",
40
- source=ndattribute.signal.source.split("ca://")[-1],
41
- dbrtype=ndattribute.dbrtype.value,
42
- description=ndattribute.description,
43
- )
44
- else:
45
- raise ValueError(
46
- f"Invalid type for ndattributes: {type(ndattribute)}. "
47
- "Expected NDAttributePv or NDAttributeParam."
48
- )
49
- xml_text = ET.tostring(root, encoding="unicode")
50
- yield from bps.abs_set(device.nd_attributes_file, xml_text, wait=True)
19
+ xml = ndattributes_to_xml(ndattributes)
20
+ yield from bps.abs_set(
21
+ device.nd_attributes_file,
22
+ xml,
23
+ wait=True,
24
+ )
51
25
 
52
26
 
53
- def setup_ndstats_sum(detector: Device):
54
- """Set up nd stats for a detector."""
27
+ def setup_ndstats_sum(detector: AreaDetector):
28
+ """Set up nd stats sum nd attribute for a detector."""
55
29
  hdf = getattr(detector, "fileio", None)
56
30
  if not isinstance(hdf, NDFileHDFIO):
57
31
  msg = (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ophyd-async
3
- Version: 0.12.3
3
+ Version: 0.13.0
4
4
  Summary: Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango
5
5
  Author-email: Tom Cobb <tom.cobb@diamond.ac.uk>
6
6
  License: BSD 3-Clause License
@@ -49,6 +49,7 @@ Requires-Dist: colorlog
49
49
  Requires-Dist: pydantic>=2.0
50
50
  Requires-Dist: pydantic-numpy
51
51
  Requires-Dist: stamina>=23.1.0
52
+ Requires-Dist: scanspec>=1.0a1
52
53
  Provides-Extra: sim
53
54
  Requires-Dist: h5py; extra == "sim"
54
55
  Provides-Extra: ca
@@ -1,14 +1,15 @@
1
1
  ophyd_async/__init__.py,sha256=dcAA3qsj1nNIMe5l-v2tlduZ_ypwBmyuHe45Lsq4k4w,206
2
2
  ophyd_async/__main__.py,sha256=n_U4O9bgm97OuboUB_9eK7eFiwy8BZSgXJ0OzbE0DqU,481
3
3
  ophyd_async/_docs_parser.py,sha256=gPYrigfSbYCF7QoSf2UvE-cpQu4snSssl7ZWN-kKDzI,352
4
- ophyd_async/_version.py,sha256=QKKPnNa5xCXCbNQc8EGW9uiKKjn_cC1ywc6jE5v1hDo,513
4
+ ophyd_async/_version.py,sha256=irlt-ETr2vtyV_bhp1DlVRDx0Kx9LTwnA5UW4gdE8ZA,513
5
5
  ophyd_async/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- ophyd_async/core/__init__.py,sha256=RtYk6FdJxc7lxoSReRV1D7weTRYFu9ylhNNd3DyN904,4752
6
+ ophyd_async/core/__init__.py,sha256=yRb_6ufTXFtf3fagShfmSDfiMduIhvNPgQyM-7GGwas,4929
7
7
  ophyd_async/core/_derived_signal.py,sha256=TuZza_j3J1Bw4QSqBYB9Ta2FyQP5BycO3nSHVtJ890Q,13015
8
8
  ophyd_async/core/_derived_signal_backend.py,sha256=PYyyont_nUR9LBC9eqVwueHCMwLJfQ_F7R_14sivBTU,12510
9
- ophyd_async/core/_detector.py,sha256=x1o-eSkvemQ-fTk47440owkTmYhuckA2ILNOCoJlHCY,15201
9
+ ophyd_async/core/_detector.py,sha256=Bl0_bpeAlo8hsp98dCkZnlKwRI0_gtXKKDE0-KrA-3k,14937
10
10
  ophyd_async/core/_device.py,sha256=lSm8FBul9NTn9VO0rsAlV9pctJyUsMdU2ztEf5CqH5M,14716
11
11
  ophyd_async/core/_device_filler.py,sha256=MDz8eQQ-eEAwo-UEMxfqPfpcBuMG01tLCGR6utwVnmE,14825
12
+ ophyd_async/core/_enums.py,sha256=7VpEvh9tFa-E3wn3x0SdXUlCG7AxSPwCRkf37B-0JbM,313
12
13
  ophyd_async/core/_flyer.py,sha256=8zKyU5aQOr_t59GIUwsYeb8NSabdvBp0swwuRe4v5VQ,3457
13
14
  ophyd_async/core/_hdf_dataset.py,sha256=0bIX_ZbFSMdXqDwRtEvV-0avHnwXhjPddE5GVNmo7H8,2608
14
15
  ophyd_async/core/_log.py,sha256=DxKR4Nz3SgTaTzKBZWqt-w48yT8WUAr_3Qr223TEWRw,3587
@@ -31,20 +32,20 @@ ophyd_async/epics/adandor/__init__.py,sha256=dlitllrAdhvh16PAcVMUSSEytTDNMu6_HuY
31
32
  ophyd_async/epics/adandor/_andor.py,sha256=SxAIP9OLefUqKcxrxhjNzil5D8-59Ps0vADdR6scO44,1281
32
33
  ophyd_async/epics/adandor/_andor_controller.py,sha256=i0AwWdk9tqB802C9d5gTgYugZ1UOcJCQLk130gVQNeo,1779
33
34
  ophyd_async/epics/adandor/_andor_io.py,sha256=DOrni8vcncQ3lW1JOpXo_jwMCLx1qsggbg6FXlsl5Ew,819
34
- ophyd_async/epics/adaravis/__init__.py,sha256=ZQaJVQiwcQn9hUZADrYgBE1sDfFEwjhVBJRPth1_LBo,395
35
+ ophyd_async/epics/adaravis/__init__.py,sha256=OY3hu1MpHbmQRZMulHC0uWfzoHmhLTvLdSrIhIQZoT0,351
35
36
  ophyd_async/epics/adaravis/_aravis.py,sha256=Ju2wuebz9_ovl-Kza39s5VQ1pV-Omt_BaIWKqP4kcGA,1315
36
- ophyd_async/epics/adaravis/_aravis_controller.py,sha256=WiFR7_FAAu6_88zG-yzGLsR9YcO4L6xR73Wnjw9n0i4,1908
37
- ophyd_async/epics/adaravis/_aravis_io.py,sha256=af5RxeXF2ligvAXwMNMKHA4QHTR_WmNFz-f18qD2dbg,855
38
- ophyd_async/epics/adcore/__init__.py,sha256=L7muAe24Uo-ow0CoUTf_ooI2ntA1ZS57TTFimJeBQJ8,1579
37
+ ophyd_async/epics/adaravis/_aravis_controller.py,sha256=aqIIj6awD2h870HAT3rPXg3Ro3G6Hp_vA5TvPbfS05o,1872
38
+ ophyd_async/epics/adaravis/_aravis_io.py,sha256=af639qsO2V8NU5I0LgxK8jrxuhXgM6eMxEFJwsCPdjQ,624
39
+ ophyd_async/epics/adcore/__init__.py,sha256=NWNLe2uFqU86P5mYbKINfHHrUVoTHeDN6829-7lBHxE,1631
39
40
  ophyd_async/epics/adcore/_core_detector.py,sha256=mRDaHgXCTZF-MIVsU1csoQx9jObutYDpMWayugx2-jI,2631
40
- ophyd_async/epics/adcore/_core_io.py,sha256=c1GqAUdv8lAQjklbKHtraLMhPOWEttDCsH9ow7M5I0U,7690
41
- ophyd_async/epics/adcore/_core_logic.py,sha256=eMS9mIF6Ef2oXg1PmzMyyVviAbhujzYGbZZGq9vzf5k,8896
42
- ophyd_async/epics/adcore/_core_writer.py,sha256=pcnJhMHuWeZvlCyGSlW1QE_bNZjGbMuI8IsYEVHDtL0,8507
43
- ophyd_async/epics/adcore/_hdf_writer.py,sha256=AvCv_5dd2HdQoE12_CR6vH2hig2EdEIWklOu6CbPVlc,5816
41
+ ophyd_async/epics/adcore/_core_io.py,sha256=w4IO3OiwCewVWWAJAB9txL_FFjdplE5ggP1OV4lCaKs,9646
42
+ ophyd_async/epics/adcore/_core_logic.py,sha256=uVg5gJCihYLnMbXQjKwb3prM-KoHM5wxjSor9v0HFK0,8900
43
+ ophyd_async/epics/adcore/_core_writer.py,sha256=vRCEPn_52PgMejgtpp00hRlzF20CT9jb3ooR9rZtNRs,8542
44
+ ophyd_async/epics/adcore/_hdf_writer.py,sha256=8rl6NQUt8r_9gTHAUWJq_YJ86f5iUiWadcaROarpLrE,5816
44
45
  ophyd_async/epics/adcore/_jpeg_writer.py,sha256=VYpUWQGEjrKG2kiRGQZlBCPXVJ1BzWb9GyB9KhxPWgo,688
45
46
  ophyd_async/epics/adcore/_single_trigger.py,sha256=tFGLT1b_rZzAvbqWP-hyCccxJMRY26T5IER-VAqKXmc,1275
46
47
  ophyd_async/epics/adcore/_tiff_writer.py,sha256=197Ky9ltsJjUKNwl8_OAuoCe8dWIc7zCFs7wautwC7Y,689
47
- ophyd_async/epics/adcore/_utils.py,sha256=eujvN4Tip_lIM2gYkbwkH_0cLRQSrQkylQOXUlpCo4k,3868
48
+ ophyd_async/epics/adcore/_utils.py,sha256=L7YQBrsNt6gL1JqVK_rW4fkHaRiDrDYlOtiLakMbXE8,5233
48
49
  ophyd_async/epics/adkinetix/__init__.py,sha256=A9xq3lGMrmza9lfukRixC0Up_kUDVFII8JguLr2x7Bw,308
49
50
  ophyd_async/epics/adkinetix/_kinetix.py,sha256=zZv0JZ8i1RSx7KBDn_1HGNOY0BoIP81mRK5TKq7d4eA,1302
50
51
  ophyd_async/epics/adkinetix/_kinetix_controller.py,sha256=UI-XcQpGj7jq-_e1ceoMOZkyfejwG6H5wX-Ntp_NJjg,1481
@@ -57,12 +58,12 @@ ophyd_async/epics/adsimdetector/__init__.py,sha256=EQqxP5DUvZGLxpvSXPagTPy3ROwE-
57
58
  ophyd_async/epics/adsimdetector/_sim.py,sha256=r3SuBENGkOU-8X-i3zDyZljFxf0SmsUfryNPFvP1_os,1147
58
59
  ophyd_async/epics/adsimdetector/_sim_controller.py,sha256=EmoorPTvRomEwfioA9UcrnhYaUBkvtLI2WY3zBLd4ec,476
59
60
  ophyd_async/epics/adsimdetector/_sim_io.py,sha256=TOQcawMtb0ypwUlPcbwGfvp5ZI9jceB9OXm53WBF1_o,233
60
- ophyd_async/epics/advimba/__init__.py,sha256=Onoe4N1DgjUZ0-00krm2-iJPXnNF8nPGs6I8pY9wB1g,429
61
+ ophyd_async/epics/advimba/__init__.py,sha256=fPfuakXOwQsFej26gLksqkfBiP6BZlfTyutw1q0tXOM,395
61
62
  ophyd_async/epics/advimba/_vimba.py,sha256=4XlEnsJMGDzHLuYaIDUmaxx0gtOAehn5BKBZMUAzoHQ,1241
62
- ophyd_async/epics/advimba/_vimba_controller.py,sha256=v0av2bGnaJ01w9Igksupt2IlkuBEFlAeRCPOVma-Xa4,1980
63
- ophyd_async/epics/advimba/_vimba_io.py,sha256=cb2Nfp05fBZAcNVXpz-rqRIRS-TiZW5DPUJOmaFyAw0,1589
63
+ ophyd_async/epics/advimba/_vimba_controller.py,sha256=KSbP4LHqYkCDplpmBk7hdf0Yz9J2vOxWn0fStFFAklA,1942
64
+ ophyd_async/epics/advimba/_vimba_io.py,sha256=CwAUGiCXcu7me7vHjGd2FfUBxUux1eidziYKhKiYoC4,1486
64
65
  ophyd_async/epics/core/__init__.py,sha256=q73i4aJ_0HApVNmf3eAw-q30XuazAyZW2MW5TXk-pOY,648
65
- ophyd_async/epics/core/_aioca.py,sha256=elNR5c2-YcDUoyzuvTEVURoqx92wkVoMDNjwjeZ0WmA,13136
66
+ ophyd_async/epics/core/_aioca.py,sha256=38aW5dd3MzwhoweNMjkOfnfZHI2JZFateO0YABGVSfQ,13230
66
67
  ophyd_async/epics/core/_epics_connector.py,sha256=S4z_wbj-aogVcjqCyUgjhcq5Y4gDC7y6wXbsSz2nODY,1918
67
68
  ophyd_async/epics/core/_epics_device.py,sha256=wGdR24I7GSPh3HmM7jsWKZhBZgt4IyLrCn4Ut7Wx_xo,510
68
69
  ophyd_async/epics/core/_p4p.py,sha256=uWh3oWPme74G4YfeJ6k8ZlHdKOwcf8Xp1J82b9aa_JI,16407
@@ -81,8 +82,9 @@ ophyd_async/epics/demo/point_detector.db,sha256=8kBa3XKpmfXCxetT4tq5_RFXa_XqS1Z2
81
82
  ophyd_async/epics/demo/point_detector_channel.db,sha256=FZ9H6HjqplhcF2jgimv_dT1nn-CBlfjs7Y--iCfHp5Y,632
82
83
  ophyd_async/epics/eiger/__init__.py,sha256=7kRqVzwoD8PVtp7Nj9iQWlgbLeoWE_8oiq-B0kixwTE,93
83
84
  ophyd_async/epics/eiger/_odin_io.py,sha256=JTnsADwNszWJoXpFmdcrZrCDc_RshGlM3Bg_aeUdrUg,6491
84
- ophyd_async/epics/pmac/__init__.py,sha256=QNL1imf90-4Jedsl1UU0bOF2SH5SWlmqAEVd2OD9_9I,163
85
- ophyd_async/epics/pmac/_pmac_io.py,sha256=3ngcNE5ytTW8TSnl34roqR9jtAKG0qlHjb44b0fd3Fk,3962
85
+ ophyd_async/epics/pmac/__init__.py,sha256=fQAFnVtzEiPlzXI5XgB62HyburMbUKDfOgkIO_JBPYc,182
86
+ ophyd_async/epics/pmac/_pmac_io.py,sha256=_lTUdNHTWOwPitAxXottyLrmiwhf36WfTbrdneqeg34,4022
87
+ ophyd_async/epics/pmac/_utils.py,sha256=6bb_yKNcmWSfuaYunRv5GcA8ML4jEUsf1jkXko_LSEo,8888
86
88
  ophyd_async/epics/testing/__init__.py,sha256=aTIv4D2DYrpnGco5RQF8QuLG1SfFkIlTyM2uYEKXltA,522
87
89
  ophyd_async/epics/testing/_example_ioc.py,sha256=uUmfMXV_Pd2SMFyb0y_4uTc6gkGRUqU1cJ-XQC2ROW8,3915
88
90
  ophyd_async/epics/testing/_utils.py,sha256=9gxpwaWX0HGtacu1LTupcw7viXN8G78RmuNciU_-cjs,1702
@@ -105,7 +107,7 @@ ophyd_async/fastcs/panda/_writer.py,sha256=UqsU44u0uIqkDNky3mIzhW3OhLeZ8TSqFS666
105
107
  ophyd_async/plan_stubs/__init__.py,sha256=sRe1Jna_6i7aKjE3pPzsP4iNMWeWdtiptLnOq9pov9M,619
106
108
  ophyd_async/plan_stubs/_ensure_connected.py,sha256=YR6VRj7koccJ4x35NV-Ugl4ZbxgAoGN9PjVIjhv0gpw,894
107
109
  ophyd_async/plan_stubs/_fly.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
108
- ophyd_async/plan_stubs/_nd_attributes.py,sha256=kwzyUSeidUH714gaZQtJLxCgDZtmIRyyoKBBRbvqg38,2350
110
+ ophyd_async/plan_stubs/_nd_attributes.py,sha256=1g41uLjBGFl9j_xs3I2RlL_IV7aPcRYvdJ_HmNy5urc,1246
109
111
  ophyd_async/plan_stubs/_panda.py,sha256=5_Mf9kGzNjXpf_YscpCUE8tgq284nOHWCG7o_LNFfII,463
110
112
  ophyd_async/plan_stubs/_settings.py,sha256=e3dGVSUV-Htay_9fKXyQTAQLdjunetGI3OBYp_oC_FY,5574
111
113
  ophyd_async/plan_stubs/_utils.py,sha256=zClRo5ve8RGia7wQnby41W-Zprj-slOA5da1LfYnuhw,45
@@ -145,8 +147,8 @@ ophyd_async/testing/_one_of_everything.py,sha256=Di0hPoKwrDOSsx50-2UdSHM2EbIKrPG
145
147
  ophyd_async/testing/_single_derived.py,sha256=5-HOTzgePcZ354NK_ssVpyIbJoJmKyjVQCxSwQXUC-4,2730
146
148
  ophyd_async/testing/_utils.py,sha256=zClRo5ve8RGia7wQnby41W-Zprj-slOA5da1LfYnuhw,45
147
149
  ophyd_async/testing/_wait_for_pending.py,sha256=YZAR48n-CW0GsPey3zFRzMJ4byDAr3HvMIoawjmTrHw,732
148
- ophyd_async-0.12.3.dist-info/licenses/LICENSE,sha256=pU5shZcsvWgz701EbT7yjFZ8rMvZcWgRH54CRt8ld_c,1517
149
- ophyd_async-0.12.3.dist-info/METADATA,sha256=iFBZBqmWlXn03HELUSW5OPBsJdcYikFX6w1BjfMdUP4,7114
150
- ophyd_async-0.12.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
151
- ophyd_async-0.12.3.dist-info/top_level.txt,sha256=-hjorMsv5Rmjo3qrgqhjpal1N6kW5vMxZO3lD4iEaXs,12
152
- ophyd_async-0.12.3.dist-info/RECORD,,
150
+ ophyd_async-0.13.0.dist-info/licenses/LICENSE,sha256=pU5shZcsvWgz701EbT7yjFZ8rMvZcWgRH54CRt8ld_c,1517
151
+ ophyd_async-0.13.0.dist-info/METADATA,sha256=VfG70R_jA05hUvMrhl61ySxIeeQ0JGRkw7Yqp6bxpeM,7145
152
+ ophyd_async-0.13.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
153
+ ophyd_async-0.13.0.dist-info/top_level.txt,sha256=-hjorMsv5Rmjo3qrgqhjpal1N6kW5vMxZO3lD4iEaXs,12
154
+ ophyd_async-0.13.0.dist-info/RECORD,,