Skip to content

Indexing pattern reference

Every NumPy indexing idiom is modeled by an IndexTransform: a domain (the result's coordinates) and one output map per source dimension. This page builds that model by hand for each idiom, so the anatomy is explicit — which map kind an idiom needs, where the offset and stride go, and how an index array's shape spells outer-product versus pointwise. Each model is then proven equal to what the selection compiler derives, and its values are checked against NumPy.

The idiom-to-model matrix

Each idiom over a 6-by-8 image, shown two ways: the Python construction, and the same transform in wire form — the ndsel canonical body to_json produces. Both spell the whole object: a domain whose extent is the result shape, then one output map per source dimension. The index-array variables (rows, columns, mask_rows, mask_columns = np.nonzero(mask), and friends) are defined in the executable matrix at the end of the page.

image[1:5, ::2] — box. The offset picks where cell 0 reads; the stride skips:

IndexTransform(
    domain=IndexDomain.from_shape((4, 4)),
    output=(
        DimensionMap(input_dimension=0, offset=1),
        DimensionMap(input_dimension=1, stride=2),
    ),
)
{
 "input_rank": 2,
 "input_inclusive_min": [0, 0],
 "input_exclusive_max": [4, 4],
 "input_labels": ["", ""],
 "output": [
  {"offset": 1, "stride": 1, "input_dimension": 0},
  {"offset": 0, "stride": 2, "input_dimension": 1}
 ]
}

image[2, :] — box. A rank-1 domain with two output maps: the dropped axis survives as the wire's constant form, a bare {"offset": 2}:

IndexTransform(
    domain=IndexDomain.from_shape((8,)),
    output=(ConstantMap(2), DimensionMap(input_dimension=0)),
)
{
 "input_rank": 1,
 "input_inclusive_min": [0],
 "input_exclusive_max": [8],
 "input_labels": [""],
 "output": [
  {"offset": 2},
  {"offset": 0, "stride": 1, "input_dimension": 0}
 ]
}

image[::-2, :] — box. Reversal is nothing but a negative stride, and the offset is where cell 0 reads (row 5):

IndexTransform(
    domain=IndexDomain.from_shape((3, 8)),
    output=(
        DimensionMap(input_dimension=0, offset=5, stride=-2),
        DimensionMap(input_dimension=1),
    ),
)
{
 "input_rank": 2,
 "input_inclusive_min": [0, 0],
 "input_exclusive_max": [3, 8],
 "input_labels": ["", ""],
 "output": [
  {"offset": 5, "stride": -2, "input_dimension": 0},
  {"offset": 0, "stride": 1, "input_dimension": 1}
 ]
}

image[2:2, :] — box. Emptiness lives in the domain (input_exclusive_max[0] equals the minimum); the maps are ordinary:

IndexTransform(
    domain=IndexDomain.from_shape((0, 8)),
    output=(
        DimensionMap(input_dimension=0, offset=2),
        DimensionMap(input_dimension=1),
    ),
)
{
 "input_rank": 2,
 "input_inclusive_min": [0, 0],
 "input_exclusive_max": [0, 8],
 "input_labels": ["", ""],
 "output": [
  {"offset": 2, "stride": 1, "input_dimension": 0},
  {"offset": 0, "stride": 1, "input_dimension": 1}
 ]
}

image[mask] — query. A mask is its nonzero coordinates: two correlated index arrays over one flat axis, entry i of each pairing into one cell:

IndexTransform(
    domain=IndexDomain.from_shape((10,)),
    output=(ArrayMap(mask_rows), ArrayMap(mask_columns)),
)
{
 "input_rank": 1,
 "input_inclusive_min": [0],
 "input_exclusive_max": [10],
 "input_labels": [""],
 "output": [
  {"offset": 0, "stride": 1, "index_array": [0, 0, 1, 1, 2, 3, 3, 4, 5, 5], "index_array_bounds": ["-inf", "+inf"]},
  {"offset": 0, "stride": 1, "index_array": [0, 5, 2, 7, 4, 1, 6, 3, 0, 5], "index_array_bounds": ["-inf", "+inf"]}
 ]
}

image[np.ix_(rows, columns)] — query. The outer product is spelled by nesting: [[4], [1], [1]] varies down the first axis, [[2, 5]] across the second, each singleton along the other:

IndexTransform(
    domain=IndexDomain.from_shape((3, 2)),
    output=(ArrayMap(rows.reshape(3, 1)), ArrayMap(columns.reshape(1, 2))),
)
{
 "input_rank": 2,
 "input_inclusive_min": [0, 0],
 "input_exclusive_max": [3, 2],
 "input_labels": ["", ""],
 "output": [
  {"offset": 0, "stride": 1, "index_array": [[4], [1], [1]], "index_array_bounds": ["-inf", "+inf"]},
  {"offset": 0, "stride": 1, "index_array": [[2, 5]], "index_array_bounds": ["-inf", "+inf"]}
 ]
}

image[vector_rows, vector_columns] — query. Pointwise: two flat arrays over one shared axis:

IndexTransform(
    domain=IndexDomain.from_shape((3,)),
    output=(ArrayMap(vector_rows), ArrayMap(vector_columns)),
)
{
 "input_rank": 1,
 "input_inclusive_min": [0],
 "input_exclusive_max": [3],
 "input_labels": [""],
 "output": [
  {"offset": 0, "stride": 1, "index_array": [4, 1, 1], "index_array_bounds": ["-inf", "+inf"]},
  {"offset": 0, "stride": 1, "index_array": [2, 5, 2], "index_array_bounds": ["-inf", "+inf"]}
 ]
}

image[broadcast_rows, broadcast_columns] — query. NumPy broadcasting, materialized: each map carries the full (2, 3) block:

IndexTransform(
    domain=IndexDomain.from_shape((2, 3)),
    output=(
        ArrayMap(np.broadcast_to(broadcast_rows, (2, 3))),
        ArrayMap(np.broadcast_to(broadcast_columns, (2, 3))),
    ),
)
{
 "input_rank": 2,
 "input_inclusive_min": [0, 0],
 "input_exclusive_max": [2, 3],
 "input_labels": ["", ""],
 "output": [
  {"offset": 0, "stride": 1, "index_array": [[0, 0, 0], [3, 3, 3]], "index_array_bounds": ["-inf", "+inf"]},
  {"offset": 0, "stride": 1, "index_array": [[1, 4, 6], [1, 4, 6]], "index_array_bounds": ["-inf", "+inf"]}
 ]
}

image[rows, 2:6] — query. One lookup table (order and repeats kept) beside one ordinary affine map — one index array makes the whole selection a query:

IndexTransform(
    domain=IndexDomain.from_shape((3, 4)),
    output=(
        ArrayMap(rows.reshape(3, 1)),
        DimensionMap(input_dimension=1, offset=2),
    ),
)
{
 "input_rank": 2,
 "input_inclusive_min": [0, 0],
 "input_exclusive_max": [3, 4],
 "input_labels": ["", ""],
 "output": [
  {"offset": 0, "stride": 1, "index_array": [[4], [1], [1]], "index_array_bounds": ["-inf", "+inf"]},
  {"offset": 2, "stride": 1, "input_dimension": 1}
 ]
}

Two structural rules do all the work:

  • Category: ConstantMap and DimensionMap entries keep a selection a box at any composition depth; one ArrayMap makes it a query permanently. See the design notes for why consumers dispatch on this.
  • Fancy flavor is spelled by shape: index arrays varying over distinct axes (singleton elsewhere) form an outer product; arrays sharing their non-singleton axes pair pointwise.

The executable matrix

Each case hand-builds the model, checks shape, category, and NumPy values (resolved through the public reader), then proves the selection compiler derives the same transform. One wrinkle the last assert documents: compiled basic selections keep literal domains (t[1:5, ...] starts at coordinate 1 — see Positions vs literal coordinates), so they equal the zero-origin models after translate_domain_to:

class PatternCase(TypedDict):
    """One indexing idiom: its hand-built transform model and its NumPy result."""

    name: str
    mode: Literal["basic", "oindex", "vindex"]
    selection: Any
    transform: IndexTransform
    expected: Any
    shape: tuple[int, ...]
    category: Literal["box", "query"]


image = np.arange(48).reshape(6, 8)
rows = np.array([4, 1, 1], dtype=np.intp)
columns = np.array([2, 5], dtype=np.intp)
mask = image % 5 == 0
mask_rows, mask_columns = np.nonzero(mask)
vector_rows = np.array([4, 1, 1], dtype=np.intp)
vector_columns = np.array([2, 5, 2], dtype=np.intp)
broadcast_rows = np.array([[0], [3]], dtype=np.intp)
broadcast_columns = np.array([[1, 4, 6]], dtype=np.intp)

PATTERN_CASES: tuple[PatternCase, ...] = (
    {
        # image[1:5, ::2] — an offset picks where cell 0 reads; a stride skips.
        "name": "basic-slice",
        "mode": "basic",
        "selection": (slice(1, 5), slice(None, None, 2)),
        "transform": IndexTransform(
            domain=IndexDomain.from_shape((4, 4)),
            output=(
                DimensionMap(input_dimension=0, offset=1),
                DimensionMap(input_dimension=1, stride=2),
            ),
        ),
        "expected": image[1:5, ::2],
        "shape": (4, 4),
        "category": "box",
    },
    {
        # image[2, :] — the dropped axis survives as a ConstantMap: the result
        # is rank 1, but there is still one output map per source dimension.
        "name": "integer-axis-removal",
        "mode": "basic",
        "selection": (2, slice(None)),
        "transform": IndexTransform(
            domain=IndexDomain.from_shape((8,)),
            output=(ConstantMap(2), DimensionMap(input_dimension=0)),
        ),
        "expected": image[2, :],
        "shape": (8,),
        "category": "box",
    },
    {
        # image[::-2, :] — reversal is only a negative stride; the offset is
        # where result cell 0 reads (the last selected row, 5).
        "name": "negative-stride",
        "mode": "basic",
        "selection": (slice(None, None, -2), slice(None)),
        "transform": IndexTransform(
            domain=IndexDomain.from_shape((3, 8)),
            output=(
                DimensionMap(input_dimension=0, offset=5, stride=-2),
                DimensionMap(input_dimension=1),
            ),
        ),
        "expected": image[::-2, :],
        "shape": (3, 8),
        "category": "box",
    },
    {
        # image[2:2, :] — emptiness lives in the domain; the maps are ordinary.
        "name": "empty-selection",
        "mode": "basic",
        "selection": (slice(2, 2), slice(None)),
        "transform": IndexTransform(
            domain=IndexDomain.from_shape((0, 8)),
            output=(
                DimensionMap(input_dimension=0, offset=2),
                DimensionMap(input_dimension=1),
            ),
        ),
        "expected": image[2:2, :],
        "shape": (0, 8),
        "category": "box",
    },
    {
        # image[mask] — a mask is its nonzero coordinates: two correlated
        # ArrayMaps over one flat result axis, row i paired with column i.
        "name": "boolean-mask",
        "mode": "vindex",
        "selection": mask,
        "transform": IndexTransform(
            domain=IndexDomain.from_shape((10,)),
            output=(ArrayMap(mask_rows), ArrayMap(mask_columns)),
        ),
        "expected": image[mask],
        "shape": (10,),
        "category": "query",
    },
    {
        # image[np.ix_(rows, columns)] — the outer product is spelled by shape:
        # each array varies over its own distinct axis, singleton on the other.
        "name": "orthogonal",
        "mode": "oindex",
        "selection": (rows, columns),
        "transform": IndexTransform(
            domain=IndexDomain.from_shape((3, 2)),
            output=(ArrayMap(rows.reshape(3, 1)), ArrayMap(columns.reshape(1, 2))),
        ),
        "expected": image[np.ix_(rows, columns)],
        "shape": (3, 2),
        "category": "query",
    },
    {
        # image[vector_rows, vector_columns] — pointwise: both arrays share
        # the same axis, so entry i of each pairs into one coordinate.
        "name": "vectorized",
        "mode": "vindex",
        "selection": (vector_rows, vector_columns),
        "transform": IndexTransform(
            domain=IndexDomain.from_shape((3,)),
            output=(ArrayMap(vector_rows), ArrayMap(vector_columns)),
        ),
        "expected": image[vector_rows, vector_columns],
        "shape": (3,),
        "category": "query",
    },
    {
        # image[broadcast_rows, broadcast_columns] — NumPy broadcasting,
        # materialized: each map carries the full (2, 3) broadcast block.
        "name": "broadcasting",
        "mode": "vindex",
        "selection": (broadcast_rows, broadcast_columns),
        "transform": IndexTransform(
            domain=IndexDomain.from_shape((2, 3)),
            output=(
                ArrayMap(np.broadcast_to(broadcast_rows, (2, 3))),
                ArrayMap(np.broadcast_to(broadcast_columns, (2, 3))),
            ),
        ),
        "expected": image[broadcast_rows, broadcast_columns],
        "shape": (2, 3),
        "category": "query",
    },
    {
        # image[rows, 2:6] — one lookup-table axis (repeats and order kept)
        # beside one ordinary affine axis: one ArrayMap makes the whole
        # selection a query.
        "name": "repeated-out-of-order",
        "mode": "oindex",
        "selection": (rows, slice(2, 6)),
        "transform": IndexTransform(
            domain=IndexDomain.from_shape((3, 4)),
            output=(
                ArrayMap(rows.reshape(3, 1)),
                DimensionMap(input_dimension=1, offset=2),
            ),
        ),
        "expected": image[rows, 2:6],
        "shape": (3, 4),
        "category": "query",
    },
)

base = IndexTransform.from_shape(image.shape)


def resolve(transform: IndexTransform) -> np.ndarray[Any, Any]:
    """Materialize a transform against `image` through the public reader."""
    out = np.empty(transform.domain.shape, dtype=image.dtype)
    numpy_reader.read_into(image, ReadContext(transform), out)
    return out


for case in PATTERN_CASES:
    transform = case["transform"]

    # The model is the idiom: shape, values, and category all follow from it.
    assert transform.domain.shape == case["shape"]
    np.testing.assert_array_equal(resolve(transform), case["expected"])
    is_query = any(isinstance(m, ArrayMap) for m in transform.output)
    assert ("query" if is_query else "box") == case["category"]

    # The selection compiler derives the same transform. Compiled basic
    # selections keep literal domains (t[1:5, ...] starts at 1, not 0);
    # re-zeroing exposes the equality with the NumPy-shaped model.
    compiled = base[case["selection"]] if case["mode"] == "basic" else (
        getattr(base, case["mode"])[case["selection"]]
    )
    assert compiled.translate_domain_to((0,) * compiled.input_rank) == transform

LazyArray adds nothing to these semantics: it is a regular array-like API whose .lazy, .lazy.oindex, and .lazy.vindex accessors compile the same dialects to the same transforms — the only difference is the return type, a view instead of an array. The test suite holds the wrapper to this matrix.

Positions vs literal coordinates

Surface Meaning of an integer index Meaning of -1
IndexDomain and IndexTransform A literal coordinate in the current domain The address -1, when the domain contains it
LazyArray.lazy A NumPy-style position in the current view The last position, normalized before transform composition

The wrapper's three indexing modes all use positions in the current view. Each derived view begins at position zero, while the transform algebra underneath retains literal coordinates. The Coordinates are addresses section develops that distinction with non-zero and negative-origin domains.