Source code for backend.base

"""
AbstractBackend ABC, @passthrough decorator, and BackendCapabilityError.

Kramer Harrison, 2025
"""

from __future__ import annotations

import contextlib
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Literal

if TYPE_CHECKING:
    from collections.abc import Callable, Generator, Sequence
    from types import ModuleType


[docs] class BackendCapabilityError(Exception): """Raised when an operation is not supported by the current backend. Example: >>> be.grad_mode.enable() # on numpy backend BackendCapabilityError: grad_mode requires a backend that supports gradients. Current backend: 'numpy'. Try: be.set_backend('torch') """
[docs] def passthrough(*func_names: str): """Inject concrete passthrough methods into the decorated class. For each name in func_names, adds a method that calls ``self._lib.<name>(*args, **kwargs)``. Only injected if the class does not already define the method — explicit overrides always take priority. Args: *func_names: Names of functions to inject from the backend library. Returns: A class decorator that injects the passthrough methods. """ def decorator(cls: type) -> type: for name in func_names: if not hasattr(cls, name): def _make(n: str) -> Callable[..., Any]: def method(self: AbstractBackend, *args: Any, **kwargs: Any) -> Any: return getattr(self._lib, n)(*args, **kwargs) method.__name__ = n method.__qualname__ = f"{cls.__name__}.{n}" return method setattr(cls, name, _make(name)) return cls return decorator
[docs] @passthrough( # Trigonometric "sin", "cos", "tan", "arcsin", "arccos", "arctan", "arctan2", "sinh", "cosh", "tanh", # Math "exp", "log", "log2", "log10", "sqrt", "abs", "sign", "floor", "ceil", "hypot", # Angle conversion (np.deg2rad / torch.deg2rad share same name) "deg2rad", "rad2deg", # Checks "isnan", "isinf", "isfinite", # Logic "logical_and", "logical_or", "logical_not", # Complex "conj", "real", # Linear algebra (identical API in np and torch) "outer", "einsum", "dot", # NaN-safe reductions (np.nansum/torch.nansum, np.nanmean/torch.nanmean) "nansum", "nanmean", # Stack variants with matching names "vstack", "column_stack", # Sorting / searching "searchsorted", "round", # Array info — torch backend overrides these with its own implementations "shape", "size", "copy", "isscalar", "load", # dtype / machine-epsilon info "finfo", # Closeness / comparison (np.allclose / torch.allclose) "allclose", # Complex helpers "imag", # Sign copying "copysign", ) class AbstractBackend(ABC): """Abstract base class that defines the full backend contract. All backends must subclass this class and implement every abstract method. Concrete passthrough methods (injected by @passthrough) delegate to ``self._lib.<name>(...)``; subclasses may override them. Attributes: _lib: The underlying library module (``numpy`` or ``torch``). """ _lib: ModuleType # Each subclass sets: _lib = np or _lib = torch # ------------------------------------------------------------------ # Identity # ------------------------------------------------------------------ @property @abstractmethod def name(self) -> str: """Return the backend name (e.g. 'numpy' or 'torch').""" # ------------------------------------------------------------------ # Submodule proxies # ------------------------------------------------------------------ @property def linalg(self) -> Any: """Expose the linear-algebra submodule of the underlying library.""" return self._lib.linalg @property def fft(self) -> Any: """Expose the FFT submodule of the underlying library.""" return self._lib.fft @property def random(self) -> Any: """Expose the random submodule of the underlying library.""" return self._lib.random # ------------------------------------------------------------------ # Capability flags # ------------------------------------------------------------------ @property def supports_gradients(self) -> bool: """Return True if this backend supports automatic differentiation.""" return False @property def supports_gpu(self) -> bool: """Return True if this backend can use GPU acceleration.""" return False # ------------------------------------------------------------------ # Precision # ------------------------------------------------------------------
[docs] @abstractmethod def set_precision(self, precision: Literal["float32", "float64"]) -> None: """Set the floating-point precision used by this backend. Args: precision: Either ``'float32'`` or ``'float64'``. """
[docs] @abstractmethod def get_precision(self) -> int: """Return the current precision as an integer (32 or 64)."""
# ------------------------------------------------------------------ # Capability-gated torch-only features # Default implementations raise BackendCapabilityError. # TorchBackend overrides these. # ------------------------------------------------------------------ @property def grad_mode(self) -> Any: """Control object for gradient computation (torch only).""" raise BackendCapabilityError( f"grad_mode requires a backend that supports gradients. " f"Current backend: '{self.name}'. Try: be.set_backend('torch')" ) @property def autograd(self) -> Any: """The autograd submodule (torch only).""" raise BackendCapabilityError( f"autograd is not supported by backend '{self.name}'." )
[docs] @contextlib.contextmanager def no_grad_unless_enabled(self) -> Generator[None, None, None]: """Context in which gradient bookkeeping may be suppressed. A no-op for backends without gradient support. The torch backend runs the context under ``torch.no_grad`` unless gradients were globally requested via ``grad_mode``, sparing per-op autograd dispatch during plain forward computations. """ yield
[docs] def set_device(self, device: str) -> None: """Set the compute device (torch only). Args: device: Device string (e.g. ``'cpu'`` or ``'cuda'``). Raises: BackendCapabilityError: Always, on non-torch backends. """ raise BackendCapabilityError( f"set_device is not supported by backend '{self.name}'." )
[docs] def get_device(self) -> str: """Return the current compute device (torch only). Raises: BackendCapabilityError: Always, on non-torch backends. """ raise BackendCapabilityError( f"get_device is not supported by backend '{self.name}'." )
[docs] def get_complex_precision(self) -> Any: """Return the complex dtype matching the current precision (torch only). Raises: BackendCapabilityError: Always, on non-torch backends. """ raise BackendCapabilityError( f"get_complex_precision is not supported by backend '{self.name}'." )
[docs] def to_tensor(self, data: Any, device: Any = None) -> Any: """Convert data to a backend tensor with current precision (torch only). Raises: BackendCapabilityError: Always, on non-torch backends. """ raise BackendCapabilityError( f"to_tensor is not supported by backend '{self.name}'." )
# ------------------------------------------------------------------ # Array creation # ------------------------------------------------------------------
[docs] @abstractmethod def array(self, x: Any) -> Any: """Create a backend array/tensor from x."""
[docs] @abstractmethod def zeros(self, shape: Sequence[int], dtype: Any = None) -> Any: """Return a new array of the given shape filled with zeros."""
[docs] @abstractmethod def ones(self, shape: Sequence[int], dtype: Any = None) -> Any: """Return a new array of the given shape filled with ones."""
[docs] @abstractmethod def full(self, shape: Sequence[int], fill_value: Any, dtype: Any = None) -> Any: """Return a new array of the given shape filled with fill_value."""
[docs] @abstractmethod def linspace(self, start: float, stop: float, num: int = 50) -> Any: """Return evenly spaced numbers over the specified interval."""
[docs] @abstractmethod def arange(self, *args: Any, **kwargs: Any) -> Any: """Return evenly spaced values within a given interval."""
[docs] @abstractmethod def zeros_like(self, x: Any) -> Any: """Return an array of zeros with the same shape and type as x."""
[docs] @abstractmethod def ones_like(self, x: Any) -> Any: """Return an array of ones with the same shape and type as x."""
[docs] @abstractmethod def full_like(self, x: Any, fill_value: Any) -> Any: """Return a full array with the same shape as x."""
[docs] @abstractmethod def empty(self, shape: Sequence[int]) -> Any: """Return a new uninitialized array of the given shape."""
[docs] @abstractmethod def empty_like(self, x: Any) -> Any: """Return an uninitialized array with the same shape as x."""
[docs] @abstractmethod def eye(self, n: int) -> Any: """Return a 2D identity matrix of size n."""
[docs] @abstractmethod def asarray(self, x: Any, **kwargs: Any) -> Any: """Convert x to a backend array without copying if possible."""
# ------------------------------------------------------------------ # Array utilities # ------------------------------------------------------------------
[docs] @abstractmethod def cast(self, x: Any) -> Any: """Cast x to the current floating-point precision."""
[docs] @abstractmethod def is_array_like(self, x: Any) -> bool: """Return True if x is a list, tuple, or backend array."""
[docs] @abstractmethod def arange_indices(self, start: Any, stop: Any = None, step: int = 1) -> Any: """Return an integer array of indices."""
[docs] @abstractmethod def ravel(self, x: Any) -> Any: """Return a 1D float array of x."""
# ------------------------------------------------------------------ # Shape and indexing # ------------------------------------------------------------------
[docs] @abstractmethod def transpose(self, x: Any, axes: Sequence[int] | None = None) -> Any: """Permute the dimensions of x."""
[docs] @abstractmethod def reshape(self, x: Any, shape: Sequence[int]) -> Any: """Return x reshaped to the given shape."""
[docs] @abstractmethod def atleast_1d(self, x: Any) -> Any: """Return x as an array with at least one dimension."""
[docs] @abstractmethod def atleast_2d(self, x: Any) -> Any: """Return x as an array with at least two dimensions."""
[docs] @abstractmethod def as_array_1d(self, data: Any) -> Any: """Force conversion to a 1D array."""
[docs] @abstractmethod def stack(self, xs: Sequence[Any], axis: int = 0) -> Any: """Join a sequence of arrays along a new axis."""
[docs] @abstractmethod def concatenate(self, arrays: Sequence[Any], axis: int = 0) -> Any: """Join arrays along an existing axis."""
[docs] @abstractmethod def flip(self, x: Any) -> Any: """Reverse the order of elements in x along axis 0."""
[docs] @abstractmethod def roll(self, x: Any, shift: Any, axis: Any = ()) -> Any: """Roll x elements along the given axis."""
[docs] @abstractmethod def repeat(self, x: Any, repeats: int) -> Any: """Repeat elements of x."""
[docs] @abstractmethod def broadcast_to(self, x: Any, shape: Sequence[int]) -> Any: """Broadcast x to the given shape."""
[docs] @abstractmethod def tile(self, x: Any, dims: Any) -> Any: """Construct an array by tiling x."""
[docs] @abstractmethod def expand_dims(self, x: Any, axis: int) -> Any: """Expand the shape of x by inserting a new axis."""
[docs] @abstractmethod def meshgrid(self, *arrays: Any) -> tuple[Any, ...]: """Return coordinate matrices from coordinate vectors."""
[docs] @abstractmethod def unsqueeze_last(self, x: Any) -> Any: """Add a trailing dimension to x."""
# ------------------------------------------------------------------ # Reductions and math with semantic mismatches # ------------------------------------------------------------------
[docs] @abstractmethod def sum(self, x: Any, axis: int | None = None) -> Any: """Sum array elements over a given axis."""
[docs] @abstractmethod def mean(self, x: Any, axis: int | None = None, keepdims: bool = False) -> Any: """Compute the arithmetic mean, ignoring NaNs."""
[docs] @abstractmethod def std(self, x: Any, axis: int | None = None) -> Any: """Compute the standard deviation along the given axis."""
[docs] @abstractmethod def max(self, x: Any) -> Any: """Return the maximum value of x."""
[docs] @abstractmethod def min(self, x: Any) -> Any: """Return the minimum value of x."""
[docs] @abstractmethod def argmin(self, x: Any, axis: int | None = None) -> Any: """Return indices of the minimum values along an axis."""
[docs] @abstractmethod def argwhere(self, x: Any) -> Any: """Return indices of non-zero elements."""
[docs] @abstractmethod def clip(self, x: Any, a_min: Any, a_max: Any) -> Any: """Clip the values in x to [a_min, a_max]."""
[docs] @abstractmethod def where(self, condition: Any, x: Any, y: Any) -> Any: """Return elements chosen from x or y depending on condition."""
[docs] @abstractmethod def maximum(self, a: Any, b: Any) -> Any: """Element-wise maximum of a and b."""
[docs] @abstractmethod def minimum(self, a: Any, b: Any) -> Any: """Element-wise minimum of a and b."""
[docs] @abstractmethod def fmax(self, a: Any, b: Any) -> Any: """Element-wise maximum, ignoring NaNs."""
[docs] @abstractmethod def power(self, x: Any, y: Any) -> Any: """Return x raised to the power y."""
[docs] @abstractmethod def diff(self, x: Any, n: int = 1, axis: int = -1, **kwargs: Any) -> Any: """Calculate the n-th discrete difference along the given axis."""
[docs] @abstractmethod def all(self, x: Any) -> bool: """Return True if all elements of x are True."""
[docs] @abstractmethod def any(self, x: Any) -> bool: """Return True if any element of x is True."""
[docs] @abstractmethod def nanmax(self, x: Any, axis: int | None = None, keepdim: bool = False) -> Any: """Return the maximum, ignoring NaNs."""
[docs] @abstractmethod def sort(self, x: Any, axis: int = -1) -> Any: """Return a sorted copy of x."""
[docs] @abstractmethod def isclose(self, a: Any, b: Any, rtol: float = 1e-5, atol: float = 1e-8) -> Any: """Return a boolean array where elements are close."""
[docs] @abstractmethod def radians(self, x: Any) -> Any: """Convert angles from degrees to radians."""
[docs] @abstractmethod def degrees(self, x: Any) -> Any: """Convert angles from radians to degrees."""
# ------------------------------------------------------------------ # Linear algebra # ------------------------------------------------------------------
[docs] @abstractmethod def matmul(self, a: Any, b: Any) -> Any: """Matrix product of two arrays."""
[docs] @abstractmethod def cross( self, a: Any, b: Any, axisa: int = -1, axisb: int = -1, axisc: int = -1, axis: int | None = None, ) -> Any: """Return the cross product of two vectors."""
[docs] @abstractmethod def batched_chain_matmul3(self, a: Any, b: Any, c: Any) -> Any: """Compute a @ b @ c with promoted dtype."""
[docs] @abstractmethod def matrix_vector_multiply_and_squeeze(self, p: Any, E: Any) -> Any: """Multiply p @ E[..., newaxis] and squeeze the trailing dimension."""
[docs] @abstractmethod def mult_p_E(self, p: Any, E: Any) -> Any: """Complex matrix-vector multiply used for polarized fields."""
[docs] @abstractmethod def lstsq(self, a: Any, b: Any) -> Any: """Return the least-squares solution to a @ x = b."""
[docs] @abstractmethod def to_complex(self, x: Any) -> Any: """Cast x to complex128."""
# ------------------------------------------------------------------ # Interpolation # ------------------------------------------------------------------
[docs] @abstractmethod def nearest_nd_interpolator(self, points: Any, values: Any, x: Any, y: Any) -> Any: """Nearest-neighbour interpolation on an N-D dataset."""
[docs] @abstractmethod def interp(self, x: Any, xp: Any, fp: Any) -> Any: """1-D linear interpolation."""
[docs] @abstractmethod def grid_sample( self, input: Any, grid: Any, mode: str = "bilinear", padding_mode: str = "zeros", align_corners: bool = False, ) -> Any: """Sample input using bilinear/nearest interpolation on a grid."""
# ------------------------------------------------------------------ # Polynomial # ------------------------------------------------------------------
[docs] @abstractmethod def polyfit(self, x: Any, y: Any, degree: int) -> Any: """Least-squares polynomial fit."""
[docs] @abstractmethod def polyval(self, coeffs: Any, x: Any) -> Any: """Evaluate a polynomial at specific values."""
# ------------------------------------------------------------------ # Signal processing # ------------------------------------------------------------------
[docs] @abstractmethod def fftconvolve(self, in1: Any, in2: Any, mode: str = "full") -> Any: """FFT-based convolution."""
# ------------------------------------------------------------------ # Random number generation # ------------------------------------------------------------------
[docs] @abstractmethod def default_rng(self, seed: int | None = None) -> Any: """Return a random number generator seeded with seed."""
[docs] @abstractmethod def random_uniform( self, low: float = 0.0, high: float = 1.0, size: Any = None, generator: Any = None, ) -> Any: """Uniform random samples in [low, high)."""
[docs] @abstractmethod def rand(self, *size: int) -> Any: """Random values from a uniform distribution on [0, 1)."""
[docs] @abstractmethod def random_normal( self, loc: float = 0.0, scale: float = 1.0, size: Any = None, generator: Any = None, ) -> Any: """Random samples from a normal (Gaussian) distribution."""
[docs] @abstractmethod def sobol_sampler( self, dim: int, num_samples: int, scramble: bool = True, seed: int | None = None, ) -> Any: """Generate quasi-random samples using Sobol sequences."""
[docs] @abstractmethod def erfinv(self, x: Any) -> Any: """Inverse error function."""
# ------------------------------------------------------------------ # Miscellaneous # ------------------------------------------------------------------
[docs] @abstractmethod def conic_intersection( self, x: Any, y: Any, z: Any, L: Any, M: Any, N: Any, radius: Any, conic: Any, contains: Callable[[Any, Any], Any] | None = None, ) -> Any: """Return ray parameters at the selected finite-conic intersections. Solve ``x_hit**2 + y_hit**2 + (1+k)*z_hit**2 - 2*R*z_hit = 0`` for hits along ``(x, y, z) + t*(L, M, N)``. Prefer the nearest positive root on the sag sheet, with an optional membership predicate selecting the used surface patch. If neither root is admissible, choose the finite root nearest the vertex; return NaN if none exists. Preserve positive discriminants and resolved near-tangent crossings. Args: x: Local ray x coordinates. y: Local ray y coordinates. z: Local ray z coordinates. L: Local direction x components. M: Local direction y components. N: Local direction z components. radius: Finite, nonzero radius, scalar or broadcastable array. conic: Conic constant, scalar or broadcastable array. contains: Optional ``contains(x_hit, y_hit)`` callable returning a boolean array on this backend. It only chooses a discrete branch; it is not differentiated with respect to its state. Returns: Intersection parameters with the backend's broadcasting and dtype semantics. Inputs are not mutated or moved between devices. Torch differentiates regular selected branches; exact double roots retain their forward value but contribute zero gradient. """
[docs] @abstractmethod def factorial(self, n: Any) -> Any: """Compute the factorial of n."""
[docs] @abstractmethod def path_contains_points(self, vertices: Any, points: Any) -> Any: """Return a boolean mask of points inside the polygon."""
[docs] @abstractmethod def pad( self, tensor: Any, pad_width: Any, mode: str = "constant", constant_values: float | None = 0, ) -> Any: """Pad an array."""
[docs] @abstractmethod def vectorize(self, pyfunc: Callable[..., Any]) -> Callable[..., Any]: """Vectorize a scalar function over array inputs."""
[docs] @abstractmethod def errstate(self, **kwargs: Any) -> Any: """Context manager for floating-point error state."""
[docs] @abstractmethod def histogram(self, x: Any, bins: Any = 10) -> tuple[Any, Any]: """Compute a histogram of x."""
[docs] @abstractmethod def histogram2d( self, x: Any, y: Any, bins: Any, weights: Any = None, ) -> tuple[Any, Any, Any]: """Compute a 2-D histogram."""