Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce PvaAbstractions and use them in SeqTable #522

Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/ophyd_async/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@
soft_signal_rw,
wait_for_value,
)
from ._signal_backend import RuntimeSubsetEnum, SignalBackend, SubsetEnum
from ._signal_backend import (
BackendConverterFactory,
RuntimeSubsetEnum,
SignalBackend,
SubsetEnum,
)
from ._soft_signal_backend import SignalMetadata, SoftSignalBackend
from ._status import AsyncStatus, WatchableAsyncStatus
from ._utils import (
Expand Down Expand Up @@ -103,6 +108,7 @@
"MockSignalBackend",
"callback_on_mock_put",
"get_mock_put",
"BackendConverterFactory",
"mock_puts_blocked",
"reset_mock_put_calls",
"set_mock_put_proceeds",
Expand Down
12 changes: 12 additions & 0 deletions src/ophyd_async/core/_device_save_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from bluesky.plan_stubs import abs_set, wait
from bluesky.protocols import Location
from bluesky.utils import Msg
from pydantic import BaseModel

from ._device import Device
from ._signal import SignalRW
Expand All @@ -18,6 +19,12 @@ def ndarray_representer(dumper: yaml.Dumper, array: npt.NDArray[Any]) -> yaml.No
)


def pydantic_model_abstraction_representer(
dumper: yaml.Dumper, model: BaseModel
) -> yaml.Node:
return dumper.represent_data(model.model_dump(mode="python"))


class OphydDumper(yaml.Dumper):
def represent_data(self, data: Any) -> Any:
if isinstance(data, Enum):
Expand Down Expand Up @@ -134,6 +141,11 @@ def save_to_yaml(phases: Sequence[Dict[str, Any]], save_path: str) -> None:
"""

yaml.add_representer(np.ndarray, ndarray_representer, Dumper=yaml.Dumper)
yaml.add_multi_representer(
BaseModel,
pydantic_model_abstraction_representer,
Dumper=yaml.Dumper,
)

with open(save_path, "w") as file:
yaml.dump(phases, file, Dumper=OphydDumper, default_flow_style=False)
Expand Down
28 changes: 26 additions & 2 deletions src/ophyd_async/core/_signal_backend.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
from abc import abstractmethod
from typing import TYPE_CHECKING, ClassVar, Generic, Literal, Optional, Tuple, Type
from abc import ABC, abstractmethod
from typing import (
TYPE_CHECKING,
ClassVar,
Generic,
Literal,
Optional,
Tuple,
Type,
)

from ._protocol import DataKey, Reading
from ._utils import DEFAULT_TIMEOUT, ReadingValueCallback, T


class BackendConverterFactory(ABC):
"""Convert between the signal backend and the signal type"""

_ALLOWED_TYPES: ClassVar[Tuple[Type]]

@classmethod
@abstractmethod
def datatype_allowed(cls, datatype: Type) -> bool:
"""Check if the datatype is allowed."""

evalott100 marked this conversation as resolved.
Show resolved Hide resolved
@classmethod
@abstractmethod
def make_converter(self, datatype: Type):
"""Updates the object with callables `to_signal` and `from_signal`."""

evalott100 marked this conversation as resolved.
Show resolved Hide resolved

class SignalBackend(Generic[T]):
"""A read/write/monitor backend for a Signals"""

Expand Down
72 changes: 57 additions & 15 deletions src/ophyd_async/core/_soft_signal_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
import time
from collections import abc
from enum import Enum
from typing import Dict, Generic, Optional, Tuple, Type, Union, cast, get_origin
from typing import Any, Dict, Generic, Optional, Tuple, Type, Union, cast, get_origin

import numpy as np
from bluesky.protocols import DataKey, Dtype, Reading
from pydantic import BaseModel
from typing_extensions import TypedDict

from ._signal_backend import RuntimeSubsetEnum, SignalBackend
from ._signal_backend import (
BackendConverterFactory,
RuntimeSubsetEnum,
SignalBackend,
)
from ._utils import DEFAULT_TIMEOUT, ReadingValueCallback, T, get_dtype

primitive_dtypes: Dict[type, Dtype] = {
Expand Down Expand Up @@ -94,7 +99,7 @@ def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
class SoftEnumConverter(SoftConverter):
choices: Tuple[str, ...]

def __init__(self, datatype: Union[RuntimeSubsetEnum, Enum]):
def __init__(self, datatype: Union[RuntimeSubsetEnum, Type[Enum]]):
if issubclass(datatype, Enum):
self.choices = tuple(v.value for v in datatype)
else:
Expand Down Expand Up @@ -122,19 +127,54 @@ def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
return cast(T, self.choices[0])


def make_converter(datatype):
is_array = get_dtype(datatype) is not None
is_sequence = get_origin(datatype) == abc.Sequence
is_enum = inspect.isclass(datatype) and (
issubclass(datatype, Enum) or issubclass(datatype, RuntimeSubsetEnum)
)
class SoftPydanticModelConverter(SoftConverter):
def __init__(self, datatype: Type[BaseModel]):
self.datatype = datatype

def reading(self, value: T, timestamp: float, severity: int) -> Reading:
value = self.value(value)
return super().reading(value, timestamp, severity)
evalott100 marked this conversation as resolved.
Show resolved Hide resolved

def value(self, value: Any) -> Any:
if isinstance(value, dict):
evalott100 marked this conversation as resolved.
Show resolved Hide resolved
value = self.datatype(**value)
return value

def write_value(self, value):
if isinstance(value, self.datatype):
return value.model_dump(mode="python")
evalott100 marked this conversation as resolved.
Show resolved Hide resolved
return value

def make_initial_value(self, datatype: Type | None) -> Any:
return super().make_initial_value(datatype)
evalott100 marked this conversation as resolved.
Show resolved Hide resolved

if is_array or is_sequence:
return SoftArrayConverter()
if is_enum:
return SoftEnumConverter(datatype)

return SoftConverter()
class SoftSignalConverterFactory(BackendConverterFactory):
_ALLOWED_TYPES = (object,) # Any type is allowed

@classmethod
def datatype_allowed(cls, datatype: Type) -> bool:
return True # Any value allowed in a soft signal

@classmethod
def make_converter(cls, datatype):
is_array = get_dtype(datatype) is not None
is_sequence = get_origin(datatype) == abc.Sequence
is_enum = inspect.isclass(datatype) and (
issubclass(datatype, Enum) or issubclass(datatype, RuntimeSubsetEnum)
)
is_pydantic_model = inspect.isclass(datatype) and issubclass(
datatype, BaseModel
)

if is_array or is_sequence:
return SoftArrayConverter()
if is_enum:
return SoftEnumConverter(datatype)
if is_pydantic_model:
return SoftPydanticModelConverter(datatype)

return SoftConverter()


class SoftSignalBackend(SignalBackend[T]):
Expand All @@ -154,7 +194,9 @@ def __init__(
self.datatype = datatype
self._initial_value = initial_value
self._metadata = metadata or {}
self.converter: SoftConverter = make_converter(datatype)
self.converter: SoftConverter = SoftSignalConverterFactory.make_converter(
datatype
)
if self._initial_value is None:
self._initial_value = self.converter.make_initial_value(self.datatype)
else:
Expand Down
2 changes: 1 addition & 1 deletion src/ophyd_async/core/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def get_dtype(typ: Type) -> Optional[np.dtype]:


def get_unique(values: Dict[str, T], types: str) -> T:
"""If all values are the same, return that value, otherwise return TypeError
"""If all values are the same, return that value, otherwise raise TypeError

>>> get_unique({"a": 1, "b": 1}, "integers")
1
Expand Down
2 changes: 2 additions & 0 deletions src/ophyd_async/epics/signal/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from ._common import LimitPair, Limits, get_supported_values
from ._p4p import PvaSignalBackend
from ._p4p_table_model import PvaTable
from ._signal import (
epics_signal_r,
epics_signal_rw,
Expand All @@ -13,6 +14,7 @@
"LimitPair",
"Limits",
"PvaSignalBackend",
"PvaTable",
"epics_signal_r",
"epics_signal_rw",
"epics_signal_rw_rbv",
Expand Down
Loading
Loading