ophyd-async 0.9.0a2__py3-none-any.whl → 0.10.0a2__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 (151) hide show
  1. ophyd_async/__init__.py +5 -8
  2. ophyd_async/_docs_parser.py +12 -0
  3. ophyd_async/_version.py +9 -4
  4. ophyd_async/core/__init__.py +97 -62
  5. ophyd_async/core/_derived_signal.py +271 -0
  6. ophyd_async/core/_derived_signal_backend.py +300 -0
  7. ophyd_async/core/_detector.py +106 -125
  8. ophyd_async/core/_device.py +69 -63
  9. ophyd_async/core/_device_filler.py +65 -1
  10. ophyd_async/core/_flyer.py +14 -5
  11. ophyd_async/core/_hdf_dataset.py +29 -22
  12. ophyd_async/core/_log.py +14 -23
  13. ophyd_async/core/_mock_signal_backend.py +11 -3
  14. ophyd_async/core/_protocol.py +65 -45
  15. ophyd_async/core/_providers.py +28 -9
  16. ophyd_async/core/_readable.py +44 -35
  17. ophyd_async/core/_settings.py +36 -27
  18. ophyd_async/core/_signal.py +262 -170
  19. ophyd_async/core/_signal_backend.py +56 -13
  20. ophyd_async/core/_soft_signal_backend.py +16 -11
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +41 -11
  23. ophyd_async/core/_utils.py +96 -49
  24. ophyd_async/core/_yaml_settings.py +2 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/_andor.py +2 -2
  27. ophyd_async/epics/adandor/_andor_controller.py +4 -2
  28. ophyd_async/epics/adandor/_andor_io.py +2 -4
  29. ophyd_async/epics/adaravis/__init__.py +5 -0
  30. ophyd_async/epics/adaravis/_aravis.py +4 -8
  31. ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
  32. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  33. ophyd_async/epics/adcore/__init__.py +23 -8
  34. ophyd_async/epics/adcore/_core_detector.py +42 -2
  35. ophyd_async/epics/adcore/_core_io.py +124 -99
  36. ophyd_async/epics/adcore/_core_logic.py +106 -27
  37. ophyd_async/epics/adcore/_core_writer.py +12 -8
  38. ophyd_async/epics/adcore/_hdf_writer.py +21 -38
  39. ophyd_async/epics/adcore/_single_trigger.py +2 -2
  40. ophyd_async/epics/adcore/_utils.py +2 -2
  41. ophyd_async/epics/adkinetix/__init__.py +2 -1
  42. ophyd_async/epics/adkinetix/_kinetix.py +3 -3
  43. ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
  44. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  45. ophyd_async/epics/adpilatus/__init__.py +5 -0
  46. ophyd_async/epics/adpilatus/_pilatus.py +1 -1
  47. ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
  48. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  49. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  50. ophyd_async/epics/adsimdetector/_sim.py +4 -14
  51. ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
  52. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  53. ophyd_async/epics/advimba/__init__.py +10 -1
  54. ophyd_async/epics/advimba/_vimba.py +3 -2
  55. ophyd_async/epics/advimba/_vimba_controller.py +4 -2
  56. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  57. ophyd_async/epics/core/_aioca.py +35 -16
  58. ophyd_async/epics/core/_epics_connector.py +4 -0
  59. ophyd_async/epics/core/_epics_device.py +2 -0
  60. ophyd_async/epics/core/_p4p.py +10 -2
  61. ophyd_async/epics/core/_pvi_connector.py +65 -8
  62. ophyd_async/epics/core/_signal.py +51 -51
  63. ophyd_async/epics/core/_util.py +4 -4
  64. ophyd_async/epics/demo/__init__.py +16 -0
  65. ophyd_async/epics/demo/__main__.py +31 -0
  66. ophyd_async/epics/demo/_ioc.py +32 -0
  67. ophyd_async/epics/demo/_motor.py +82 -0
  68. ophyd_async/epics/demo/_point_detector.py +42 -0
  69. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  70. ophyd_async/epics/demo/_stage.py +15 -0
  71. ophyd_async/epics/{sim/mover.db → demo/motor.db} +2 -1
  72. ophyd_async/epics/demo/point_detector.db +59 -0
  73. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  74. ophyd_async/epics/eiger/_eiger.py +1 -3
  75. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  76. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  77. ophyd_async/epics/eiger/_odin_io.py +1 -2
  78. ophyd_async/epics/motor.py +65 -28
  79. ophyd_async/epics/signal.py +4 -1
  80. ophyd_async/epics/testing/_example_ioc.py +21 -9
  81. ophyd_async/epics/testing/_utils.py +3 -0
  82. ophyd_async/epics/testing/test_records.db +8 -0
  83. ophyd_async/epics/testing/test_records_pva.db +17 -16
  84. ophyd_async/fastcs/__init__.py +1 -0
  85. ophyd_async/fastcs/core.py +6 -0
  86. ophyd_async/fastcs/odin/__init__.py +1 -0
  87. ophyd_async/fastcs/panda/__init__.py +8 -6
  88. ophyd_async/fastcs/panda/_block.py +29 -9
  89. ophyd_async/fastcs/panda/_control.py +5 -0
  90. ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
  91. ophyd_async/fastcs/panda/_table.py +9 -6
  92. ophyd_async/fastcs/panda/_trigger.py +23 -9
  93. ophyd_async/fastcs/panda/_writer.py +27 -30
  94. ophyd_async/plan_stubs/__init__.py +2 -0
  95. ophyd_async/plan_stubs/_ensure_connected.py +1 -0
  96. ophyd_async/plan_stubs/_fly.py +2 -4
  97. ophyd_async/plan_stubs/_nd_attributes.py +2 -0
  98. ophyd_async/plan_stubs/_panda.py +1 -0
  99. ophyd_async/plan_stubs/_settings.py +43 -16
  100. ophyd_async/plan_stubs/_utils.py +3 -0
  101. ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
  102. ophyd_async/sim/__init__.py +24 -14
  103. ophyd_async/sim/__main__.py +43 -0
  104. ophyd_async/sim/_blob_detector.py +33 -0
  105. ophyd_async/sim/_blob_detector_controller.py +48 -0
  106. ophyd_async/sim/_blob_detector_writer.py +105 -0
  107. ophyd_async/sim/_mirror_horizontal.py +46 -0
  108. ophyd_async/sim/_mirror_vertical.py +74 -0
  109. ophyd_async/sim/_motor.py +233 -0
  110. ophyd_async/sim/_pattern_generator.py +124 -0
  111. ophyd_async/sim/_point_detector.py +86 -0
  112. ophyd_async/sim/_stage.py +19 -0
  113. ophyd_async/tango/__init__.py +1 -0
  114. ophyd_async/tango/core/__init__.py +6 -1
  115. ophyd_async/tango/core/_base_device.py +41 -33
  116. ophyd_async/tango/core/_converters.py +81 -0
  117. ophyd_async/tango/core/_signal.py +18 -32
  118. ophyd_async/tango/core/_tango_readable.py +2 -19
  119. ophyd_async/tango/core/_tango_transport.py +136 -60
  120. ophyd_async/tango/core/_utils.py +47 -0
  121. ophyd_async/tango/{sim → demo}/_counter.py +2 -0
  122. ophyd_async/tango/{sim → demo}/_detector.py +2 -0
  123. ophyd_async/tango/{sim → demo}/_mover.py +5 -4
  124. ophyd_async/tango/{sim → demo}/_tango/_servers.py +4 -0
  125. ophyd_async/tango/testing/__init__.py +6 -0
  126. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  127. ophyd_async/testing/__init__.py +29 -7
  128. ophyd_async/testing/_assert.py +145 -83
  129. ophyd_async/testing/_mock_signal_utils.py +56 -70
  130. ophyd_async/testing/_one_of_everything.py +41 -21
  131. ophyd_async/testing/_single_derived.py +89 -0
  132. ophyd_async/testing/_utils.py +3 -0
  133. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/METADATA +25 -26
  134. ophyd_async-0.10.0a2.dist-info/RECORD +149 -0
  135. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/WHEEL +1 -1
  136. ophyd_async/epics/sim/__init__.py +0 -54
  137. ophyd_async/epics/sim/_ioc.py +0 -29
  138. ophyd_async/epics/sim/_mover.py +0 -101
  139. ophyd_async/epics/sim/_sensor.py +0 -37
  140. ophyd_async/epics/sim/sensor.db +0 -19
  141. ophyd_async/sim/_pattern_detector/__init__.py +0 -13
  142. ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
  143. ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
  144. ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
  145. ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
  146. ophyd_async/sim/_sim_motor.py +0 -107
  147. ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
  148. /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
  149. /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
  150. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info/licenses}/LICENSE +0 -0
  151. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,300 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Awaitable, Callable, Mapping
5
+ from functools import cached_property
6
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, is_typeddict
7
+
8
+ from bluesky.protocols import Location, Reading, Subscribable
9
+ from event_model import DataKey
10
+ from pydantic import BaseModel
11
+
12
+ from ._protocol import AsyncLocatable, AsyncReadable
13
+ from ._signal_backend import SignalBackend, SignalDatatypeT, make_datakey, make_metadata
14
+ from ._utils import Callback, T, gather_dict, merge_gathered_dicts
15
+
16
+ RawT = TypeVar("RawT")
17
+ DerivedT = TypeVar("DerivedT")
18
+
19
+
20
+ class Transform(BaseModel, Generic[RawT, DerivedT]):
21
+ """Baseclass for bidirectional transforms for Derived Signals.
22
+
23
+ Subclass and add:
24
+ - type hinted parameters that should be fetched from Signals
25
+ - a raw_to_derived method that takes the elements of RawT and returns a DerivedT
26
+ - a derived_to_raw method that takes the elements of DerivedT and returns a RawT
27
+
28
+ :example:
29
+ ```python
30
+ class MyRaw(TypedDict):
31
+ raw1: float
32
+ raw2: float
33
+
34
+ class MyDerived(TypedDict):
35
+ derived1: float
36
+ derived2: float
37
+
38
+ class MyTransform(Transform):
39
+ param1: float
40
+
41
+ def raw_to_derived(self, *, raw1: float, raw2: float) -> MyDerived:
42
+ derived1, derived2 = some_maths(self.param1, raw1, raw2)
43
+ return MyDerived(derived1=derived1, derived2=derived2)
44
+
45
+ def derived_to_raw(self, *, derived1: float, derived2: float) -> MyRaw:
46
+ raw1, raw2 = some_inverse_maths(self.param1, derived1, derived2)
47
+ return MyRaw(raw1=raw1, raw2=raw2)
48
+ ```
49
+ """
50
+
51
+ if TYPE_CHECKING:
52
+ # Guard with if type checking so they don't appear in pydantic argument list
53
+ # Ideally they would be:
54
+ # def raw_to_derived(self, **kwargs: Unpack[RawT]) -> DerivedT: ...
55
+ # but TypedDicts are not valid as generics
56
+ # https://github.com/microsoft/pyright/discussions/7317
57
+ raw_to_derived: Callable[..., DerivedT]
58
+ derived_to_raw: Callable[..., RawT]
59
+
60
+
61
+ TransformT = TypeVar("TransformT", bound=Transform)
62
+
63
+
64
+ def filter_by_type(raw_devices: Mapping[str, Any], type_: type[T]) -> dict[str, T]:
65
+ filtered_devices: dict[str, T] = {}
66
+ for name, device in raw_devices.items():
67
+ if not isinstance(device, type_):
68
+ msg = f"{device} is not an instance of {type_}"
69
+ raise TypeError(msg)
70
+ filtered_devices[name] = device
71
+ return filtered_devices
72
+
73
+
74
+ class SignalTransformer(Generic[TransformT]):
75
+ def __init__(
76
+ self,
77
+ transform_cls: type[TransformT],
78
+ set_derived: Callable[..., Awaitable[None]] | None,
79
+ set_derived_datatype: type | None,
80
+ **raw_and_transform_devices,
81
+ ):
82
+ self._transform_cls = transform_cls
83
+ self._set_derived = set_derived
84
+ self._need_dict = is_typeddict(set_derived_datatype)
85
+ self._transform_devices = {
86
+ k: raw_and_transform_devices.pop(k) for k in transform_cls.model_fields
87
+ }
88
+ self._raw_devices = raw_and_transform_devices
89
+ self._derived_callbacks: dict[str, Callback[Reading]] = {}
90
+ self._cached_readings: dict[str, Reading] | None = None
91
+
92
+ @cached_property
93
+ def raw_locatables(self) -> dict[str, AsyncLocatable]:
94
+ return filter_by_type(self._raw_devices, AsyncLocatable)
95
+
96
+ @cached_property
97
+ def transform_readables(self) -> dict[str, AsyncReadable]:
98
+ return filter_by_type(self._transform_devices, AsyncReadable)
99
+
100
+ @cached_property
101
+ def raw_and_transform_readables(self) -> dict[str, AsyncReadable]:
102
+ return filter_by_type(
103
+ self._raw_devices | self._transform_devices, AsyncReadable
104
+ )
105
+
106
+ @cached_property
107
+ def raw_and_transform_subscribables(self) -> dict[str, Subscribable]:
108
+ return filter_by_type(self._raw_devices | self._transform_devices, Subscribable)
109
+
110
+ def _complete_cached_reading(self) -> dict[str, Reading] | None:
111
+ if self._cached_readings and len(self._cached_readings) == len(
112
+ self.raw_and_transform_subscribables
113
+ ):
114
+ return self._cached_readings
115
+ return None
116
+
117
+ def _make_transform_from_readings(
118
+ self, transform_readings: dict[str, Reading]
119
+ ) -> TransformT:
120
+ # Make the transform using the values from the readings for those args
121
+ transform_args = {
122
+ k: transform_readings[sig.name]["value"]
123
+ for k, sig in self.transform_readables.items()
124
+ }
125
+ return self._transform_cls(**transform_args)
126
+
127
+ def _make_derived_readings(
128
+ self, raw_and_transform_readings: dict[str, Reading]
129
+ ) -> dict[str, Reading]:
130
+ # Calculate the latest timestamp and max severity from them
131
+ timestamp = max(
132
+ raw_and_transform_readings[device.name]["timestamp"]
133
+ for device in self.raw_and_transform_subscribables.values()
134
+ )
135
+ alarm_severity = max(
136
+ raw_and_transform_readings[device.name].get("alarm_severity", 0)
137
+ for device in self.raw_and_transform_subscribables.values()
138
+ )
139
+ # Make the transform using the values from the readings for those args
140
+ transform = self._make_transform_from_readings(raw_and_transform_readings)
141
+ # Create the raw values from the rest then calculate the derived readings
142
+ # using the transform
143
+ raw_values = {
144
+ k: raw_and_transform_readings[sig.name]["value"]
145
+ for k, sig in self._raw_devices.items()
146
+ }
147
+ derived_readings = {
148
+ name: Reading(
149
+ value=derived, timestamp=timestamp, alarm_severity=alarm_severity
150
+ )
151
+ for name, derived in transform.raw_to_derived(**raw_values).items()
152
+ }
153
+ return derived_readings
154
+
155
+ async def get_transform(self) -> TransformT:
156
+ if raw_and_transform_readings := self._complete_cached_reading():
157
+ transform_readings = raw_and_transform_readings
158
+ else:
159
+ transform_readings = await merge_gathered_dicts(
160
+ device.read() for device in self.transform_readables.values()
161
+ )
162
+ return self._make_transform_from_readings(transform_readings)
163
+
164
+ async def get_derived_readings(self) -> dict[str, Reading]:
165
+ if not (raw_and_transform_readings := self._complete_cached_reading()):
166
+ raw_and_transform_readings = await merge_gathered_dicts(
167
+ device.read() for device in self.raw_and_transform_readables.values()
168
+ )
169
+ return self._make_derived_readings(raw_and_transform_readings)
170
+
171
+ async def get_derived_values(self) -> dict[str, Any]:
172
+ derived_readings = await self.get_derived_readings()
173
+ return {k: v["value"] for k, v in derived_readings.items()}
174
+
175
+ def _update_cached_reading(self, value: dict[str, Reading]):
176
+ if self._cached_readings is None:
177
+ msg = "Cannot update cached reading as it has not been initialised"
178
+ raise RuntimeError(msg)
179
+ self._cached_readings.update(value)
180
+ if self._complete_cached_reading():
181
+ # We've got a complete set of values, callback on them
182
+ derived_readings = self._make_derived_readings(self._cached_readings)
183
+ for name, callback in self._derived_callbacks.items():
184
+ callback(derived_readings[name])
185
+
186
+ def set_callback(self, name: str, callback: Callback[Reading] | None) -> None:
187
+ if callback is None:
188
+ self._derived_callbacks.pop(name, None)
189
+ if not self._derived_callbacks:
190
+ # Remove the callbacks to all the raw devices
191
+ for raw in self.raw_and_transform_subscribables.values():
192
+ raw.clear_sub(self._update_cached_reading)
193
+ # and clear the cached readings that will now be stale
194
+ self._cached_readings = None
195
+ else:
196
+ if name in self._derived_callbacks:
197
+ msg = f"Callback already set for {name}"
198
+ raise RuntimeError(msg)
199
+ self._derived_callbacks[name] = callback
200
+ if self._cached_readings is None:
201
+ # Add the callbacks to all the raw devices, this will run the first
202
+ # callback
203
+ self._cached_readings = {}
204
+ for raw in self.raw_and_transform_subscribables.values():
205
+ raw.subscribe(self._update_cached_reading)
206
+ elif self._complete_cached_reading():
207
+ # Callback on the last complete set of readings
208
+ derived_readings = self._make_derived_readings(self._cached_readings)
209
+ callback(derived_readings[name])
210
+
211
+ async def get_locations(self) -> dict[str, Location]:
212
+ locations, transform = await asyncio.gather(
213
+ gather_dict({k: sig.locate() for k, sig in self.raw_locatables.items()}),
214
+ self.get_transform(),
215
+ )
216
+ raw_setpoints = {k: v["setpoint"] for k, v in locations.items()}
217
+ raw_readbacks = {k: v["readback"] for k, v in locations.items()}
218
+ derived_setpoints = transform.raw_to_derived(**raw_setpoints)
219
+ derived_readbacks = transform.raw_to_derived(**raw_readbacks)
220
+ return {
221
+ name: Location(
222
+ setpoint=derived_setpoints[name],
223
+ readback=derived_readbacks[name],
224
+ )
225
+ for name in derived_setpoints
226
+ }
227
+
228
+ async def set_derived(self, name: str, value: Any):
229
+ if self._set_derived is None:
230
+ msg = "Cannot put as no set_derived method given"
231
+ raise RuntimeError(msg)
232
+ if self._need_dict:
233
+ # Need to get the other derived values and update the one that's changing
234
+ derived = await self.get_locations()
235
+ setpoints = {k: v["setpoint"] for k, v in derived.items()}
236
+ setpoints[name] = value
237
+ await self._set_derived(setpoints)
238
+ else:
239
+ # Only one derived signal, so pass it directly
240
+ await self._set_derived(value)
241
+
242
+
243
+ class DerivedSignalBackend(SignalBackend[SignalDatatypeT]):
244
+ def __init__(
245
+ self,
246
+ datatype: type[SignalDatatypeT],
247
+ name: str,
248
+ transformer: SignalTransformer,
249
+ units: str | None = None,
250
+ precision: int | None = None,
251
+ ):
252
+ self.name = name
253
+ self.transformer = transformer
254
+ # Add the extra static metadata to the dictionary
255
+ self.metadata = make_metadata(datatype, units, precision)
256
+ super().__init__(datatype)
257
+
258
+ def source(self, name: str, read: bool) -> str:
259
+ return f"derived://{name}"
260
+
261
+ async def connect(self, timeout: float):
262
+ # Assume that the underlying signals are already connected
263
+ pass
264
+
265
+ def set_value(self, value: SignalDatatypeT):
266
+ msg = (
267
+ "Cannot set the value of a derived signal, "
268
+ "set the underlying raw signals instead"
269
+ )
270
+ raise RuntimeError(msg)
271
+
272
+ async def put(self, value: SignalDatatypeT | None, wait: bool) -> None:
273
+ if wait is False:
274
+ msg = "Cannot put with wait=False"
275
+ raise RuntimeError(msg)
276
+ if value is None:
277
+ msg = "Must be given a value to put"
278
+ raise RuntimeError(msg)
279
+ await self.transformer.set_derived(self.name, value)
280
+
281
+ async def get_datakey(self, source: str) -> DataKey:
282
+ return make_datakey(
283
+ self.datatype or float, await self.get_value(), source, self.metadata
284
+ )
285
+
286
+ async def get_reading(self) -> Reading[SignalDatatypeT]:
287
+ readings = await self.transformer.get_derived_readings()
288
+ return readings[self.name]
289
+
290
+ async def get_value(self) -> SignalDatatypeT:
291
+ derived = await self.transformer.get_derived_values()
292
+ return derived[self.name]
293
+
294
+ async def get_setpoint(self) -> SignalDatatypeT:
295
+ # TODO: should be get_location
296
+ locations = await self.transformer.get_locations()
297
+ return locations[self.name]["setpoint"]
298
+
299
+ def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
300
+ self.transformer.set_callback(self.name, callback)