Source code for at.lattice.parambase

from __future__ import annotations

__all__ = ["Operand", "Number", "ParamBase", "ParamDef"]

import abc
from operator import add, sub, mul, truediv, neg
from collections.abc import Callable
from typing import Any, TypeAlias

# Define a type variable for numeric types
Number: TypeAlias = int | float


def _nop(value: Any) -> Any:
    """No-operation function that returns its input unchanged.

    This function is used as a default conversion function in parameter classes.
    """
    return value


class _Evaluator(abc.ABC):
    """Abstract base class for evaluators.

    An evaluator is a callable object that returns a scalar value.
    """

    @staticmethod
    def _convert_to_evaluator(value):
        """Convert a value to an evaluator."""
        if isinstance(value, (int, float)):
            return _Constant(value)
        elif isinstance(value, Operand):
            return value
        msg = f"Parameter operation not defined for type {type(value)}"
        raise TypeError(msg)

    @abc.abstractmethod
    def __call__(self) -> Number:
        """Evaluate and return the value.

        Returns:
            The evaluated value
        """
        ...


class _Constant(_Evaluator):
    """An evaluator that always returns a constant value."""

    __slots__ = ["value"]

    def __init__(self, value: Number):
        """Initialise a constant evaluator.

        Args:
            value: The constant value to return

        Raises:
            TypeError: If the value is not a scalar (int or float)
        """
        if not isinstance(value, Number):
            msg = "The parameter value must be a scalar"
            raise TypeError(msg)
        self.value: Number = value

    def __call__(self) -> Number:
        return self.value


class _BinaryOperator(_Evaluator):
    __slots__ = ["left_operand", "operator", "right_operand"]

    def __init__(self, operator, left, right) -> None:
        """Initialise a binary operator.

        Args:
            operator: The operator function to apply
            left: The left operand of the operator
            right: The right operand of the operator
        """
        self.operator = operator
        self.right_operand = self._convert_to_evaluator(right)
        self.left_operand = self._convert_to_evaluator(left)

    def __call__(self) -> Number:
        return self.operator(self.left_operand.value, self.right_operand.value)


class _UnaryOperator(_Evaluator):
    __slots__ = ["operand", "operator"]

    def __init__(self, operator, operand) -> None:
        """Initialise a unary operator.

        Args:
            operator: The operator function to apply
            operand: The operand to apply the operator to
        """
        self.operator = operator
        self.operand = self._convert_to_evaluator(operand)

    def __call__(self) -> Number:
        return self.operator(self.operand.value)


[docs] class Operand(abc.ABC): """Abstract base class for arithmetic combinations of parameters.""" name: str #: Operand name def __init__(self, name: str, **kwargs): self.name = name super().__init__(**kwargs) @staticmethod def _nm(obj, priority: int): """Return the parenthesised name of the object.""" if isinstance(obj, ParamBase): return obj.name if obj._priority >= priority else f"({obj.name})" else: return str(obj) @property @abc.abstractmethod def value(self): ... def __str__(self): return self.name def __repr__(self): return repr(self.value) def __add__(self, other): try: op = _BinaryOperator(add, self, other) except TypeError: return NotImplemented else: name = "+".join((self._nm(self, 10), self._nm(other, 10))) return ParamBase(evaluator=op, name=name, priority=10) __radd__ = __add__ def __pos__(self): return self def __neg__(self): name = "-" + self._nm(self, 20) op = _UnaryOperator(neg, self) return ParamBase(evaluator=op, name=name, priority=0) def __abs__(self): name = f"abs({self._nm(self, 0)})" op = _UnaryOperator(abs, self) return ParamBase(evaluator=op, name=name, priority=20) def __sub__(self, other): try: op = _BinaryOperator(sub, self, other) except TypeError: return NotImplemented else: name = "-".join((self._nm(self, 10), self._nm(other, 10))) return ParamBase(evaluator=op, name=name, priority=10) def __rsub__(self, other): try: op = _BinaryOperator(sub, other, self) except TypeError: return NotImplemented else: name = "-".join((self._nm(other, 10), self._nm(self, 10))) return ParamBase(evaluator=op, name=name, priority=10) def __mul__(self, other): try: op = _BinaryOperator(mul, self, other) except TypeError: return NotImplemented else: name = "*".join((self._nm(self, 20), self._nm(other, 20))) return ParamBase(evaluator=op, name=name, priority=20) __rmul__ = __mul__ def __truediv__(self, other): try: op = _BinaryOperator(truediv, self, other) except TypeError: return NotImplemented else: name = "/".join((self._nm(self, 20), self._nm(other, 20))) return ParamBase(evaluator=op, name=name, priority=20) def __rtruediv__(self, other): try: op = _BinaryOperator(truediv, other, self) except TypeError: return NotImplemented else: name = "/".join((self._nm(other, 20), self._nm(self, 20))) return ParamBase(evaluator=op, name=name, priority=20) def __pow__(self, other): try: op = _BinaryOperator(pow, self, other) except TypeError: return NotImplemented else: name = "**".join((self._nm(self, 20), self._nm(other, 20))) return ParamBase(evaluator=op, name=name, priority=20) def __rpow__(self, other): try: op = _BinaryOperator(pow, other, self) except TypeError: return NotImplemented else: name = "**".join((self._nm(other, 20), self._nm(self, 20))) return ParamBase(evaluator=op, name=name, priority=20) def __gt__(self, other): return float(self.value) > other def __lt__(self, other): return float(self.value) < other def __ge__(self, other): return float(self.value) >= other def __le__(self, other): return float(self.value) <= other def __float__(self): return float(self.value) def __int__(self): return int(self.value)
[docs] class ParamDef(abc.ABC): """Abstract base class for parameter definitions. This class defines the interface for parameter objects that can be used as element attributes. It provides a *value* property and a method for converting values to the appropriate type. """ def __init__(self, *, conversion: Callable[[Any], Any] | None = _nop, **kwargs): """ Args: conversion: Function to convert values to the appropriate type. """ self._conversion = _nop if conversion is None else conversion super().__init__(**kwargs) def __copy__(self): # Parameters are not copied return self def __deepcopy__(self, memo): # Parameters are not deep-copied return self
[docs] def set_conversion(self, conversion: Callable[[Any], Any]) -> None: """Set the data type conversion function. This method is called when a parameter is assigned to an :py:class:`.Element` attribute. It can only be set once. Args: conversion: Function to convert values to the appropriate type Raises: ValueError: If attempting to change an already set conversion function """ if conversion is not self._conversion: if self._conversion is _nop: self._conversion = conversion else: msg = "Cannot change the data type of the parameter" raise ValueError(msg)
[docs] @abc.abstractmethod def fast_value(self) -> Any: """Return the value of the parameter.""" # This method is called by the __getattr__ method of Element ...
@property def value(self) -> Any: """Current value of the parameter.""" return self.fast_value()
[docs] class ParamBase(ParamDef, Operand): """Read-only base class for parameters. It is used for computed parameters and should not be instantiated otherwise. """ _evaluator: _Evaluator _priority: int def __init__(self, evaluator: _Evaluator, *, priority: int = 20, **kwargs) -> None: """ Args: evaluator: Evaluator function name: Name of the parameter conversion: data conversion function priority: priority of the operator. """ if not isinstance(evaluator, _Evaluator): msg = "'Evaluate' must be an _Evaluate object" raise TypeError(msg) self._evaluator = evaluator self._priority = priority super().__init__(**kwargs)
[docs] def fast_value(self): return self._conversion(self._evaluator())