ophyd-async 0.8.0a6__py3-none-any.whl → 0.9.0a1__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 (51) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +2 -26
  3. ophyd_async/core/_detector.py +9 -9
  4. ophyd_async/core/_device.py +27 -8
  5. ophyd_async/core/_protocol.py +0 -28
  6. ophyd_async/core/_signal.py +38 -136
  7. ophyd_async/core/_utils.py +11 -2
  8. ophyd_async/epics/adaravis/_aravis_controller.py +8 -8
  9. ophyd_async/epics/adaravis/_aravis_io.py +4 -4
  10. ophyd_async/epics/adcore/_core_io.py +21 -21
  11. ophyd_async/epics/adcore/_core_logic.py +3 -2
  12. ophyd_async/epics/adcore/_hdf_writer.py +6 -3
  13. ophyd_async/epics/adcore/_single_trigger.py +1 -1
  14. ophyd_async/epics/adcore/_utils.py +35 -35
  15. ophyd_async/epics/adkinetix/_kinetix_controller.py +7 -7
  16. ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
  17. ophyd_async/epics/adpilatus/_pilatus.py +3 -3
  18. ophyd_async/epics/adpilatus/_pilatus_controller.py +4 -4
  19. ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
  20. ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
  21. ophyd_async/epics/advimba/_vimba_controller.py +14 -14
  22. ophyd_async/epics/advimba/_vimba_io.py +23 -23
  23. ophyd_async/epics/core/_p4p.py +19 -0
  24. ophyd_async/epics/core/_pvi_connector.py +4 -2
  25. ophyd_async/epics/core/_signal.py +9 -2
  26. ophyd_async/epics/core/_util.py +9 -0
  27. ophyd_async/epics/demo/_mover.py +2 -2
  28. ophyd_async/epics/demo/_sensor.py +2 -2
  29. ophyd_async/epics/eiger/_eiger_controller.py +4 -4
  30. ophyd_async/epics/eiger/_eiger_io.py +3 -3
  31. ophyd_async/epics/motor.py +8 -5
  32. ophyd_async/epics/testing/_example_ioc.py +5 -3
  33. ophyd_async/epics/testing/test_records.db +6 -0
  34. ophyd_async/fastcs/core.py +2 -2
  35. ophyd_async/fastcs/panda/_block.py +9 -9
  36. ophyd_async/fastcs/panda/_control.py +2 -2
  37. ophyd_async/fastcs/panda/_hdf_panda.py +4 -1
  38. ophyd_async/fastcs/panda/_trigger.py +7 -7
  39. ophyd_async/plan_stubs/_fly.py +1 -1
  40. ophyd_async/sim/demo/_sim_motor.py +34 -32
  41. ophyd_async/tango/core/_tango_transport.py +1 -1
  42. ophyd_async/testing/__init__.py +33 -0
  43. ophyd_async/testing/_assert.py +128 -0
  44. ophyd_async/{core → testing}/_mock_signal_utils.py +12 -8
  45. ophyd_async/testing/_wait_for_pending.py +22 -0
  46. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/METADATA +3 -1
  47. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/RECORD +51 -48
  48. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/LICENSE +0 -0
  49. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/WHEEL +0 -0
  50. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/entry_points.txt +0 -0
  51. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/top_level.txt +0 -0
@@ -2,8 +2,8 @@ from ophyd_async.core import Device, DeviceConnector
2
2
  from ophyd_async.epics.core import PviDeviceConnector
3
3
 
4
4
 
5
- def fastcs_connector(device: Device, uri: str) -> DeviceConnector:
5
+ def fastcs_connector(device: Device, uri: str, error_hint: str = "") -> DeviceConnector:
6
6
  # TODO: add Tango support based on uri scheme
7
- connector = PviDeviceConnector(uri)
7
+ connector = PviDeviceConnector(uri, error_hint)
8
8
  connector.create_children_from_annotations(device)
9
9
  return connector
@@ -36,14 +36,14 @@ class PulseBlock(Device):
36
36
 
37
37
 
38
38
  class PcompDirection(StrictEnum):
39
- positive = "Positive"
40
- negative = "Negative"
41
- either = "Either"
39
+ POSITIVE = "Positive"
40
+ NEGATIVE = "Negative"
41
+ EITHER = "Either"
42
42
 
43
43
 
44
44
  class BitMux(SubsetEnum):
45
- zero = "ZERO"
46
- one = "ONE"
45
+ ZERO = "ZERO"
46
+ ONE = "ONE"
47
47
 
48
48
 
49
49
  class PcompBlock(Device):
@@ -57,10 +57,10 @@ class PcompBlock(Device):
57
57
 
58
58
 
59
59
  class TimeUnits(StrictEnum):
60
- min = "min"
61
- s = "s"
62
- ms = "ms"
63
- us = "us"
60
+ MIN = "min"
61
+ S = "s"
62
+ MS = "ms"
63
+ US = "us"
64
64
 
65
65
 
66
66
  class SeqBlock(Device):
@@ -19,8 +19,8 @@ class PandaPcapController(DetectorController):
19
19
 
20
20
  async def prepare(self, trigger_info: TriggerInfo):
21
21
  assert trigger_info.trigger in (
22
- DetectorTrigger.constant_gate,
23
- DetectorTrigger.variable_gate,
22
+ DetectorTrigger.CONSTANT_GATE,
23
+ DetectorTrigger.VARIABLE_GATE,
24
24
  ), "Only constant_gate and variable_gate triggering is supported on the PandA"
25
25
 
26
26
  async def arm(self):
@@ -9,6 +9,8 @@ from ._block import CommonPandaBlocks
9
9
  from ._control import PandaPcapController
10
10
  from ._writer import PandaHDFWriter
11
11
 
12
+ MINIMUM_PANDA_IOC = "0.11.4"
13
+
12
14
 
13
15
  class HDFPanda(CommonPandaBlocks, StandardDetector):
14
16
  def __init__(
@@ -18,8 +20,9 @@ class HDFPanda(CommonPandaBlocks, StandardDetector):
18
20
  config_sigs: Sequence[SignalR] = (),
19
21
  name: str = "",
20
22
  ):
23
+ error_hint = f"Is PandABlocks-ioc at least version {MINIMUM_PANDA_IOC}?"
21
24
  # This has to be first so we make self.pcap
22
- connector = fastcs_connector(self, prefix)
25
+ connector = fastcs_connector(self, prefix, error_hint)
23
26
  controller = PandaPcapController(pcap=self.pcap)
24
27
  writer = PandaHDFWriter(
25
28
  path_provider=path_provider,
@@ -20,8 +20,8 @@ class StaticSeqTableTriggerLogic(FlyerController[SeqTableInfo]):
20
20
 
21
21
  async def prepare(self, value: SeqTableInfo):
22
22
  await asyncio.gather(
23
- self.seq.prescale_units.set(TimeUnits.us),
24
- self.seq.enable.set(BitMux.zero),
23
+ self.seq.prescale_units.set(TimeUnits.US),
24
+ self.seq.enable.set(BitMux.ZERO),
25
25
  )
26
26
  await asyncio.gather(
27
27
  self.seq.prescale.set(value.prescale_as_us),
@@ -30,14 +30,14 @@ class StaticSeqTableTriggerLogic(FlyerController[SeqTableInfo]):
30
30
  )
31
31
 
32
32
  async def kickoff(self) -> None:
33
- await self.seq.enable.set(BitMux.one)
33
+ await self.seq.enable.set(BitMux.ONE)
34
34
  await wait_for_value(self.seq.active, True, timeout=1)
35
35
 
36
36
  async def complete(self) -> None:
37
37
  await wait_for_value(self.seq.active, False, timeout=None)
38
38
 
39
39
  async def stop(self):
40
- await self.seq.enable.set(BitMux.zero)
40
+ await self.seq.enable.set(BitMux.ZERO)
41
41
  await wait_for_value(self.seq.active, False, timeout=1)
42
42
 
43
43
 
@@ -68,7 +68,7 @@ class StaticPcompTriggerLogic(FlyerController[PcompInfo]):
68
68
  self.pcomp = pcomp
69
69
 
70
70
  async def prepare(self, value: PcompInfo):
71
- await self.pcomp.enable.set(BitMux.zero)
71
+ await self.pcomp.enable.set(BitMux.ZERO)
72
72
  await asyncio.gather(
73
73
  self.pcomp.start.set(value.start_postion),
74
74
  self.pcomp.width.set(value.pulse_width),
@@ -78,12 +78,12 @@ class StaticPcompTriggerLogic(FlyerController[PcompInfo]):
78
78
  )
79
79
 
80
80
  async def kickoff(self) -> None:
81
- await self.pcomp.enable.set(BitMux.one)
81
+ await self.pcomp.enable.set(BitMux.ONE)
82
82
  await wait_for_value(self.pcomp.active, True, timeout=1)
83
83
 
84
84
  async def complete(self, timeout: float | None = None) -> None:
85
85
  await wait_for_value(self.pcomp.active, False, timeout=timeout)
86
86
 
87
87
  async def stop(self):
88
- await self.pcomp.enable.set(BitMux.zero)
88
+ await self.pcomp.enable.set(BitMux.ZERO)
89
89
  await wait_for_value(self.pcomp.active, False, timeout=1)
@@ -62,7 +62,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
62
62
 
63
63
  trigger_info = TriggerInfo(
64
64
  number_of_triggers=number_of_frames * repeats,
65
- trigger=DetectorTrigger.constant_gate,
65
+ trigger=DetectorTrigger.CONSTANT_GATE,
66
66
  deadtime=deadtime,
67
67
  livetime=exposure,
68
68
  frame_timeout=frame_timeout,
@@ -2,6 +2,7 @@ import asyncio
2
2
  import contextlib
3
3
  import time
4
4
 
5
+ import numpy as np
5
6
  from bluesky.protocols import Movable, Stoppable
6
7
 
7
8
  from ophyd_async.core import (
@@ -44,22 +45,20 @@ class SimMotor(StandardReadable, Movable, Stoppable):
44
45
 
45
46
  async def _move(self, old_position: float, new_position: float, move_time: float):
46
47
  start = time.monotonic()
47
- distance = abs(new_position - old_position)
48
- while True:
49
- time_elapsed = round(time.monotonic() - start, 2)
50
-
51
- # update position based on time elapsed
52
- if time_elapsed >= move_time:
53
- # successfully reached our target position
54
- self._user_readback_set(new_position)
55
- break
56
- else:
57
- current_position = old_position + distance * time_elapsed / move_time
58
-
59
- self._user_readback_set(current_position)
60
-
61
- # 10hz update loop
62
- await asyncio.sleep(0.1)
48
+ # Make an array of relative update times at 10Hz intervals
49
+ update_times = np.arange(0.1, move_time, 0.1)
50
+ # With the end position appended
51
+ update_times = np.concatenate((update_times, [move_time]))
52
+ # Interpolate the [old, new] position array with those update times
53
+ new_positions = np.interp(
54
+ update_times, [0, move_time], [old_position, new_position]
55
+ )
56
+ for update_time, new_position in zip(update_times, new_positions, strict=True):
57
+ # Calculate how long to wait to get there
58
+ relative_time = time.monotonic() - start
59
+ await asyncio.sleep(update_time - relative_time)
60
+ # Update the readback position
61
+ self._user_readback_set(new_position)
63
62
 
64
63
  @WatchableAsyncStatus.wrap
65
64
  async def set(self, value: float):
@@ -75,22 +74,25 @@ class SimMotor(StandardReadable, Movable, Stoppable):
75
74
  self.velocity.get_value(),
76
75
  )
77
76
  # If zero velocity, do instant move
78
- move_time = abs(new_position - old_position) / velocity if velocity else 0
79
- self._move_status = AsyncStatus(
80
- self._move(old_position, new_position, move_time)
81
- )
82
- # If stop is called then this will raise a CancelledError, ignore it
83
- with contextlib.suppress(asyncio.CancelledError):
84
- async for current_position in observe_value(
85
- self.user_readback, done_status=self._move_status
86
- ):
87
- yield WatcherUpdate(
88
- current=current_position,
89
- initial=old_position,
90
- target=new_position,
91
- name=self.name,
92
- unit=units,
93
- )
77
+ if velocity == 0:
78
+ self._user_readback_set(new_position)
79
+ else:
80
+ move_time = abs(new_position - old_position) / velocity
81
+ self._move_status = AsyncStatus(
82
+ self._move(old_position, new_position, move_time)
83
+ )
84
+ # If stop is called then this will raise a CancelledError, ignore it
85
+ with contextlib.suppress(asyncio.CancelledError):
86
+ async for current_position in observe_value(
87
+ self.user_readback, done_status=self._move_status
88
+ ):
89
+ yield WatcherUpdate(
90
+ current=current_position,
91
+ initial=old_position,
92
+ target=new_position,
93
+ name=self.name,
94
+ unit=units,
95
+ )
94
96
  if not self._set_success:
95
97
  raise RuntimeError("Motor was stopped")
96
98
 
@@ -189,7 +189,7 @@ class AttributeProxy(TangoProxy):
189
189
  raise TimeoutError(f"{self._name} attr put failed: Timeout") from te
190
190
  except DevFailed as de:
191
191
  raise RuntimeError(
192
- f"{self._name} device" f" failure: {de.args[0].desc}"
192
+ f"{self._name} device failure: {de.args[0].desc}"
193
193
  ) from de
194
194
 
195
195
  else:
@@ -0,0 +1,33 @@
1
+ from ._assert import (
2
+ assert_configuration,
3
+ assert_emitted,
4
+ assert_reading,
5
+ assert_value,
6
+ )
7
+ from ._mock_signal_utils import (
8
+ callback_on_mock_put,
9
+ get_mock,
10
+ get_mock_put,
11
+ mock_puts_blocked,
12
+ reset_mock_put_calls,
13
+ set_mock_put_proceeds,
14
+ set_mock_value,
15
+ set_mock_values,
16
+ )
17
+ from ._wait_for_pending import wait_for_pending_wakeups
18
+
19
+ __all__ = [
20
+ "assert_configuration",
21
+ "assert_emitted",
22
+ "assert_reading",
23
+ "assert_value",
24
+ "callback_on_mock_put",
25
+ "get_mock",
26
+ "get_mock_put",
27
+ "mock_puts_blocked",
28
+ "reset_mock_put_calls",
29
+ "set_mock_put_proceeds",
30
+ "set_mock_value",
31
+ "set_mock_values",
32
+ "wait_for_pending_wakeups",
33
+ ]
@@ -0,0 +1,128 @@
1
+ from collections.abc import Mapping
2
+ from typing import Any
3
+
4
+ from bluesky.protocols import Reading
5
+
6
+ from ophyd_async.core import AsyncConfigurable, AsyncReadable, SignalDatatypeT, SignalR
7
+
8
+
9
+ def _generate_assert_error_msg(name: str, expected_result, actual_result) -> str:
10
+ WARNING = "\033[93m"
11
+ FAIL = "\033[91m"
12
+ ENDC = "\033[0m"
13
+ return (
14
+ f"Expected {WARNING}{name}{ENDC} to produce"
15
+ + f"\n{FAIL}{expected_result}{ENDC}"
16
+ + f"\nbut actually got \n{FAIL}{actual_result}{ENDC}"
17
+ )
18
+
19
+
20
+ async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
21
+ """Assert a signal's value and compare it an expected signal.
22
+
23
+ Parameters
24
+ ----------
25
+ signal:
26
+ signal with get_value.
27
+ value:
28
+ The expected value from the signal.
29
+
30
+ Notes
31
+ -----
32
+ Example usage::
33
+ await assert_value(signal, value)
34
+
35
+ """
36
+ actual_value = await signal.get_value()
37
+ assert actual_value == value, _generate_assert_error_msg(
38
+ name=signal.name,
39
+ expected_result=value,
40
+ actual_result=actual_value,
41
+ )
42
+
43
+
44
+ async def assert_reading(
45
+ readable: AsyncReadable, expected_reading: Mapping[str, Reading]
46
+ ) -> None:
47
+ """Assert readings from readable.
48
+
49
+ Parameters
50
+ ----------
51
+ readable:
52
+ Callable with readable.read function that generate readings.
53
+
54
+ reading:
55
+ The expected readings from the readable.
56
+
57
+ Notes
58
+ -----
59
+ Example usage::
60
+ await assert_reading(readable, reading)
61
+
62
+ """
63
+ actual_reading = await readable.read()
64
+ assert expected_reading == actual_reading, _generate_assert_error_msg(
65
+ name=readable.name,
66
+ expected_result=expected_reading,
67
+ actual_result=actual_reading,
68
+ )
69
+
70
+
71
+ async def assert_configuration(
72
+ configurable: AsyncConfigurable,
73
+ configuration: Mapping[str, Reading],
74
+ ) -> None:
75
+ """Assert readings from Configurable.
76
+
77
+ Parameters
78
+ ----------
79
+ configurable:
80
+ Configurable with Configurable.read function that generate readings.
81
+
82
+ configuration:
83
+ The expected readings from configurable.
84
+
85
+ Notes
86
+ -----
87
+ Example usage::
88
+ await assert_configuration(configurable configuration)
89
+
90
+ """
91
+ actual_configurable = await configurable.read_configuration()
92
+ assert configuration == actual_configurable, _generate_assert_error_msg(
93
+ name=configurable.name,
94
+ expected_result=configuration,
95
+ actual_result=actual_configurable,
96
+ )
97
+
98
+
99
+ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
100
+ """Assert emitted document generated by running a Bluesky plan
101
+
102
+ Parameters
103
+ ----------
104
+ Doc:
105
+ A dictionary
106
+
107
+ numbers:
108
+ expected emission in kwarg from
109
+
110
+ Notes
111
+ -----
112
+ Example usage::
113
+ docs = defaultdict(list)
114
+ RE.subscribe(lambda name, doc: docs[name].append(doc))
115
+ RE(my_plan())
116
+ assert_emitted(docs, start=1, descriptor=1, event=1, stop=1)
117
+ """
118
+ assert list(docs) == list(numbers), _generate_assert_error_msg(
119
+ name="documents",
120
+ expected_result=list(numbers),
121
+ actual_result=list(docs),
122
+ )
123
+ actual_numbers = {name: len(d) for name, d in docs.items()}
124
+ assert actual_numbers == numbers, _generate_assert_error_msg(
125
+ name="emitted",
126
+ expected_result=numbers,
127
+ actual_result=actual_numbers,
128
+ )
@@ -1,12 +1,16 @@
1
1
  from collections.abc import Awaitable, Callable, Iterable
2
- from contextlib import asynccontextmanager, contextmanager
2
+ from contextlib import contextmanager
3
3
  from unittest.mock import AsyncMock, Mock
4
4
 
5
- from ._device import Device
6
- from ._mock_signal_backend import MockSignalBackend
7
- from ._signal import Signal, SignalConnector, SignalR
8
- from ._soft_signal_backend import SignalDatatypeT
9
- from ._utils import LazyMock
5
+ from ophyd_async.core import (
6
+ Device,
7
+ LazyMock,
8
+ MockSignalBackend,
9
+ Signal,
10
+ SignalConnector,
11
+ SignalDatatypeT,
12
+ SignalR,
13
+ )
10
14
 
11
15
 
12
16
  def get_mock(device: Device | Signal) -> Mock:
@@ -40,8 +44,8 @@ def set_mock_put_proceeds(signal: Signal, proceeds: bool):
40
44
  backend.put_proceeds.clear()
41
45
 
42
46
 
43
- @asynccontextmanager
44
- async def mock_puts_blocked(*signals: Signal):
47
+ @contextmanager
48
+ def mock_puts_blocked(*signals: Signal):
45
49
  for signal in signals:
46
50
  set_mock_put_proceeds(signal, False)
47
51
  yield
@@ -0,0 +1,22 @@
1
+ import asyncio
2
+
3
+
4
+ async def wait_for_pending_wakeups(max_yields=20, raise_if_exceeded=True):
5
+ """Allow any ready asyncio tasks to be woken up.
6
+
7
+ Used in:
8
+
9
+ - Tests to allow tasks like ``set()`` to start so that signal
10
+ puts can be tested
11
+ - `observe_value` to allow it to be wrapped in `asyncio.wait_for`
12
+ with a timeout
13
+ """
14
+ loop = asyncio.get_event_loop()
15
+ # If anything has called loop.call_soon or is scheduled a wakeup
16
+ # then let it run
17
+ for _ in range(max_yields):
18
+ await asyncio.sleep(0)
19
+ if not loop._ready: # type: ignore # noqa: SLF001
20
+ return
21
+ if raise_if_exceeded:
22
+ raise RuntimeError(f"Tasks still scheduling wakeups after {max_yields} yields")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ophyd-async
3
- Version: 0.8.0a6
3
+ Version: 0.9.0a1
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
@@ -37,6 +37,7 @@ Classifier: Development Status :: 3 - Alpha
37
37
  Classifier: License :: OSI Approved :: BSD License
38
38
  Classifier: Programming Language :: Python :: 3.10
39
39
  Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
40
41
  Requires-Python: >=3.10
41
42
  Description-Content-Type: text/markdown
42
43
  License-File: LICENSE
@@ -67,6 +68,7 @@ Requires-Dist: ophyd_async[tango]; extra == "dev"
67
68
  Requires-Dist: inflection; extra == "dev"
68
69
  Requires-Dist: ipython; extra == "dev"
69
70
  Requires-Dist: ipywidgets; extra == "dev"
71
+ Requires-Dist: import-linter; extra == "dev"
70
72
  Requires-Dist: matplotlib; extra == "dev"
71
73
  Requires-Dist: myst-parser; extra == "dev"
72
74
  Requires-Dist: numpydoc; extra == "dev"