Skip to content

Commit

Permalink
Add create_soft_signal_r and creates_soft_signal_rw methods (#217)
Browse files Browse the repository at this point in the history
* Add create_soft_signal methods to signal

* rename to soft_signal_r/rw and return backend with soft_signal_r

* Rename soft_signal_r to soft_signal_r_and_backend

* Allow soft_signal methods to set initial value of SimSignalBackend

* Make backend source a method.

signals can pass a value to the backend, signal sources are now properties

Can no longer pass source to SimSignalBackend at init

* Support numpy floating and integer dtypes in SimConverter

test numpy dtype with SimSignalBackend

---------

Co-authored-by: James Souter <hqv85942@diamtl20.diamond.ac.uk>
  • Loading branch information
jsouter and James Souter authored Apr 19, 2024
1 parent f294582 commit 4f58a41
Show file tree
Hide file tree
Showing 21 changed files with 160 additions and 95 deletions.
4 changes: 4 additions & 0 deletions src/ophyd_async/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
set_sim_callback,
set_sim_put_proceeds,
set_sim_value,
soft_signal_r_and_backend,
soft_signal_rw,
wait_for_value,
)
from .signal_backend import SignalBackend
Expand Down Expand Up @@ -67,6 +69,8 @@
"SignalW",
"SignalRW",
"SignalX",
"soft_signal_r_and_backend",
"soft_signal_rw",
"observe_value",
"set_and_wait_for_value",
"set_sim_callback",
Expand Down
38 changes: 32 additions & 6 deletions src/ophyd_async/core/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import asyncio
import functools
from typing import AsyncGenerator, Callable, Dict, Generic, Optional, Union
from typing import AsyncGenerator, Callable, Dict, Generic, Optional, Tuple, Type, Union

from bluesky.protocols import (
Descriptor,
Expand Down Expand Up @@ -60,9 +60,7 @@ def set_name(self, name: str = ""):

async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT):
if sim:
self._backend = SimSignalBackend(
datatype=self._init_backend.datatype, source=self._init_backend.source
)
self._backend = SimSignalBackend(datatype=self._init_backend.datatype)
_sim_backends[self] = self._backend
else:
self._backend = self._init_backend
Expand All @@ -72,7 +70,7 @@ async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT):
@property
def source(self) -> str:
"""Like ca://PV_PREFIX:SIGNAL, or "" if not set"""
return self._backend.source
return self._backend.source(self.name)

__lt__ = __le__ = __eq__ = __ge__ = __gt__ = __ne__ = _fail

Expand Down Expand Up @@ -168,7 +166,7 @@ async def read(self, cached: Optional[bool] = None) -> Dict[str, Reading]:
@_add_timeout
async def describe(self) -> Dict[str, Descriptor]:
"""Return a single item dict with the descriptor in it"""
return {self.name: await self._backend.get_descriptor()}
return {self.name: await self._backend.get_descriptor(self.source)}

@_add_timeout
async def get_value(self, cached: Optional[bool] = None) -> T:
Expand Down Expand Up @@ -253,6 +251,34 @@ def set_sim_callback(signal: Signal[T], callback: ReadingValueCallback[T]) -> No
return _sim_backends[signal].set_callback(callback)


def soft_signal_rw(
datatype: Optional[Type[T]] = None,
initial_value: Optional[T] = None,
name: Optional[str] = None,
) -> SignalRW[T]:
"""Creates a read-writable Signal with a SimSignalBackend"""
signal = SignalRW(SimSignalBackend(datatype, initial_value))
if name is not None:
signal.set_name(name)
return signal


def soft_signal_r_and_backend(
datatype: Optional[Type[T]] = None,
initial_value: Optional[T] = None,
name: Optional[str] = None,
) -> Tuple[SignalR[T], SimSignalBackend]:
"""Returns a tuple of a read-only Signal and its SimSignalBackend through
which the signal can be internally modified within the device. Use
soft_signal_rw if you want a device that is externally modifiable
"""
backend = SimSignalBackend(datatype, initial_value)
signal = SignalR(backend)
if name is not None:
signal.set_name(name)
return (signal, backend)


async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, None]:
"""Subscribe to the value of a signal so it can be iterated from.
Expand Down
7 changes: 5 additions & 2 deletions src/ophyd_async/core/signal_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ class SignalBackend(Generic[T]):
datatype: Optional[Type[T]] = None

#: Like ca://PV_PREFIX:SIGNAL
source: str = ""
@abstractmethod
def source(name: str) -> str:
"""Return source of signal. Signals may pass a name to the backend, which can be
used or discarded."""

@abstractmethod
async def connect(self, timeout: float = DEFAULT_TIMEOUT):
Expand All @@ -24,7 +27,7 @@ async def put(self, value: Optional[T], wait=True, timeout=None):
"""Put a value to the PV, if wait then wait for completion for up to timeout"""

@abstractmethod
async def get_descriptor(self) -> Descriptor:
async def get_descriptor(self, source: str) -> Descriptor:
"""Metadata like source, dtype, shape, precision, units"""

@abstractmethod
Expand Down
38 changes: 26 additions & 12 deletions src/ophyd_async/core/sim_signal_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import asyncio
import inspect
import re
import time
from collections import abc
from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, Generic, Optional, Type, Union, cast, get_origin

import numpy as np
from bluesky.protocols import Descriptor, Dtype, Reading

from .signal_backend import SignalBackend
Expand Down Expand Up @@ -37,11 +37,16 @@ def reading(self, value: T, timestamp: float, severity: int) -> Reading:
)

def descriptor(self, source: str, value) -> Descriptor:
dtype = type(value)
if np.issubdtype(dtype, np.integer):
dtype = int
elif np.issubdtype(dtype, np.floating):
dtype = float
assert (
type(value) in primitive_dtypes
dtype in primitive_dtypes
), f"invalid converter for value of type {type(value)}"
dtype = primitive_dtypes[type(value)]
return {"source": source, "dtype": dtype, "shape": []}
dtype_name = primitive_dtypes[dtype]
return {"source": source, "dtype": dtype_name, "shape": []}

def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
if datatype is None:
Expand Down Expand Up @@ -107,23 +112,32 @@ class SimSignalBackend(SignalBackend[T]):
"""An simulated backend to a Signal, created with ``Signal.connect(sim=True)``"""

_value: T
_initial_value: T
_initial_value: Optional[T]
_timestamp: float
_severity: int

def __init__(self, datatype: Optional[Type[T]], source: str) -> None:
pv = re.split(r"://", source)[-1]
self.source = f"sim://{pv}"
def __init__(
self,
datatype: Optional[Type[T]],
initial_value: Optional[T] = None,
) -> None:
self.datatype = datatype
self.pv = source
self.converter: SimConverter = DisconnectedSimConverter()
self._initial_value = initial_value
self.put_proceeds = asyncio.Event()
self.put_proceeds.set()
self.callback: Optional[ReadingValueCallback[T]] = None

def source(self, name: str) -> str:
return f"soft://{name}"

async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
self.converter = make_converter(self.datatype)
self._initial_value = self.converter.make_initial_value(self.datatype)
if self._initial_value is None:
self._initial_value = self.converter.make_initial_value(self.datatype)
else:
# convert potentially unconverted initial value passed to init method
self._initial_value = self.converter.write_value(self._initial_value)
self._severity = 0

await self.put(None)
Expand All @@ -150,8 +164,8 @@ def _set_value(self, value: T):
if self.callback:
self.callback(reading, self._value)

async def get_descriptor(self) -> Descriptor:
return self.converter.descriptor(self.source, self._value)
async def get_descriptor(self, source: str) -> Descriptor:
return self.converter.descriptor(source, self._value)

async def get_reading(self) -> Reading:
return self.converter.reading(self._value, self._timestamp, self._severity)
Expand Down
8 changes: 5 additions & 3 deletions src/ophyd_async/epics/_backend/_aioca.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,11 @@ def __init__(self, datatype: Optional[Type[T]], read_pv: str, write_pv: str):
self.write_pv = write_pv
self.initial_values: Dict[str, AugmentedValue] = {}
self.converter: CaConverter = DisconnectedCaConverter(None, None)
self.source = f"ca://{self.read_pv}"
self.subscription: Optional[Subscription] = None

def source(self, name: str):
return f"ca://{self.read_pv}"

async def _store_initial_value(self, pv, timeout: float = DEFAULT_TIMEOUT):
try:
self.initial_values[pv] = await caget(
Expand Down Expand Up @@ -216,9 +218,9 @@ async def _caget(self, format: Format) -> AugmentedValue:
timeout=None,
)

async def get_descriptor(self) -> Descriptor:
async def get_descriptor(self, source: str) -> Descriptor:
value = await self._caget(FORMAT_CTRL)
return self.converter.descriptor(self.source, value)
return self.converter.descriptor(source, value)

async def get_reading(self) -> Reading:
value = await self._caget(FORMAT_TIME)
Expand Down
9 changes: 6 additions & 3 deletions src/ophyd_async/epics/_backend/_p4p.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,12 @@ def __init__(self, datatype: Optional[Type[T]], read_pv: str, write_pv: str):
self.write_pv = write_pv
self.initial_values: Dict[str, Any] = {}
self.converter: PvaConverter = DisconnectedPvaConverter()
self.source = f"pva://{self.read_pv}"
self.subscription: Optional[Subscription] = None

@property
def source(self, name: str):
return f"pva://{self.read_pv}"

@property
def ctxt(self) -> Context:
if PvaSignalBackend._ctxt is None:
Expand Down Expand Up @@ -290,9 +293,9 @@ async def put(self, value: Optional[T], wait=True, timeout=None):
)
raise NotConnected(f"pva://{self.write_pv}") from exc

async def get_descriptor(self) -> Descriptor:
async def get_descriptor(self, source: str) -> Descriptor:
value = await self.ctxt.get(self.read_pv)
return self.converter.descriptor(self.source, value)
return self.converter.descriptor(source, value)

def _pva_request_string(self, fields: List[str]) -> str:
"""
Expand Down
6 changes: 3 additions & 3 deletions src/ophyd_async/epics/pvi/pvi.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ def _sim_common_blocks(device: Device, stripped_type: Optional[Type] = None):

if is_device_vector:
if is_signal:
sub_device_1 = device_cls(SimSignalBackend(signal_dtype, device_name))
sub_device_2 = device_cls(SimSignalBackend(signal_dtype, device_name))
sub_device_1 = device_cls(SimSignalBackend(signal_dtype))
sub_device_2 = device_cls(SimSignalBackend(signal_dtype))
sub_device = DeviceVector({1: sub_device_1, 2: sub_device_2})
else:
sub_device = DeviceVector({1: device_cls(), 2: device_cls()})
Expand All @@ -185,7 +185,7 @@ def _sim_common_blocks(device: Device, stripped_type: Optional[Type] = None):
value.parent = sub_device
else:
if is_signal:
sub_device = device_cls(SimSignalBackend(signal_dtype, device_name))
sub_device = device_cls(SimSignalBackend(signal_dtype))
else:
sub_device = device_cls()

Expand Down
4 changes: 2 additions & 2 deletions src/ophyd_async/sim/pattern_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def get_full_file_description(
):
full_file_description: Dict[str, Descriptor] = {}
for d in datasets:
source = f"sim://{d.name}"
source = f"soft://{d.name}"
shape = outer_shape + tuple(d.shape)
dtype = "number" if d.shape == [1] else "array"
descriptor = Descriptor(
Expand Down Expand Up @@ -158,7 +158,7 @@ def __init__(
self.written_images_counter: int = 0

# it automatically initializes to 0
self.signal_backend = SimSignalBackend(int, "sim://sim_images_counter")
self.signal_backend = SimSignalBackend(int)
self.sim_signal = SignalR(self.signal_backend)
blob = np.array(
generate_gaussian_blob(width=detector_width, height=detector_height)
Expand Down
4 changes: 2 additions & 2 deletions tests/core/test_flyer.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ async def stop(self):

class DummyWriter(DetectorWriter):
def __init__(self, name: str, shape: Sequence[int]):
self.dummy_signal = SignalRW(backend=SimSignalBackend(int, source="test"))
self.dummy_signal = SignalRW(backend=SimSignalBackend(int))
self._shape = shape
self._name = name
self._file: Optional[ComposeStreamResourceBundle] = None
Expand All @@ -61,7 +61,7 @@ def __init__(self, name: str, shape: Sequence[int]):
async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]:
return {
self._name: Descriptor(
source="sim://some-source",
source="soft://some-source",
shape=self._shape,
dtype="number",
external="STREAM:",
Expand Down
Loading

0 comments on commit 4f58a41

Please sign in to comment.