Source code for pydft_qmmm.system.variable

"""Data container classes for implementing the observer pattern.
"""
from __future__ import annotations

from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any
from typing import Generic
from typing import TYPE_CHECKING
from typing import TypeVar

import numpy as np
from numpy.typing import NDArray

if TYPE_CHECKING:
    from types import EllipsisType
    from typing import Callable
    from typing import TypeAlias

    ArrayLikeInt: TypeAlias = (
        int | np.integer[Any] | Sequence[int | np.integer[Any]]
        | Sequence[Sequence[Any]] | NDArray[Any]
    )

    Index: TypeAlias = (
        ArrayLikeInt | slice | EllipsisType
        | tuple[ArrayLikeInt | slice | EllipsisType]
    )

T = TypeVar("T")

DT = TypeVar("DT", covariant=True, bound=np.dtype[Any])
ST = TypeVar("ST", bound=Any)

DT2 = TypeVar("DT2", bound=np.dtype[Any])
ST2 = TypeVar("ST2", bound=Any)

array_float: TypeAlias = np.dtype[np.float64]
array_int: TypeAlias = np.dtype[np.int32]
array_str: TypeAlias = np.dtype[np.str_]
array_obj: TypeAlias = np.dtype[np.object_]


[docs] class ObservedArray(np.ndarray[ST, DT]): """A data container for arrays implementing the observer pattern. Attributes: base: The actual array data _notifiers: Functions that are called when a value of the array is changed. """ base: np.ndarray[ST, DT] _notifiers: list[Callable[[np.ndarray[ST, DT]], None]] def __new__(cls, array: np.ndarray[ST, DT]) -> ObservedArray[ST, DT]: """Generate a new observed array object. Args: array: The Numpy array data from which to make an observed array. Returns: An observed array containing the same data as was input. """ obj = np.asarray(array).view(cls) obj._notifiers = [] return obj def __setitem__( self, key: Index, value: Any, ) -> None: """Change a value of the observed array. Args: key: The index of the array corresponding to the value that will be updated. value: The new value for the array element corresponding to the index. """ super().__setitem__(key, value) for notify in self._notifiers: notify(self.base) def __array_wrap__( self, array: np.ndarray[ST2, DT2], context: tuple[np.ufunc, tuple[Any, ...], int] | None = None, ) -> np.ndarray[ST2, DT2]: """Notify observers when operations are performed on the array. Args: array: The array which is being updated by an operation. context: The operation being performed on an array. Returns: A regular Numpy array resulting from the operation. """ if array is self: for notify in self._notifiers: notify(self.base) return array return array.view(np.ndarray) def __array_finalize__(self, array: None | np.ndarray[Any, Any]) -> None: """Finalize the instantiation of an array. Args: array: The array being instantiated. """ if array is None: return self._notifiers = getattr(array, "_notifiers", [])
[docs] def register_notifier( self, notifier: Callable[[np.ndarray[ST, DT]], None], ) -> None: """Register observers to be notified when array data is edited. Args: notifier: A function which will be called when array data is edited. """ self._notifiers.append(notifier)
[docs] @dataclass class ArrayValue(Generic[T]): """A wrapper for single values from observed arrays. Args: _array: The observed array containing the value. _key: The index for the value in the observed array. """ _array: ObservedArray[Any, array_float | array_int | array_str | array_obj] _key: int
[docs] def update(self, value: T) -> None: """Update the value in the observed array. Args: value: The new value for the array element corresponding to the index. """ self._array[self._key] = value
[docs] def value(self) -> T: """Return the observed array value. Returns: The value of the array element corresponding to the index. """ return self._array[self._key]