8. Settle on Signal Types#

Date: 2024-10-18

Status#

Accepted

Context#

At present, soft Signals allow any sort of datatype, while CA, PVA, Tango restrict these to what the control system allows. This means that some soft signals when describe() is called on them will give dtype=object which is not understood by downstream tools. It also means that load/save will not necessarily understand how to serialize results. Finally we now require dtype_numpy for tiled, so arbitrary object types are not suitable even if they are serializable. We should restrict the datatypes allowed in Signals to objects that are serializable and are sensible to add support for in downstream tools.

Decision#

We will allow the following:

  • Primitives:

    • bool

    • int

    • float

    • str

  • Enums:

    • StrictEnum subclass which will be checked to have the same members as the CS

    • SubsetEnum subclass which will be checked to be a subset of the CS members

  • 1D arrays:

    • Array1D[np.bool_]

    • Array1D[np.int8]

    • Array1D[np.uint8]

    • Array1D[np.int16]

    • Array1D[np.uint16]

    • Array1D[np.int32]

    • Array1D[np.uint32]

    • Array1D[np.int64]

    • Array1D[np.uint64]

    • Array1D[np.float32]

    • Array1D[np.float64]

    • Sequence[str]

    • Sequence[MyEnum] where MyEnum is a subclass of StrictEnum or SubsetEnum

  • Specific structures:

    • np.ndarray to represent arrays where dimensionality and dtype can change and must be read from CS

    • Table subclass (which is a pydantic BaseModel) where all members are 1D arrays

Consequences#

Clients will be expected to understand:

  • Python primitives (with Enums serializing as strings)

  • Numpy arrays

  • Pydantic BaseModels

All of the above have sensible dtype_numpy fields, but Table will give a structured row-wise dtype_numpy, while the data will be serialized in a column-wise fashion.

The following breaking changes will be made to ophyd-async:

pvi structure changes#

Structure now read from .value rather than .pvi. Supported in FastCS. Requires at least PandABlocks-ioc 0.10.0

StrictEnum is now requried for all strictly checked Enums#

# old
from enum import Enum
class MyEnum(str, Enum):
    ONE = "one"
    TWO = "two"
# new
from ophyd_async.core import StrictEnum
class MyEnum(StrictEnum):
    ONE = "one"
    TWO = "two"

SubsetEnum is now an Enum subclass:#

from ophyd_async.core import SubsetEnum
# old
MySubsetEnum = SubsetEnum["one", "two"]
# new
class MySubsetEnum(SubsetEnum):
    ONE = "one"
    TWO = "two"

Use python primitives for scalar types instead of numpy types#

# old
import numpy as np
x = epics_signal_rw(np.int32, "PV")
# new
x = epics_signal_rw(int, "PV")

Use Array1D for 1D arrays instead of npt.NDArray#

import numpy as np
# old
import numpy.typing as npt
x = epics_signal_rw(npt.NDArray[np.int32], "PV")
# new
from ophyd_async.core import Array1D
x = epics_signal_rw(Array1D[np.int32], "PV")

Use Sequence[str] for arrays of strings instead of npt.NDArray[np.str_]#

import numpy as np
# old
import numpy.typing as npt
x = epics_signal_rw(npt.NDArray[np.str_], "PV")
# new
from collections.abc import Sequence
x = epics_signal_rw(Sequence[str], "PV")

MockSignalBackend requires a real backend#

# old
fake_set_signal = SignalRW(MockSignalBackend(float))
# new
fake_set_signal = soft_signal_rw(float)
await fake_set_signal.connect(mock=True)

get_mock_put is no longer passed timeout as it is handled in Signal#

# old
get_mock_put(driver.capture).assert_called_once_with(Writing.ON, wait=ANY, timeout=ANY)
# new
get_mock_put(driver.capture).assert_called_once_with(Writing.ON, wait=ANY)

super().__init__ required for Device subclasses#

# old
class MyDevice(Device):
    def __init__(self, name: str = ""):
        self.signal, self.backend_put = soft_signal_r_and_setter(int)
# new
class MyDevice(Device):
    def __init__(self, name: str = ""):
        self.signal, self.backend_put = soft_signal_r_and_setter(int)
        super().__init__(name=name)

Arbitrary BaseModels not supported, pending use cases for them#

The Table type has been suitable for everything we have seen so far, if you need an arbitrary BaseModel subclass then please make an issue

Child Devices set parent on attach, and can’t be public children of more than one parent#

class SourceDevice(Device):
    def __init__(self, name: str = ""):
        self.signal = soft_signal_rw(int)
        super().__init__(name=name)

# old
class ReferenceDevice(Device):
    def __init__(self, signal: SignalRW[int], name: str = ""):
        self.signal = signal
        super().__init__(name=name)

    def set(self, value) -> AsyncStatus:
        return self.signal.set(value + 1)
# new
from ophyd_async.core import Reference

class ReferenceDevice(Device):
    def __init__(self, signal: SignalRW[int], name: str = ""):
        self._signal_ref = Reference(signal)
        super().__init__(name=name)

    def set(self, value) -> AsyncStatus:
        return self._signal_ref().set(value + 1)