Skip to content

sgnts.base.backend

Namespace-based backend resolution — "the array is the backend."

The historical approach in sgn-ts tracked the array backend as configuration: a backend field stored on buffers and elements, threaded through call after call, kept in sync by hand, and reconciled with the data via ad-hoc isinstance(data, NumpyArray/TorchArray) ladders scattered across the code.

This module replaces that with a single idea: the array already knows what it is. We derive the backend from the data through the Python Array API standard (https://data-apis.org/array-api), accessed via array-api-compat so it works uniformly across numpy (<2 and >=2), torch, cupy, jax and dask.

Primitives:

  • :func:array_namespace — the standard xp namespace for writing backend-blind code (xp.zeros_like(x) etc.).
  • :func:backend_name — the backend name ("numpy"/"torch") for an array, used by the capability system.
  • :func:new_zeros — create a zeros array matching a reference array's namespace, dtype and device.

Two related vocabularies, kept deliberately distinct: a namespace is the xp object you compute with (the Array API standard term, hence :func:array_namespace); a backend is its string name ("numpy"/"torch") — the comparable label the capability layer declares and checks (backends, ANY_BACKEND, BackendError). One is the dispatch object; the other is a name you can put in a frozenset or a class attribute.

BackendError

Bases: Exception


              flowchart TD
              sgnts.base.backend.BackendError[BackendError]

              

              click sgnts.base.backend.BackendError href "" "sgnts.base.backend.BackendError"
            

Raised when an element receives data in a backend it does not declare.

The capability system (a transform's backends attribute) is validated at runtime against the actual data; a mismatch raises this with a message naming the element, the declared backends, the received backend, and the fix (insert a Converter).

Source code in src/sgnts/base/backend.py
class BackendError(Exception):
    """Raised when an element receives data in a backend it does not declare.

    The capability system (a transform's ``backends`` attribute) is validated at
    runtime against the actual data; a mismatch raises this with a message naming
    the element, the declared backends, the received backend, and the fix
    (insert a ``Converter``).
    """

array_namespace(*xs)

Return the Array API namespace (xp) for the given array(s).

Thin, forgiving wrapper over :func:array_api_compat.array_namespace. None (gap data), Python scalars, and unrecognized objects yield None rather than raising, so callers can fall back to a default backend.

Parameters:

Name Type Description Default
*xs Any

One or more candidate arrays. Non-array arguments are ignored.

()

Returns:

Type Description
Optional[Any]

The shared xp namespace, or None if no argument is an array.

Source code in src/sgnts/base/backend.py
def array_namespace(*xs: Any) -> Optional[Any]:
    """Return the Array API namespace (``xp``) for the given array(s).

    Thin, forgiving wrapper over :func:`array_api_compat.array_namespace`.
    ``None`` (gap data), Python scalars, and unrecognized objects yield ``None``
    rather than raising, so callers can fall back to a default backend.

    Args:
        *xs:
            One or more candidate arrays. Non-array arguments are ignored.

    Returns:
        The shared ``xp`` namespace, or ``None`` if no argument is an array.
    """
    arrays = [x for x in xs if x is not None and not isinstance(x, _SCALAR_TYPES)]
    if not arrays:
        return None
    try:
        return array_api_compat.array_namespace(*arrays)
    except TypeError:
        # e.g. a string, an object array, or mixed namespaces
        return None

backend_name(data)

Return the backend name ("numpy" / "torch") for an array.

Yields the string name that capability sets are written in (used by the runtime capability check). Returns None for None, scalars, and unrecognized types.

Parameters:

Name Type Description Default
data Any

The array (or non-array) to classify.

required

Returns:

Type Description
Optional[str]

"numpy" / "torch", or None.

Source code in src/sgnts/base/backend.py
def backend_name(data: Any) -> Optional[str]:
    """Return the backend *name* (``"numpy"`` / ``"torch"``) for an array.

    Yields the string name that capability sets are written in (used by the
    runtime capability check). Returns ``None`` for ``None``, scalars, and
    unrecognized types.

    Args:
        data:
            The array (or non-array) to classify.

    Returns:
        ``"numpy"`` / ``"torch"``, or ``None``.
    """
    if data is None or isinstance(data, _SCALAR_TYPES):
        return None
    if array_api_compat.is_numpy_array(data):
        return "numpy"
    if _is_torch_array(data):
        return "torch"
    return None

device(x)

The device of an array, portably ("cpu" for numpy, the device for torch).

Thin wrapper over :func:array_api_compat.device — numpy 1.x arrays have no .device attribute, so this is the safe way to read it.

Source code in src/sgnts/base/backend.py
def device(x: Any) -> Any:
    """The device of an array, portably (``"cpu"`` for numpy, the device for torch).

    Thin wrapper over :func:`array_api_compat.device` — numpy 1.x arrays have no
    ``.device`` attribute, so this is the safe way to read it.
    """
    return array_api_compat.device(x)

new_zeros(reference, shape)

Zeros of shape matching reference's namespace, dtype, and device.

The end-state replacement for backend.zeros(shape): rather than a configured backend, the new array follows a reference array (e.g. the non-gap data a gap is being filled alongside).

Source code in src/sgnts/base/backend.py
def new_zeros(reference: Any, shape: tuple[int, ...]) -> Any:
    """Zeros of ``shape`` matching ``reference``'s namespace, dtype, and device.

    The end-state replacement for ``backend.zeros(shape)``: rather than a
    configured backend, the new array follows a reference array (e.g. the
    non-gap data a gap is being filled alongside).
    """
    xp = array_api_compat.array_namespace(reference)
    return xp.zeros(shape, dtype=reference.dtype, device=device(reference))

normalize_dtype(namespace, dtype)

Resolve a dtype spec to namespace's own dtype object.

dtype may be a string name ("float32") or a dtype object already in the namespace (e.g. torch.float16); returns the namespace's matching dtype. Lets a caller accept a friendly "float32" and hand the result straight to xp ops, with no per-backend if ladder.

Raises:

Type Description
ValueError

if dtype is not a known numeric/bool dtype of namespace.

Source code in src/sgnts/base/backend.py
def normalize_dtype(namespace: Any, dtype: Any) -> Any:
    """Resolve a dtype spec to ``namespace``'s own dtype object.

    ``dtype`` may be a string name (``"float32"``) or a dtype object already in
    the namespace (e.g. ``torch.float16``); returns the namespace's matching
    dtype. Lets a caller accept a friendly ``"float32"`` and hand the result
    straight to ``xp`` ops, with no per-backend ``if`` ladder.

    Raises:
        ValueError: if ``dtype`` is not a known numeric/bool dtype of ``namespace``.
    """
    resolved = getattr(namespace, dtype, None) if isinstance(dtype, str) else dtype
    if resolved is not None and namespace.isdtype(resolved, ("bool", "numeric")):
        return resolved
    raise ValueError(f"unsupported dtype {dtype!r} for this backend")