Skip to content

Add optional validation function(s) to soft_signals #1119

@oliwenmandiamond

Description

@oliwenmandiamond

In dodal, we are wanting to implement a very simple device (DiamondLightSource/dodal#1661)

class UndulatorOrder(StandardReadable, Locatable[int]):
    """
    Represents the order of an undulator device. Allows setting and locating the order.
    """

    def __init__(self, name: str = "") -> None:
        """
        Args:
            name: Name for device. Defaults to ""
        """
        with self.add_children_as_readables():
            self._value = soft_signal_rw(int, initial_value=3)
        super().__init__(name=name)

    @AsyncStatus.wrap
    async def set(self, value: int) -> None:
        if (value >= 0) and isinstance(value, int):
            await self._value.set(value)
        else:
            raise ValueError(
                f"Undulator order must be a positive integer. Requested value: {value}"
            )

    async def locate(self) -> Location[int]:
        return await self._value.locate()

The device has a set method with validation, but this can be ignored by just moving the signal directly

e.g

await order._value.set(invalid_value)

It would be nice when creating a soft_signal_rw, you could provide some sort of validation function. We have played around using a derived_signal_rw, however we still have to create a soft_signal which defeats the purpose as a user can still bypass this (DiamondLightSource/dodal#1661 (comment))

class UndulatorOrder(StandardReadable, Locatable[int]):
    """
    Represents the order of an undulator device. Allows setting and locating the order.
    """

    def __init__(self, name: str = "") -> None:
        """
        Args:
            name: Name for device. Defaults to ""
        """
        self._internal_order = soft_signal_rw(int, initial_value=3)
        with self.add_children_as_readables():
            self.order = derived_signal_rw(
                self._get_order,
                self._set_order,
                current_order = self._internal_order
                )
        super().__init__(name=name)

    @AsyncStatus.wrap
    async def set(self, value: int) -> None:
        await self.order.set(value)

    def _get_order(self, current_order:int) -> int:
        return current_order

    async def _set_order(self, new_order: int) -> None:
        if (new_order < 0) or not isinstance(new_order, int):
            raise ValueError("Undulator order must be a positive integer")
        LOGGER.info(f"Setting undulator order to {new_order}")
        self._internal_order.set(new_order)

    async def locate(self) -> Location[int]:
        return await self.order.locate()

It would be great if we could edit SoftSignalBackend to allow us to add custom validation functions e.g something like below:

    def __init__(self, name: str = "") -> None:
        """
        Args:
            name: Name for device. Defaults to ""
        """
        with self.add_children_as_readables():
            self._value = soft_signal_rw(int, initial_value=3, validation=SignalValidator(func=self._value_validation))
        super().__init__(name=name)

    def _value_validation(self, value: int) -> None:
        if (value <= 0) and not isinstance(value, int):
            raise ValueError(
                f"Undulator order must be a positive integer. Requested value: {value}"
            )

   @AsyncStatus.wrap
   async def set(self, value: int):
         await self._value(value)

    async def locate(self) -> Location[int]:
        return await self._value.locate()

Now in this example, we don't need to rely on external signals for validation, but I can see in future we may need a soft signal which can only be validated by other signals e.g

        ...
        with self.add_children_as_readables():
            self.epics_signal1 = epics_signal_rw(float, prefix + "1")
            self.epics_signal2 = epics_signal_rw(float, prefix + "2")
            self._value = soft_signal_rw(int, initial_value=3, validation=SignalValidator(func=self._signal_validation, epics_value1=self.epics_signal1, epics_value2 = epics_value2))
   
    ...

    def _signal_validation(value: int, epics_value1: float, epics_value2: float) -> None:
         # My custom validation logic here
         ...

So it is similar to derived_signal, but you don't need an existing soft signal with no validation

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions