Source code for hawk._plane_layout

# Copyright 2026 Alessandro Masat
# SPDX-License-Identifier: Apache-2.0

"""How a plane's layout is classified (component-major, free sample-major
view, single sample), and the refusal texts for the rest."""

from __future__ import annotations

from .ir import HawkError

# -- Sample-major planes. A per-sample plane of declared width w >= 2 is
# component-major, (w, L); most array code stores vectors sample-major,
# (L, w). HAWK binds by ADDRESS and never copies, so it accepts a
# sample-major plane only when its transpose is C-contiguous (its bytes
# ARE the native (w, L) layout); any other is refused with the fix named,
# or copied by eagle's doors under an `eagle.LayoutWarning`.
#
# A shape that reads BOTH ways -- (w, w), i.e. N == w -- is AMBIGUOUS and is
# refused, never guessed. The caller says which axis holds the samples, zero
# copy, in two ways: per call, `layout="samples_first"` (planes are (N, w)) or
# `layout="samples_last"` (planes are (w, N), native), which resolves every
# ambiguous plane of the call; per array, `hawk.samples_first(x)` /
# `hawk.samples_last(x)`, honoured for ANY shape, refused when it contradicts
# the shape, and winning over the call's `layout=`. The marker PROTOCOL is
# shared with eagle (which hawk does not import): any object carrying
# `__raptor_samples_axis__` ("first" or "last") and the wrapped array as
# `.array` is a marker, so each package accepts the other's. A marker on one
# sample's value is ignored. ----------------------------------------------- #

MARK_ATTR = "__raptor_samples_axis__"
LAYOUTS = ("samples_first", "samples_last")


class _Marked:
    """One array with the axis that holds its samples declared."""

    __slots__ = ("array", "__raptor_samples_axis__")

    def __init__(self, array, axis):
        self.array = array
        self.__raptor_samples_axis__ = axis

    def __repr__(self):
        return f"hawk.samples_{self.__raptor_samples_axis__}({self.array!r})"


[docs] def samples_first(x): """Mark ``x`` as sample-major: its FIRST axis holds the samples, ``(N, w)``. Zero-copy, honoured for any shape, accepted by every hawk (and eagle) door that binds per-sample planes, and it beats the call's ``layout=``; a shape that contradicts it is refused naming the argument. hawk still binds by address, so a ``samples_first`` plane must be a free transposed view (its ``.T`` C-contiguous), as any sample-major plane.""" return _Marked(x, "first")
[docs] def samples_last(x): """Mark ``x`` as component-major (native): its LAST axis holds the samples, ``(w, N)``. Zero-copy, honoured for any shape, beats the call's ``layout=``; a shape that contradicts it is refused naming the argument.""" return _Marked(x, "last")
def check_layout(layout) -> None: """Refuse a ``layout=`` that is not ``None``, ``"samples_first"`` or ``"samples_last"``.""" if layout is not None and layout not in LAYOUTS: raise HawkError( f"layout={layout!r} is not one of 'samples_first' (planes are " f"(N, w)), 'samples_last' (planes are (w, N)), or None") def split_marks(arrays: dict, layout=None): """``(plain, axes)``: ``arrays`` with each marker unwrapped to its array, and ``{name: "first"|"last"}`` for the marked names. ``layout`` is validated. ``arrays`` itself is returned when nothing is marked.""" check_layout(layout) axes = {} for name, value in arrays.items(): axis = getattr(value, MARK_ATTR, None) if axis in ("first", "last") and hasattr(value, "array"): axes[name] = axis if not axes: return arrays, axes return {n: (v.array if n in axes else v) for n, v in arrays.items()}, axes def _is_single(shape: tuple, width: int) -> bool: """Whether ``shape`` is ONE sample of a plane of declared ``width``. A shape that is also a batch is a batch: ``(1,)``, ``(w, 1)``, ``(1, w)`` and ``(w, w)`` are never one sample.""" if width <= 1: return shape == () if shape == (width,): return True return (len(shape) == 2 and shape[0] * shape[1] == width and shape[0] not in (1, width)) def plane_layout(mv: memoryview, width: int, axis=None, layout=None) -> str: """How a bound plane of declared per-sample ``width`` is laid out. ``"single"``: one sample. ``"native"``: anything not sample-major 2-D. ``"view"``: a sample-major plane whose transpose is C-contiguous, bound zero-copy. ``"copy"``: one only a copy could bind, refused. ``"ambiguous"``: ``(w, w)``, which reads both ways, refused unless ``axis`` (the array's own marker, ``"first"``/``"last"``) or ``layout`` (the call's, ``"samples_first"``/``"samples_last"``) says which; the marker wins. ``"conflict"``: a marker the shape contradicts, refused.""" shape = tuple(mv.shape) if _is_single(shape, width): return "single" if width <= 1 or len(shape) != 2: return "native" is_native, is_sample_major = shape[0] == width, shape[1] == width if axis is None and is_native and is_sample_major: axis = {"samples_first": "first", "samples_last": "last"}.get(layout) if axis is None: return "ambiguous" if axis == "first" and not is_sample_major: return "conflict" if axis == "last" and not is_native: return "conflict" if axis == "last" or (axis is None and (is_native or not is_sample_major)): return "native" return "view" if mv.f_contiguous else "copy" class LayoutChoice: """One call's layout choices: the per-call ``layout`` and the per-array markers. :meth:`of` classifies one bound plane under them.""" __slots__ = ("layout", "axes") def __init__(self, layout=None, axes=None): self.layout, self.axes = layout, axes or {} def of(self, name: str, mv: memoryview, width: int) -> str: return plane_layout(mv, width, self.axes.get(name), self.layout) _NO_CHOICE = LayoutChoice() def _native_extent(mv: memoryview, width: int, layout: str | None = None) -> int: """The trailing extent the plane has in its native ``(w, L)`` layout; ``layout`` is the plane's already-settled classification.""" layout = layout or plane_layout(mv, width) if layout == "single": return 1 shape = tuple(mv.shape) if not shape: return 0 return shape[0] if layout in ("view", "copy") else shape[-1] def number_refusal(name: str, value) -> str: """The refusal for a Python number bound to a plane (hawk binds by address; one sample is a 0-d array, never a number).""" return ( f"argument {name!r} was given a Python {type(value).__name__}, which " f"cannot be bound by address or receive a write; pass a writable 0-d " f"array (np.array({name}))" ) def mixed_refusal(single: tuple, batch: tuple) -> str: """The refusal for a call that mixes one sample with a batch.""" (s_name, s_shape), (b_name, b_shape, b_n) = single, batch tile = (f"np.full({b_n}, {s_name})" if s_shape == () else f"np.repeat({s_name}.reshape(-1, 1), {b_n}, axis=1)") return ( f"{s_name!r} is one sample (shape {s_shape}) but {b_name!r} is a batch " f"of {b_n} (shape {b_shape}); a call is one sample or one batch, never " f"both. A value shared by every sample is a Param (scalar) or a Table " f"(vector); a Grid kernel's per-lane input also takes a scalar; or tile it " f"once: {tile}" ) def sample_major_refusal(name: str, shape: tuple, width: int) -> str: """The refusal for a sample-major plane only a copy could bind.""" return ( f"argument {name!r}: given sample-major, shape {shape}, but this " f"kernel's plane is component-major ({width}, L). hawk.runtime binds " f"by address and never copies, so it accepts a sample-major plane only " f"as a zero-copy transposed view (the .T of a contiguous ({width}, L) " f"array). Pass np.ascontiguousarray({name}.T), mark a free view with " f"hawk.samples_first({name}), allocate it as " f"({width}, L), or launch through eagle, which copies it with an " f"eagle.LayoutWarning" ) def ambiguous_refusal(name: str, shape: tuple, width: int) -> str: """The refusal for a ``(w, w)`` plane that reads both ways.""" return ( f"argument {name!r} has shape {shape}, which reads both as " f"component-major ({width}, L) with L = {shape[-1]} (samples last) and " f"as sample-major (L, {width}) with L = {shape[-1]} (samples first); " f"hawk will not guess. Say which axis holds the samples, zero-copy: " f"pass layout=\"samples_first\" or layout=\"samples_last\" to the " f"call (resolves every ambiguous plane), or mark this array: " f"hawk.samples_first({name}) / hawk.samples_last({name})" ) def conflict_refusal(name: str, shape: tuple, width: int, axis: str) -> str: """The refusal for a marker its array's shape contradicts.""" want = f"(L, {width})" if axis == "first" else f"({width}, L)" return ( f"argument {name!r} is marked samples_{axis} (shape {want}) but its " f"shape is {shape}; mark it with hawk.samples_" f"{'last' if axis == 'first' else 'first'}({name}) or drop the marker" ) def layout_refusal(kind: str, name: str, shape: tuple, width: int, axis=None) -> str: """The refusal for a plane classified ``"ambiguous"``, ``"conflict"`` or ``"copy"``.""" if kind == "ambiguous": return ambiguous_refusal(name, shape, width) if kind == "conflict": return conflict_refusal(name, shape, width, axis) return sample_major_refusal(name, shape, width)