ophyd-async 0.3a4__py3-none-any.whl → 0.3a5__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.
@@ -1,8 +1,8 @@
1
1
  from enum import Enum
2
- from typing import Callable, Dict, Literal, Optional, Tuple
2
+ from typing import Literal
3
3
 
4
4
  from ophyd_async.epics.areadetector.drivers import ADBase
5
- from ophyd_async.epics.signal.signal import epics_signal_r, epics_signal_rw_rbv
5
+ from ophyd_async.epics.signal.signal import epics_signal_rw_rbv
6
6
 
7
7
 
8
8
  class AravisTriggerMode(str, Enum):
@@ -22,113 +22,6 @@ class AravisTriggerMode(str, Enum):
22
22
  AravisTriggerSource = Literal["Freerun", "Line1", "Line2", "Line3", "Line4"]
23
23
 
24
24
 
25
- def _reverse_lookup(
26
- model_deadtimes: Dict[float, Tuple[str, ...]],
27
- ) -> Callable[[str], float]:
28
- def inner(pixel_format: str, model_name: str) -> float:
29
- for deadtime, pixel_formats in model_deadtimes.items():
30
- if pixel_format in pixel_formats:
31
- return deadtime
32
- raise ValueError(
33
- f"Model {model_name} does not have a defined deadtime "
34
- f"for pixel format {pixel_format}"
35
- )
36
-
37
- return inner
38
-
39
-
40
- _deadtimes: Dict[str, Callable[[str, str], float]] = {
41
- # cite: https://cdn.alliedvision.com/fileadmin/content/documents/products/cameras/Manta/techman/Manta_TechMan.pdf retrieved 2024-04-05 # noqa: E501
42
- "Manta G-125": lambda _, __: 63e-6,
43
- "Manta G-145": lambda _, __: 106e-6,
44
- "Manta G-235": _reverse_lookup(
45
- {
46
- 118e-6: (
47
- "Mono8",
48
- "Mono12Packed",
49
- "BayerRG8",
50
- "BayerRG12",
51
- "BayerRG12Packed",
52
- "YUV411Packed",
53
- ),
54
- 256e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
55
- 390e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
56
- }
57
- ),
58
- "Manta G-895": _reverse_lookup(
59
- {
60
- 404e-6: (
61
- "Mono8",
62
- "Mono12Packed",
63
- "BayerRG8",
64
- "BayerRG12Packed",
65
- "YUV411Packed",
66
- ),
67
- 542e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
68
- 822e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
69
- }
70
- ),
71
- "Manta G-2460": _reverse_lookup(
72
- {
73
- 979e-6: (
74
- "Mono8",
75
- "Mono12Packed",
76
- "BayerRG8",
77
- "BayerRG12Packed",
78
- "YUV411Packed",
79
- ),
80
- 1304e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
81
- 1961e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
82
- }
83
- ),
84
- # cite: https://cdn.alliedvision.com/fileadmin/content/documents/products/cameras/various/appnote/GigE/GigE-Cameras_AppNote_PIV-Min-Time-Between-Exposures.pdf retrieved 2024-04-05 # noqa: E501
85
- "Manta G-609": lambda _, __: 47e-6,
86
- # cite: https://cdn.alliedvision.com/fileadmin/content/documents/products/cameras/Mako/techman/Mako_TechMan_en.pdf retrieved 2024-04-05 # noqa: E501
87
- "Mako G-040": _reverse_lookup(
88
- {
89
- 101e-6: (
90
- "Mono8",
91
- "Mono12Packed",
92
- "BayerRG8",
93
- "BayerRG12Packed",
94
- "YUV411Packed",
95
- ),
96
- 140e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
97
- 217e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
98
- }
99
- ),
100
- "Mako G-125": lambda _, __: 70e-6,
101
- # Assume 12 bits: 10 bits = 275e-6
102
- "Mako G-234": _reverse_lookup(
103
- {
104
- 356e-6: (
105
- "Mono8",
106
- "BayerRG8",
107
- "BayerRG12",
108
- "BayerRG12Packed",
109
- "YUV411Packed",
110
- "YUV422Packed",
111
- ),
112
- # Assume 12 bits: 10 bits = 563e-6
113
- 726e-6: ("RGB8Packed", "BRG8Packed", "YUV444Packed"),
114
- }
115
- ),
116
- "Mako G-507": _reverse_lookup(
117
- {
118
- 270e-6: (
119
- "Mono8",
120
- "Mono12Packed",
121
- "BayerRG8",
122
- "BayerRG12Packed",
123
- "YUV411Packed",
124
- ),
125
- 363e-6: ("Mono12", "BayerRG12", "YUV422Packed"),
126
- 554e-6: ("RGB8Packed", "BGR8Packed", "YUV444Packed"),
127
- }
128
- ),
129
- }
130
-
131
-
132
25
  class AravisDriver(ADBase):
133
26
  # If instantiating a new instance, ensure it is supported in the _deadtimes dict
134
27
  """Generic Driver supporting the Manta and Mako drivers.
@@ -142,15 +35,4 @@ class AravisDriver(ADBase):
142
35
  AravisTriggerMode, prefix + "TriggerMode"
143
36
  )
144
37
  self.trigger_source = epics_signal_rw_rbv(str, prefix + "TriggerSource")
145
- self.model = epics_signal_r(str, prefix + "Model_RBV")
146
- self.pixel_format = epics_signal_rw_rbv(str, prefix + "PixelFormat")
147
- self.dead_time: Optional[float] = None
148
38
  super().__init__(prefix, name=name)
149
-
150
- async def fetch_deadtime(self) -> None:
151
- # All known in-use version B/C have same deadtime as non-B/C
152
- model: str = (await self.model.get_value()).removesuffix("B").removesuffix("C")
153
- if model not in _deadtimes:
154
- raise ValueError(f"Model {model} does not have defined deadtimes")
155
- pixel_format: str = await self.pixel_format.get_value()
156
- self.dead_time = _deadtimes.get(model)(pixel_format, model)
@@ -6,23 +6,23 @@ import random
6
6
  import string
7
7
  import subprocess
8
8
  import sys
9
- import time
9
+ from dataclasses import replace
10
10
  from enum import Enum
11
11
  from pathlib import Path
12
- from typing import Callable, List, Optional
13
12
 
14
13
  import numpy as np
15
14
  from bluesky.protocols import Movable, Stoppable
16
15
 
17
16
  from ophyd_async.core import (
18
- AsyncStatus,
19
17
  ConfigSignal,
20
18
  Device,
21
19
  DeviceVector,
22
20
  HintedSignal,
23
21
  StandardReadable,
22
+ WatchableAsyncStatus,
24
23
  observe_value,
25
24
  )
25
+ from ophyd_async.core.utils import WatcherUpdate
26
26
 
27
27
  from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x
28
28
 
@@ -85,46 +85,41 @@ class Mover(StandardReadable, Movable, Stoppable):
85
85
  # Readback should be named the same as its parent in read()
86
86
  self.readback.set_name(name)
87
87
 
88
- async def _move(self, new_position: float, watchers: List[Callable] = []):
88
+ async def _move(self, new_position: float):
89
89
  self._set_success = True
90
90
  # time.monotonic won't go backwards in case of NTP corrections
91
- start = time.monotonic()
92
91
  old_position, units, precision = await asyncio.gather(
93
92
  self.setpoint.get_value(),
94
93
  self.units.get_value(),
95
94
  self.precision.get_value(),
96
95
  )
97
96
  # Wait for the value to set, but don't wait for put completion callback
98
- await self.setpoint.set(new_position, wait=False)
99
- async for current_position in observe_value(self.readback):
100
- for watcher in watchers:
101
- watcher(
102
- name=self.name,
103
- current=current_position,
104
- initial=old_position,
105
- target=new_position,
106
- unit=units,
107
- precision=precision,
108
- time_elapsed=time.monotonic() - start,
109
- )
110
- if np.isclose(current_position, new_position):
111
- break
97
+ move_status = self.setpoint.set(new_position, wait=True)
112
98
  if not self._set_success:
113
99
  raise RuntimeError("Motor was stopped")
100
+ # return a template to set() which it can use to yield progress updates
101
+ return (
102
+ WatcherUpdate(
103
+ initial=old_position,
104
+ current=old_position,
105
+ target=new_position,
106
+ unit=units,
107
+ precision=precision,
108
+ ),
109
+ move_status,
110
+ )
114
111
 
115
- def move(self, new_position: float, timeout: Optional[float] = None):
116
- """Commandline only synchronous move of a Motor"""
117
- from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
118
-
119
- if in_bluesky_event_loop():
120
- raise RuntimeError("Will deadlock run engine if run in a plan")
121
- call_in_bluesky_event_loop(self._move(new_position), timeout) # type: ignore
122
-
123
- # TODO: this fails if we call from the cli, but works if we "ipython await" it
124
- def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus:
125
- watchers: List[Callable] = []
126
- coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout)
127
- return AsyncStatus(coro, watchers)
112
+ @WatchableAsyncStatus.wrap # uses the timeout argument from the function it wraps
113
+ async def set(self, new_position: float, timeout: float | None = None):
114
+ update, _ = await self._move(new_position)
115
+ async for current_position in observe_value(self.readback):
116
+ yield replace(
117
+ update,
118
+ name=self.name,
119
+ current=current_position,
120
+ )
121
+ if np.isclose(current_position, new_position):
122
+ break
128
123
 
129
124
  async def stop(self, success=True):
130
125
  self._set_success = success
@@ -1,10 +1,17 @@
1
1
  import asyncio
2
- import time
3
- from typing import Callable, List, Optional
2
+ from dataclasses import replace
4
3
 
5
4
  from bluesky.protocols import Movable, Stoppable
6
5
 
7
- from ophyd_async.core import AsyncStatus, ConfigSignal, HintedSignal, StandardReadable
6
+ from ophyd_async.core import (
7
+ AsyncStatus,
8
+ ConfigSignal,
9
+ HintedSignal,
10
+ StandardReadable,
11
+ WatchableAsyncStatus,
12
+ )
13
+ from ophyd_async.core.signal import observe_value
14
+ from ophyd_async.core.utils import WatcherUpdate
8
15
 
9
16
  from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x
10
17
 
@@ -41,54 +48,46 @@ class Motor(StandardReadable, Movable, Stoppable):
41
48
  self.user_readback.set_name(name)
42
49
 
43
50
  async def _move(
44
- self, new_position: float, watchers: Optional[List[Callable]] = None
45
- ):
46
- if watchers is None:
47
- watchers = []
51
+ self, new_position: float
52
+ ) -> tuple[WatcherUpdate[float], AsyncStatus]:
48
53
  self._set_success = True
49
- start = time.monotonic()
50
54
  old_position, units, precision = await asyncio.gather(
51
55
  self.user_setpoint.get_value(),
52
56
  self.motor_egu.get_value(),
53
57
  self.precision.get_value(),
54
58
  )
55
-
56
- def update_watchers(current_position: float):
57
- for watcher in watchers:
58
- watcher(
59
- name=self.name,
60
- current=current_position,
61
- initial=old_position,
62
- target=new_position,
63
- unit=units,
64
- precision=precision,
65
- time_elapsed=time.monotonic() - start,
66
- )
67
-
68
- self.user_readback.subscribe_value(update_watchers)
69
- try:
70
- await self.user_setpoint.set(new_position)
71
- finally:
72
- self.user_readback.clear_sub(update_watchers)
59
+ move_status = self.user_setpoint.set(new_position, wait=True)
73
60
  if not self._set_success:
74
61
  raise RuntimeError("Motor was stopped")
62
+ return (
63
+ WatcherUpdate(
64
+ initial=old_position,
65
+ current=old_position,
66
+ target=new_position,
67
+ unit=units,
68
+ precision=precision,
69
+ ),
70
+ move_status,
71
+ )
75
72
 
76
- def move(self, new_position: float, timeout: Optional[float] = None):
77
- """Commandline only synchronous move of a Motor"""
78
- from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
79
-
80
- if in_bluesky_event_loop():
81
- raise RuntimeError("Will deadlock run engine if run in a plan")
82
- call_in_bluesky_event_loop(self._move(new_position), timeout) # type: ignore
83
-
84
- def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus:
85
- watchers: List[Callable] = []
86
- coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout)
87
- return AsyncStatus(coro, watchers)
73
+ @WatchableAsyncStatus.wrap
74
+ async def set(self, new_position: float, timeout: float | None = None):
75
+ update, move_status = await self._move(new_position)
76
+ async for current_position in observe_value(
77
+ self.user_readback, done_status=move_status
78
+ ):
79
+ if not self._set_success:
80
+ raise RuntimeError("Motor was stopped")
81
+ yield replace(
82
+ update,
83
+ name=self.name,
84
+ current=current_position,
85
+ )
88
86
 
89
87
  async def stop(self, success=False):
90
88
  self._set_success = success
91
89
  # Put with completion will never complete as we are waiting for completion on
92
90
  # the move above, so need to pass wait=False
93
- status = self.motor_stop.trigger(wait=False)
94
- await status
91
+ await self.motor_stop.trigger(wait=False)
92
+ # Trigger any callbacks
93
+ await self.user_readback._backend.put(await self.user_readback.get_value())
@@ -91,7 +91,7 @@ def _verify_common_blocks(entry: PVIEntry, common_device: Type[Device]):
91
91
  return
92
92
  common_sub_devices = get_type_hints(common_device)
93
93
  for sub_name, sub_device in common_sub_devices.items():
94
- if sub_name in ("_name", "parent"):
94
+ if sub_name.startswith("_") or sub_name == "parent":
95
95
  continue
96
96
  assert entry.sub_entries
97
97
  device_t, is_optional = _strip_union(sub_device)
@@ -161,7 +161,7 @@ def _mock_common_blocks(device: Device, stripped_type: Optional[Type] = None):
161
161
  sub_devices = (
162
162
  (field, field_type)
163
163
  for field, field_type in get_type_hints(device_t).items()
164
- if field not in ("_name", "parent")
164
+ if not field.startswith("_") and field != "parent"
165
165
  )
166
166
 
167
167
  for device_name, device_cls in sub_devices:
@@ -1,8 +1,15 @@
1
- from .signal import epics_signal_r, epics_signal_rw, epics_signal_w, epics_signal_x
1
+ from .signal import (
2
+ epics_signal_r,
3
+ epics_signal_rw,
4
+ epics_signal_rw_rbv,
5
+ epics_signal_w,
6
+ epics_signal_x,
7
+ )
2
8
 
3
9
  __all__ = [
4
10
  "epics_signal_r",
5
11
  "epics_signal_rw",
12
+ "epics_signal_rw_rbv",
6
13
  "epics_signal_w",
7
14
  "epics_signal_x",
8
15
  ]
@@ -1,5 +1,9 @@
1
+ from .ensure_connected import ensure_connected
1
2
  from .prepare_trigger_and_dets import (
2
3
  prepare_static_seq_table_flyer_and_detectors_with_same_trigger,
3
4
  )
4
5
 
5
- __all__ = ["prepare_static_seq_table_flyer_and_detectors_with_same_trigger"]
6
+ __all__ = [
7
+ "prepare_static_seq_table_flyer_and_detectors_with_same_trigger",
8
+ "ensure_connected",
9
+ ]
@@ -0,0 +1,22 @@
1
+ import bluesky.plan_stubs as bps
2
+
3
+ from ophyd_async.core.device import Device
4
+ from ophyd_async.core.utils import DEFAULT_TIMEOUT, wait_for_connection
5
+
6
+
7
+ def ensure_connected(
8
+ *devices: Device,
9
+ mock: bool = False,
10
+ timeout: float = DEFAULT_TIMEOUT,
11
+ force_reconnect=False,
12
+ ):
13
+ yield from bps.wait_for(
14
+ [
15
+ lambda: wait_for_connection(
16
+ **{
17
+ device.name: device.connect(mock, timeout, force_reconnect)
18
+ for device in devices
19
+ }
20
+ )
21
+ ]
22
+ )
ophyd_async/protocols.py CHANGED
@@ -1,9 +1,20 @@
1
+ from __future__ import annotations
2
+
1
3
  from abc import abstractmethod
2
- from typing import Dict, Protocol, runtime_checkable
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ Dict,
8
+ Generic,
9
+ Protocol,
10
+ TypeVar,
11
+ runtime_checkable,
12
+ )
3
13
 
4
14
  from bluesky.protocols import DataKey, HasName, Reading
5
15
 
6
- from ophyd_async.core.async_status import AsyncStatus
16
+ if TYPE_CHECKING:
17
+ from ophyd_async.core.async_status import AsyncStatus
7
18
 
8
19
 
9
20
  @runtime_checkable
@@ -94,3 +105,22 @@ class AsyncStageable(Protocol):
94
105
  unstaging.
95
106
  """
96
107
  ...
108
+
109
+
110
+ C = TypeVar("C", contravariant=True)
111
+
112
+
113
+ class Watcher(Protocol, Generic[C]):
114
+ @staticmethod
115
+ def __call__(
116
+ *,
117
+ current: C,
118
+ initial: C,
119
+ target: C,
120
+ name: str | None,
121
+ unit: str | None,
122
+ precision: float | None,
123
+ fraction: float | None,
124
+ time_elapsed: float | None,
125
+ time_remaining: float | None,
126
+ ) -> Any: ...
@@ -1,13 +1,18 @@
1
1
  import asyncio
2
2
  import time
3
- from typing import Callable, List, Optional
3
+ from dataclasses import replace
4
4
 
5
5
  from bluesky.protocols import Movable, Stoppable
6
6
 
7
7
  from ophyd_async.core import StandardReadable
8
- from ophyd_async.core.async_status import AsyncStatus
9
- from ophyd_async.core.signal import soft_signal_r_and_setter, soft_signal_rw
8
+ from ophyd_async.core.async_status import AsyncStatus, WatchableAsyncStatus
9
+ from ophyd_async.core.signal import (
10
+ observe_value,
11
+ soft_signal_r_and_setter,
12
+ soft_signal_rw,
13
+ )
10
14
  from ophyd_async.core.standard_readable import ConfigSignal, HintedSignal
15
+ from ophyd_async.core.utils import WatcherUpdate
11
16
 
12
17
 
13
18
  class SimMotor(StandardReadable, Movable, Stoppable):
@@ -27,10 +32,10 @@ class SimMotor(StandardReadable, Movable, Stoppable):
27
32
 
28
33
  with self.add_children_as_readables(ConfigSignal):
29
34
  self.velocity = soft_signal_rw(float, 1.0)
30
- self.egu = soft_signal_rw(float, "mm")
35
+ self.egu = soft_signal_rw(str, "mm")
31
36
 
32
37
  self._instant = instant
33
- self._move_task: Optional[asyncio.Task] = None
38
+ self._move_status: AsyncStatus | None = None
34
39
 
35
40
  # Define some signals
36
41
  self.user_setpoint = soft_signal_rw(float, 0)
@@ -44,21 +49,37 @@ class SimMotor(StandardReadable, Movable, Stoppable):
44
49
  """
45
50
  Stop the motor if it is moving
46
51
  """
47
- if self._move_task:
48
- self._move_task.cancel()
49
- self._move_task = None
52
+ if self._move_status:
53
+ self._move_status.task.cancel()
54
+ self._move_status = None
55
+
56
+ async def trigger_callbacks():
57
+ await self.user_readback._backend.put(
58
+ await self.user_readback._backend.get_value()
59
+ )
60
+
61
+ asyncio.create_task(trigger_callbacks())
50
62
 
51
63
  self._set_success = success
52
64
 
53
- def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus: # noqa: F821
65
+ @WatchableAsyncStatus.wrap
66
+ async def set(self, new_position: float, timeout: float | None = None):
54
67
  """
55
68
  Asynchronously move the motor to a new position.
56
69
  """
57
- watchers: List[Callable] = []
58
- coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout)
59
- return AsyncStatus(coro, watchers)
70
+ update, move_status = await self._move(new_position, timeout)
71
+ async for current_position in observe_value(
72
+ self.user_readback, done_status=move_status
73
+ ):
74
+ if not self._set_success:
75
+ raise RuntimeError("Motor was stopped")
76
+ yield replace(
77
+ update,
78
+ name=self.name,
79
+ current=current_position,
80
+ )
60
81
 
61
- async def _move(self, new_position: float, watchers: List[Callable] = []):
82
+ async def _move(self, new_position: float, timeout: float | None = None):
62
83
  """
63
84
  Start the motor moving to a new position.
64
85
 
@@ -67,6 +88,7 @@ class SimMotor(StandardReadable, Movable, Stoppable):
67
88
  """
68
89
  self.stop()
69
90
  start = time.monotonic()
91
+ self._set_success = True
70
92
 
71
93
  current_position = await self.user_readback.get_value()
72
94
  distance = abs(new_position - current_position)
@@ -94,25 +116,18 @@ class SimMotor(StandardReadable, Movable, Stoppable):
94
116
 
95
117
  self._user_readback_set(current_position)
96
118
 
97
- # notify watchers of the new position
98
- for watcher in watchers:
99
- watcher(
100
- name=self.name,
101
- current=current_position,
102
- initial=old_position,
103
- target=new_position,
104
- unit=units,
105
- time_elapsed=time.monotonic() - start,
106
- )
107
-
108
119
  # 10hz update loop
109
120
  await asyncio.sleep(0.1)
110
121
 
111
- # set up a task that updates the motor position at 10hz
112
- self._move_task = asyncio.create_task(update_position())
113
-
114
- try:
115
- await self._move_task
116
- finally:
117
- if not self._set_success:
118
- raise RuntimeError("Motor was stopped")
122
+ # set up a task that updates the motor position at ~10hz
123
+ self._move_status = AsyncStatus(asyncio.wait_for(update_position(), timeout))
124
+
125
+ return (
126
+ WatcherUpdate(
127
+ initial=old_position,
128
+ current=old_position,
129
+ target=new_position,
130
+ unit=units,
131
+ ),
132
+ self._move_status,
133
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ophyd-async
3
- Version: 0.3a4
3
+ Version: 0.3a5
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