Source code for spacecore.backend.numpy._ops

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Sequence, Tuple, Literal, Type, cast

from .._family import BackendFamily
from .._eager import EagerControlFlowMixin
from .._ops import BackendOps
from ...types import ArrayLike, DenseArray, SparseArray, DType, Index


if TYPE_CHECKING:
    from numpy.typing import NDArray
    NativeArray = NDArray[Any]
else:
    NativeArray = DenseArray


[docs] class NumpyOps(EagerControlFlowMixin, BackendOps[NativeArray]): """ Backend operations on NumPy arrays, with SciPy sparse matrices and arrays. Methods delegate to NumPy and SciPy and follow their semantics; a ``device`` argument must be ``"cpu"`` or ``None``. ``ops.np`` and ``ops.sp`` are an explicit escape hatch. """ import numpy as _np import scipy as _sp import array_api_compat.numpy as xp # Concrete library handles exposed as ``Any`` so the portable protocols # can flow into typed NumPy/SciPy calls without per-boundary casts; mirrors # the base ``xp: ClassVar[Any]`` design. np: Any = _np sp: Any = _sp _family = BackendFamily.numpy.value _allow_sparse = True @property def dense_array(self) -> Type[Any]: """ Dense array type using NumPy. Returns ------- Concrete dense array class accepted by this backend. See: https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html """ return self.np.ndarray @property def sparse_array(self) -> Tuple[Type[Any], ...]: """ Sparse array type tuple using SciPy. Returns ------- Concrete sparse array classes accepted by this backend, or None. See: https://docs.scipy.org/doc/scipy/reference/sparse.html """ # SciPy has spmatrix (legacy) and sparray (newer); accept whichever exist. sparse = self.sp.sparse types: list[type[Any]] = [] if hasattr(sparse, "spmatrix"): types.append(sparse.spmatrix) if hasattr(sparse, "sparray"): types.append(sparse.sparray) return tuple(types)
[docs] def sanitize_dtype(self, dtype: DType | None) -> DType: """ Normalize a dtype specifier using NumPy. Input: dtype: Optional dtype requested by SpaceCore or the caller. Output: Backend dtype object accepted by array constructors. See: https://numpy.org/doc/stable/reference/generated/numpy.dtype.html """ if dtype is None: return self.np.float64 return self.np.dtype(dtype)
[docs] def assparse( self, x: Any, *, format: Literal["csr", "csc", "coo"] = "csr", dtype: DType | None = None ) -> SparseArray: """Convert to a 2-D SciPy sparse matrix in the requested format.""" self._reject_complex_to_real(x, dtype, operation="assparse") if format not in ("csr", "csc", "coo"): raise ValueError(f"Unknown sparse format: {format!r}") # Already sparse: cast if needed, then convert the format. if self.is_sparse(x): if dtype is not None and self.get_dtype(x) != self.sanitize_dtype(dtype): x = x.astype(self.sanitize_dtype(dtype)) return getattr(x, "to" + format)() # Dense input: only 2-D arrays have a sparse-matrix form. x_arr = self.asarray(x, dtype=dtype) if x_arr.ndim != 2: raise ValueError(f"{self.family} sparse conversion currently expects a 2D array.") return getattr(self.sp.sparse, format + "_matrix")(x_arr)
[docs] def sparse_matmul(self, a: SparseArray, b: DenseArray) -> NativeArray: """Multiply a sparse matrix by a dense array.""" if not self.is_sparse(a): raise TypeError(f"sparse_matmul expects `a` to be a {self.family} sparse matrix/array.") if not self.is_dense(b): raise TypeError(f"sparse_matmul expects `b` to be a {self.family} dense array.") return a @ b
[docs] def logsumexp( self, a: DenseArray, axis: int | Sequence[int] | None = None, b: DenseArray | None = None, keepdims: bool = False, return_sign: bool = False, ) -> NativeArray | Tuple[NativeArray, NativeArray]: """ Compute a stable log-sum-exp reduction using SciPy. Input: a: Dense backend array; axis, weights, and sign options control the reduction. Output: Dense backend array or tuple containing log-sum-exp results. See: https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.logsumexp.html """ result = self.sp.special.logsumexp( a, axis=axis, b=b, keepdims=keepdims, return_sign=return_sign ) if return_sign: return self._as_array(result[0]), self._as_array(result[1]) return self._as_array(result)
def _copy(self, x: DenseArray) -> NativeArray: """Return a NumPy copy of ``x`` (mutation primitive for index ops).""" return cast(Any, x).copy() def _scatter_add_inplace( self, y: DenseArray, index: Index, values: ArrayLike | float | complex ) -> None: """Accumulate ``values`` into ``y`` at ``index`` via ``numpy.add.at``.""" self.np.add.at(y, index, values)
[docs] def ix_(self, *args: Any) -> Any: r""" Build open mesh index arrays using NumPy. Input: args: One-dimensional index arrays or sequences. Output: Tuple of dense backend arrays usable for open-mesh indexing. See: `Open-mesh indexing documentation <https://numpy.org/doc/stable/reference/generated/numpy.ix_.html>`_ """ return self.np.ix_(*args)
[docs] def allclose_sparse( self, a: SparseArray, b: SparseArray, rtol: float = 1e-5, atol: float = 1e-8, ) -> bool: """Compare two SciPy sparse matrices within tolerances, via their CSR difference.""" self._require_two_sparse(a, b) # Compare in CSR: different shapes are never close, an empty difference always is. a_csr = cast(Any, a).tocsr() b_csr = cast(Any, b).tocsr() if a_csr.shape != b_csr.shape: return False diff = (a_csr - b_csr).tocsr() if diff.nnz == 0: return True # NaN differences are never "close": ``abs(NaN) > tol`` is False, which # would otherwise mask a NaN-vs-finite (or NaN-vs-NaN) entry as equal and # wrongly report two different operators as close. This matches the # torch/cupy paths (dense ``allclose`` with ``equal_nan=False``). if self.np.isnan(diff.data).any(): return False # Entrywise max(|a|, |b|) scales the relative tolerance. a_abs = abs(a_csr).tocsr() b_abs = abs(b_csr).tocsr() scale = self.sp.sparse.csr_matrix.maximum(a_abs, b_abs) # tolerance_ij = atol + rtol * max(|a_ij|, |b_ij|) tol = scale.multiply(rtol) tol.data += atol # Close iff no stored entry of |a - b| exceeds its tolerance. bad = abs(diff) > tol return bad.nnz == 0