Skip to content

zarr_indexing.output_map

zarr_indexing.output_map

Output index maps — three ordered mappings to integer coordinates.

An output index map describes how input cells address one dimension of the output space. Its coordinates form an ordered, duplicate-preserving sequence aligned with the input domain, never a mathematical set. Three representations cover the cases that arise in practice:

  • ConstantMap(offset=5) — every request cell maps to coordinate 5
  • DimensionMap(input_dimension=0, offset=3, stride=2) over input [0, 5) — the ordered arithmetic progression [3, 5, 7, 9, 11]
  • ArrayMap(index_array=[5, 1, 1]) — the explicit sequence [5, 1, 1], preserving both order and the repeated coordinate

Every output map participates in two operations defined on IndexTransform, which provides the input-domain context these maps lack:

  • intersect — retain mapped cells whose coordinates lie within a range (e.g., a chunk), without changing their order or multiplicity. Restricting [3, 5, 5, 9] to [4, 8) produces [5, 5].
  • translate — shift every coordinate by a constant (e.g., make chunk-local). Translating [5, 5, 7] by -4 produces [1, 1, 3].

These two operations are the foundation of chunk resolution: for each chunk, intersect the map with the chunk's range, then translate to chunk-local coordinates.

The three types exist because they trade off generality for efficiency:

  • ConstantMap: O(1) storage, O(1) intersection
  • DimensionMap: O(1) storage, O(1) intersection (analytical)
  • ArrayMap: O(n) storage, O(n) intersection (must scan the array)

Collapsing everything to ArrayMap would be correct but wasteful — a billion-element slice would materialize a billion coordinates just to group them by chunk, when DimensionMap does it with three integers.

OutputIndexMap module-attribute

OutputIndexMap = ConstantMap | DimensionMap | ArrayMap

ArrayMap dataclass

An explicit ordered, duplicate-preserving coordinate mapping.

Maps each input position i to offset + stride * index_array[i]. Index-array order and repeated entries are semantic and remain present in the result. Arises from fancy indexing (e.g., arr[[5, 1, 1]] or boolean masks).

A map used in a transform must have its full input rank: index_array has the enclosing domain's rank, sized fully on the axes it varies over and singleton (size 1) elsewhere. The shape is the single source of truth for what the map depends on — its dependency axes are exactly its axes of size greater than one (see _array_map_dependency_axes) — and it distinguishes the two flavors of multi-array fancy indexing:

  • orthogonal (oindex): each array varies along a single, distinct axis (all others singleton); the result is their outer product.
  • vectorized (vindex): the arrays are correlated and share the same non-singleton (broadcast) axes; the result is a pointwise scatter.

A map holding exactly one coordinate carries no shape to read a dependency from, and none is needed: it is the ConstantMap it equals, and the selection layer builds that instead (see array_map_or_constant). A hand-built all-singleton ArrayMap is still a valid value; resolution classifies it with the correlated maps and reads it pointwise.

Examples:

The fancy selection arr[[5, 1, 1]] reads coordinate 5, then 1, then 1 — order and the duplicate preserved, exactly as NumPy fancy indexing:

>>> m = ArrayMap(index_array=np.array([5, 1, 1]))
>>> [m.offset + m.stride * c for c in m.index_array.tolist()]
[5, 1, 1]
>>> np.arange(10)[[5, 1, 1]].tolist()
[5, 1, 1]
Source code in src/zarr_indexing/output_map.py
@dataclass(frozen=True, slots=True)
class ArrayMap:
    """An explicit ordered, duplicate-preserving coordinate mapping.

    Maps each input position `i` to `offset + stride * index_array[i]`.
    Index-array order and repeated entries are semantic and remain present in
    the result. Arises from fancy indexing (e.g., `arr[[5, 1, 1]]` or boolean
    masks).

    A map used in a transform must have its **full input rank**:
    `index_array` has the enclosing domain's rank, sized
    fully on the axes it varies over and singleton (size 1) elsewhere. The
    shape is the single source of truth for what the map depends on — its
    **dependency axes** are exactly its axes of size greater than one (see
    `_array_map_dependency_axes`) — and it distinguishes the two
    flavors of multi-array fancy indexing:

    - **orthogonal** (`oindex`): each array varies along a single, *distinct*
      axis (all others singleton); the result is their outer product.
    - **vectorized** (`vindex`): the arrays are correlated and share the same
      non-singleton (broadcast) axes; the result is a pointwise scatter.

    A map holding exactly one coordinate carries no shape to read a dependency
    from, and none is needed: it is the `ConstantMap` it equals, and the
    selection layer builds that instead (see `array_map_or_constant`). A
    hand-built all-singleton `ArrayMap` is still a valid value; resolution
    classifies it with the correlated maps and reads it pointwise.

    Examples
    --------
    The fancy selection `arr[[5, 1, 1]]` reads coordinate 5, then 1, then 1
    — order and the duplicate preserved, exactly as NumPy fancy indexing:

    >>> m = ArrayMap(index_array=np.array([5, 1, 1]))
    >>> [m.offset + m.stride * c for c in m.index_array.tolist()]
    [5, 1, 1]
    >>> np.arange(10)[[5, 1, 1]].tolist()
    [5, 1, 1]
    """

    index_array: npt.NDArray[np.integer[Any]]
    """Explicit coordinates at the enclosing transform's full input rank; order and
    duplicates are semantic. Its non-singleton axes are the map's dependency axes."""

    offset: int = 0
    """Constant term of the affine adjustment: the output coordinate is `offset + stride * index_array[i]`."""

    stride: int = 1
    """Multiplier applied to each `index_array` value before `offset` is added."""

    def __post_init__(self) -> None:
        """Own an immutable snapshot of the integer index coordinates.

        The snapshot is backed by immutable bytes, so callers cannot modify it
        or re-enable its WRITEABLE flag. Changes to the supplied array do not
        change the map's coordinates or hash. This freezes the coordinate
        mapping, not the source values read through it."""
        # Immutable bytes are the ultimate owner so callers cannot re-enable
        # the WRITEABLE flag, as they can on a read-only array that owns its
        # allocation. `asarray` also accepts the NumPy scalars that reach here
        # after indexing an array down to one element.
        array = np.asarray(self.index_array)
        if not np.issubdtype(array.dtype, np.integer):
            raise TypeError(f"index_array must have an integer dtype, got {array.dtype}")
        normalized = checked_affine(0, 1, array)
        frozen = np.frombuffer(normalized.tobytes(), dtype=np.intp).reshape(normalized.shape)
        object.__setattr__(self, "index_array", frozen)

    def __reduce__(self) -> tuple[object, tuple[object, int, int]]:
        """Reconstruct through `__init__`, preserving the ownership invariant."""
        return (
            type(self),
            (self.index_array, self.offset, self.stride),
        )

    def _with_affine(self, offset: int, stride: int) -> ArrayMap:
        """Return a map with a different affine adjustment.

        Share the immutable index array while replacing the offset and stride.
        This preserves coordinate ownership without copying the array."""
        new = object.__new__(ArrayMap)
        object.__setattr__(new, "index_array", self.index_array)
        object.__setattr__(new, "offset", offset)
        object.__setattr__(new, "stride", stride)
        return new

    def __eq__(self, other: object) -> bool:
        """Compare offset, stride, array shape, and index values.

        Return a scalar boolean for another ArrayMap and NotImplemented for
        other types."""
        if not isinstance(other, ArrayMap):
            return NotImplemented
        return (
            self.offset == other.offset
            and self.stride == other.stride
            and self.index_array.shape == other.index_array.shape
            and bool(np.array_equal(self.index_array, other.index_array))
        )

    def __hash__(self) -> int:
        """Hash the offset, stride, array shape, and index bytes.

        The immutable coordinate snapshot keeps the hash stable, and equal
        maps have equal hashes."""
        return hash(
            (
                self.offset,
                self.stride,
                self.index_array.shape,
                self.index_array.tobytes(),
            )
        )

    @property
    def dependency_axes(self) -> tuple[int, ...]:
        """Structural dependency axes: axes of size greater than one.

        Axes of size greater than one are reported, regardless of coordinate
        values or a zero-size axis elsewhere. Whether the whole transform is
        orthogonal also depends on how other maps use these axes; a single
        map's shape does not establish independence.

        Examples
        --------
        >>> ArrayMap(index_array=np.array([[4, 0, 2]])).dependency_axes
        (1,)
        >>> ArrayMap(index_array=np.array([[1, 2], [3, 4]])).dependency_axes
        (0, 1)
        """
        return _array_map_dependency_axes(self.index_array)

    @property
    def dependent_axis(self) -> int | None:
        """Return the single input axis an orthogonal `ArrayMap` varies over.

        This is the array's one non-singleton axis, read from the shape — the
        single source of truth for what a map depends on. The selection layer
        collapses a single-coordinate map to a `ConstantMap`
        (`array_map_or_constant`), so a non-empty map built by this package always
        has at least one dependency axis.

        Returns
        -------
        int or None
            The axis the map varies over, or `None` when it varies over no input
            axis of size greater than one, such as an all-singleton map. `None`
            is a valid result, not an error; such maps resolve through the
            pointwise (general) path.

        Raises
        ------
        ValueError
            If the map varies over more than one axis, which makes it correlated
            rather than orthogonal.

        Examples
        --------
        An `oindex` selection on axis 1 of a rank-2 transform stores its
        coordinates full-sized on axis 1 and singleton on axis 0, so the
        dependency axis is read straight off the shape:

        >>> m = ArrayMap(index_array=np.array([[4, 0, 2]]))
        >>> m.index_array.shape
        (1, 3)
        >>> m.dependent_axis
        1
        """
        dep = self.dependency_axes
        if len(dep) == 1:
            return dep[0]
        if len(dep) == 0:
            return None
        raise ValueError(
            f"orthogonal ArrayMap must vary over exactly one axis; got dependency axes {dep}"
        )

    def to_json(self) -> OutputIndexMapJSON:
        """Convert to the canonical wire form, collapsing a degenerate map.

        A map holding exactly one coordinate, or none at all, is emitted as a
        `constant` map — see the module note on the wire format in
        [`zarr_indexing.json`][zarr_indexing.json]. Both are degenerate: the
        first selects one coordinate whatever the input, and the second names
        no cell and can only be empty because an input dimension is, so the
        emptiness travels in the domain instead.

        Examples
        --------
        >>> ArrayMap(np.array([[4], [1], [1]])).to_json()["index_array"]
        [[4], [1], [1]]
        >>> ArrayMap(np.array([7])).to_json()  # degenerate: one coordinate
        {'offset': 7}
        """
        if self.index_array.size == 1:
            value = int(self.index_array.reshape(-1)[0])
            return {"offset": self.offset + self.stride * value}
        if self.index_array.size == 0:
            return {"offset": 0}
        return {
            "offset": self.offset,
            "stride": self.stride,
            "index_array": self.index_array.tolist(),
            "index_array_bounds": ["-inf", "+inf"],
        }

dependency_axes property

dependency_axes: tuple[int, ...]

Structural dependency axes: axes of size greater than one.

Axes of size greater than one are reported, regardless of coordinate values or a zero-size axis elsewhere. Whether the whole transform is orthogonal also depends on how other maps use these axes; a single map's shape does not establish independence.

Examples:

>>> ArrayMap(index_array=np.array([[4, 0, 2]])).dependency_axes
(1,)
>>> ArrayMap(index_array=np.array([[1, 2], [3, 4]])).dependency_axes
(0, 1)

dependent_axis property

dependent_axis: int | None

Return the single input axis an orthogonal ArrayMap varies over.

This is the array's one non-singleton axis, read from the shape — the single source of truth for what a map depends on. The selection layer collapses a single-coordinate map to a ConstantMap (array_map_or_constant), so a non-empty map built by this package always has at least one dependency axis.

Returns:

  • int or None

    The axis the map varies over, or None when it varies over no input axis of size greater than one, such as an all-singleton map. None is a valid result, not an error; such maps resolve through the pointwise (general) path.

Raises:

  • ValueError

    If the map varies over more than one axis, which makes it correlated rather than orthogonal.

Examples:

An oindex selection on axis 1 of a rank-2 transform stores its coordinates full-sized on axis 1 and singleton on axis 0, so the dependency axis is read straight off the shape:

>>> m = ArrayMap(index_array=np.array([[4, 0, 2]]))
>>> m.index_array.shape
(1, 3)
>>> m.dependent_axis
1

index_array instance-attribute

index_array: NDArray[integer[Any]]

Explicit coordinates at the enclosing transform's full input rank; order and duplicates are semantic. Its non-singleton axes are the map's dependency axes.

offset class-attribute instance-attribute

offset: int = 0

Constant term of the affine adjustment: the output coordinate is offset + stride * index_array[i].

stride class-attribute instance-attribute

stride: int = 1

Multiplier applied to each index_array value before offset is added.

__eq__

__eq__(other: object) -> bool

Compare offset, stride, array shape, and index values.

Return a scalar boolean for another ArrayMap and NotImplemented for other types.

Source code in src/zarr_indexing/output_map.py
def __eq__(self, other: object) -> bool:
    """Compare offset, stride, array shape, and index values.

    Return a scalar boolean for another ArrayMap and NotImplemented for
    other types."""
    if not isinstance(other, ArrayMap):
        return NotImplemented
    return (
        self.offset == other.offset
        and self.stride == other.stride
        and self.index_array.shape == other.index_array.shape
        and bool(np.array_equal(self.index_array, other.index_array))
    )

__hash__

__hash__() -> int

Hash the offset, stride, array shape, and index bytes.

The immutable coordinate snapshot keeps the hash stable, and equal maps have equal hashes.

Source code in src/zarr_indexing/output_map.py
def __hash__(self) -> int:
    """Hash the offset, stride, array shape, and index bytes.

    The immutable coordinate snapshot keeps the hash stable, and equal
    maps have equal hashes."""
    return hash(
        (
            self.offset,
            self.stride,
            self.index_array.shape,
            self.index_array.tobytes(),
        )
    )

__init__

__init__(
    index_array: NDArray[integer[Any]],
    offset: int = 0,
    stride: int = 1,
) -> None

__post_init__

__post_init__() -> None

Own an immutable snapshot of the integer index coordinates.

The snapshot is backed by immutable bytes, so callers cannot modify it or re-enable its WRITEABLE flag. Changes to the supplied array do not change the map's coordinates or hash. This freezes the coordinate mapping, not the source values read through it.

Source code in src/zarr_indexing/output_map.py
def __post_init__(self) -> None:
    """Own an immutable snapshot of the integer index coordinates.

    The snapshot is backed by immutable bytes, so callers cannot modify it
    or re-enable its WRITEABLE flag. Changes to the supplied array do not
    change the map's coordinates or hash. This freezes the coordinate
    mapping, not the source values read through it."""
    # Immutable bytes are the ultimate owner so callers cannot re-enable
    # the WRITEABLE flag, as they can on a read-only array that owns its
    # allocation. `asarray` also accepts the NumPy scalars that reach here
    # after indexing an array down to one element.
    array = np.asarray(self.index_array)
    if not np.issubdtype(array.dtype, np.integer):
        raise TypeError(f"index_array must have an integer dtype, got {array.dtype}")
    normalized = checked_affine(0, 1, array)
    frozen = np.frombuffer(normalized.tobytes(), dtype=np.intp).reshape(normalized.shape)
    object.__setattr__(self, "index_array", frozen)

__reduce__

__reduce__() -> tuple[object, tuple[object, int, int]]

Reconstruct through __init__, preserving the ownership invariant.

Source code in src/zarr_indexing/output_map.py
def __reduce__(self) -> tuple[object, tuple[object, int, int]]:
    """Reconstruct through `__init__`, preserving the ownership invariant."""
    return (
        type(self),
        (self.index_array, self.offset, self.stride),
    )

to_json

to_json() -> OutputIndexMapJSON

Convert to the canonical wire form, collapsing a degenerate map.

A map holding exactly one coordinate, or none at all, is emitted as a constant map — see the module note on the wire format in zarr_indexing.json. Both are degenerate: the first selects one coordinate whatever the input, and the second names no cell and can only be empty because an input dimension is, so the emptiness travels in the domain instead.

Examples:

>>> ArrayMap(np.array([[4], [1], [1]])).to_json()["index_array"]
[[4], [1], [1]]
>>> ArrayMap(np.array([7])).to_json()  # degenerate: one coordinate
{'offset': 7}
Source code in src/zarr_indexing/output_map.py
def to_json(self) -> OutputIndexMapJSON:
    """Convert to the canonical wire form, collapsing a degenerate map.

    A map holding exactly one coordinate, or none at all, is emitted as a
    `constant` map — see the module note on the wire format in
    [`zarr_indexing.json`][zarr_indexing.json]. Both are degenerate: the
    first selects one coordinate whatever the input, and the second names
    no cell and can only be empty because an input dimension is, so the
    emptiness travels in the domain instead.

    Examples
    --------
    >>> ArrayMap(np.array([[4], [1], [1]])).to_json()["index_array"]
    [[4], [1], [1]]
    >>> ArrayMap(np.array([7])).to_json()  # degenerate: one coordinate
    {'offset': 7}
    """
    if self.index_array.size == 1:
        value = int(self.index_array.reshape(-1)[0])
        return {"offset": self.offset + self.stride * value}
    if self.index_array.size == 0:
        return {"offset": 0}
    return {
        "offset": self.offset,
        "stride": self.stride,
        "index_array": self.index_array.tolist(),
        "index_array_bounds": ["-inf", "+inf"],
    }

ConstantMap dataclass

A constant output-coordinate mapping.

Every input cell maps to offset. Arises from integer indexing (e.g., arr[5] fixes one dimension to coordinate 5).

Examples:

Every input cell maps to the same output coordinate, like broadcasting coordinate 5 with np.broadcast_to(5, (3,)). Repeated fancy indices can also describe these coordinates, using an explicit list:

>>> from zarr_indexing.domain import IndexDomain
>>> from zarr_indexing.transform import IndexTransform
>>> domain = IndexDomain.from_shape((3,))
>>> t = IndexTransform(domain=domain, output=(ConstantMap(offset=5),))
>>> t.apply((0,)), t.apply((1,)), t.apply((2,))
((5,), (5,), (5,))
Source code in src/zarr_indexing/output_map.py
@dataclass(frozen=True, slots=True)
class ConstantMap:
    """A constant output-coordinate mapping.

    Every input cell maps to `offset`. Arises from integer indexing (e.g.,
    `arr[5]` fixes one dimension to coordinate 5).

    Examples
    --------
    Every input cell maps to the same output coordinate, like broadcasting
    coordinate 5 with `np.broadcast_to(5, (3,))`. Repeated fancy indices can
    also describe these coordinates, using an explicit list:

    >>> from zarr_indexing.domain import IndexDomain
    >>> from zarr_indexing.transform import IndexTransform
    >>> domain = IndexDomain.from_shape((3,))
    >>> t = IndexTransform(domain=domain, output=(ConstantMap(offset=5),))
    >>> t.apply((0,)), t.apply((1,)), t.apply((2,))
    ((5,), (5,), (5,))
    """

    offset: int = 0
    """The fixed output coordinate every input cell maps to."""

    def to_json(self) -> OutputIndexMapJSON:
        """Convert to the canonical wire form: the bare `constant` map.

        Examples
        --------
        >>> ConstantMap(5).to_json()
        {'offset': 5}
        """
        return {"offset": self.offset}

offset class-attribute instance-attribute

offset: int = 0

The fixed output coordinate every input cell maps to.

__init__

__init__(offset: int = 0) -> None

to_json

to_json() -> OutputIndexMapJSON

Convert to the canonical wire form: the bare constant map.

Examples:

>>> ConstantMap(5).to_json()
{'offset': 5}
Source code in src/zarr_indexing/output_map.py
def to_json(self) -> OutputIndexMapJSON:
    """Convert to the canonical wire form: the bare `constant` map.

    Examples
    --------
    >>> ConstantMap(5).to_json()
    {'offset': 5}
    """
    return {"offset": self.offset}

DimensionMap dataclass

An ordered affine mapping to output coordinates.

Maps each input coordinate i to offset + stride * i, where the input range comes from the enclosing IndexTransform's domain. Arises from slice indexing (e.g., arr[2:10:3] gives offset=2, stride=3).

Examples:

The slice arr[2:11:3] reads coordinates 2, 5, 8 — the rule offset + stride * i with offset=2, stride=3:

>>> m = DimensionMap(input_dimension=0, offset=2, stride=3)
>>> [m.offset + m.stride * i for i in range(3)]
[2, 5, 8]
>>> np.arange(11)[2:11:3].tolist()
[2, 5, 8]
Source code in src/zarr_indexing/output_map.py
@dataclass(frozen=True, slots=True)
class DimensionMap:
    """An ordered affine mapping to output coordinates.

    Maps each input coordinate `i` to `offset + stride * i`, where the input
    range comes from the enclosing `IndexTransform`'s domain. Arises from slice
    indexing (e.g., `arr[2:10:3]` gives offset=2, stride=3).

    Examples
    --------
    The slice `arr[2:11:3]` reads coordinates `2, 5, 8` — the rule
    `offset + stride * i` with `offset=2`, `stride=3`:

    >>> m = DimensionMap(input_dimension=0, offset=2, stride=3)
    >>> [m.offset + m.stride * i for i in range(3)]
    [2, 5, 8]
    >>> np.arange(11)[2:11:3].tolist()
    [2, 5, 8]
    """

    input_dimension: int
    """The input (domain) dimension whose coordinate this map reads."""

    offset: int = 0
    """The output coordinate that input coordinate `0` maps to."""

    stride: int = 1
    """The output-coordinate step per unit input step; negative walks backward, zero repeats `offset`."""

    def to_json(self) -> OutputIndexMapJSON:
        """Convert to the canonical wire form: the `single_input_dimension` map.

        Examples
        --------
        >>> DimensionMap(input_dimension=1, offset=0, stride=2).to_json()
        {'offset': 0, 'stride': 2, 'input_dimension': 1}
        """
        return {
            "offset": self.offset,
            "stride": self.stride,
            "input_dimension": self.input_dimension,
        }

input_dimension instance-attribute

input_dimension: int

The input (domain) dimension whose coordinate this map reads.

offset class-attribute instance-attribute

offset: int = 0

The output coordinate that input coordinate 0 maps to.

stride class-attribute instance-attribute

stride: int = 1

The output-coordinate step per unit input step; negative walks backward, zero repeats offset.

__init__

__init__(
    input_dimension: int, offset: int = 0, stride: int = 1
) -> None

to_json

to_json() -> OutputIndexMapJSON

Convert to the canonical wire form: the single_input_dimension map.

Examples:

>>> DimensionMap(input_dimension=1, offset=0, stride=2).to_json()
{'offset': 0, 'stride': 2, 'input_dimension': 1}
Source code in src/zarr_indexing/output_map.py
def to_json(self) -> OutputIndexMapJSON:
    """Convert to the canonical wire form: the `single_input_dimension` map.

    Examples
    --------
    >>> DimensionMap(input_dimension=1, offset=0, stride=2).to_json()
    {'offset': 0, 'stride': 2, 'input_dimension': 1}
    """
    return {
        "offset": self.offset,
        "stride": self.stride,
        "input_dimension": self.input_dimension,
    }

array_map_or_constant

array_map_or_constant(
    index_array: NDArray[integer[Any]],
    offset: int = 0,
    stride: int = 1,
) -> ArrayMap | ConstantMap

An ArrayMap, collapsed to the ConstantMap it equals when it can be.

An index array holding exactly one coordinate maps every input cell to the same place; representing it as a lookup table would leave a map whose shape names no dependency axis, the one form the shape-derived classifier cannot read. The selection and composition layers build their array maps through this helper so that a non-empty ArrayMap always varies over at least one axis. An empty array stays an ArrayMap: it maps no cell at all, and the emptiness lives in the domain that accompanies it.

Source code in src/zarr_indexing/output_map.py
def array_map_or_constant(
    index_array: npt.NDArray[np.integer[Any]],
    offset: int = 0,
    stride: int = 1,
) -> ArrayMap | ConstantMap:
    """An `ArrayMap`, collapsed to the `ConstantMap` it equals when it can be.

    An index array holding exactly one coordinate maps every input cell to the
    same place; representing it as a lookup table would leave a map whose shape
    names no dependency axis, the one form the shape-derived classifier cannot
    read. The selection and composition layers build their array maps through
    this helper so that a non-empty `ArrayMap` always varies over at least one
    axis. An empty array stays an `ArrayMap`: it maps no cell at all, and the
    emptiness lives in the domain that accompanies it.
    """
    arr = np.asarray(index_array)
    if arr.size == 1:
        return ConstantMap(offset=checked_affine(offset, stride, int(arr.reshape(-1)[0])))
    return ArrayMap(index_array=arr, offset=offset, stride=stride)

output_index_map_from_json

output_index_map_from_json(
    data: OutputIndexMapJSON,
) -> OutputIndexMap

Construct the output map a canonical wire form names.

The wire form is structurally discriminated: the presence of index_array selects an array map, input_dimension selects a dimension map, and neither selects a constant map.

Array maps with index_array_bounds other than ["-inf", "+inf"] raise NdselError, because the engine cannot retain those constraints.

Examples:

>>> output_index_map_from_json({"offset": 5})
ConstantMap(offset=5)
>>> output_index_map_from_json({"offset": 0, "stride": 2, "input_dimension": 1})
DimensionMap(input_dimension=1, offset=0, stride=2)
Source code in src/zarr_indexing/output_map.py
def output_index_map_from_json(data: OutputIndexMapJSON) -> OutputIndexMap:
    """Construct the output map a canonical wire form names.

    The wire form is structurally discriminated: the presence of `index_array`
    selects an array map, `input_dimension` selects a dimension map, and
    neither selects a constant map.

    Array maps with `index_array_bounds` other than `["-inf", "+inf"]` raise
    `NdselError`, because the engine cannot retain those constraints.

    Examples
    --------
    >>> output_index_map_from_json({"offset": 5})
    ConstantMap(offset=5)
    >>> output_index_map_from_json({"offset": 0, "stride": 2, "input_dimension": 1})
    DimensionMap(input_dimension=1, offset=0, stride=2)
    """
    from zarr_indexing._wire import check_index_array_bounds, lower_index_array

    if "index_array" in data:
        check_index_array_bounds(data.get("index_array_bounds", ["-inf", "+inf"]), "output")
        return ArrayMap(
            index_array=lower_index_array(data["index_array"], "index_array"),
            offset=data.get("offset", 0),
            stride=data.get("stride", 1),
        )
    if "input_dimension" in data:
        return DimensionMap(
            input_dimension=data["input_dimension"],
            offset=data.get("offset", 0),
            stride=data.get("stride", 1),
        )
    return ConstantMap(offset=data.get("offset", 0))