Skip to content

zarr_indexing.lazy_array

LazyArray.lazy[...] is metadata-only: every derived view keeps the same reader and composes its transform without reading data. result() allocates owned system memory, then calls that reader once for each projected part. Rectangular parts write directly into their final slices; advanced placement may first use an owned dense temporary. LazyArray(source) assumes only basic indexing, while LazyArray.from_numpy(array) selects numpy_reader. Both currently use the same slab-and-gather implementation.

The built-in readers lower through NumPy system memory and support sources whose basic reads can be converted there. A device source that refuses NumPy conversion needs a custom reader that transfers into the output buffer. Derived views and parts share their reader and part views may be materialized concurrently, so stateful readers must synchronize their own mutable state.

Every public Partition.view.transform directly maps that view's zero-origin coordinates into its raw Partition.view.array, including for non-first partitions. Partition.projection.chunk_transform intentionally stays local to the selected chunk. During parent materialization (view.result(parts=parts)) the reader receives both frames in one ReadContext: the public global transform in context.transform and the same local plan in context.projection. Use part.result() to execute a partition independently with both frames. Direct part.view.result() calls resolve the general view with no projection.

zarr_indexing.lazy_array

LazyArray — TensorStore-style lazy indexing over array-like sources.

LazyArray wraps a source with shape, dtype, and basic integer/slice __getitem__, whose reads can be lowered through NumPy system memory. It adds a .lazy accessor whose indexing operations build up an IndexTransform instead of reading data:

view = LazyArray(source).lazy[10:50, ::2].lazy.oindex[[3, 1, 1], :]
view.shape          # known without touching the data
values = view.result()

Selection construction does not read source values: result(), __array__, and eager __getitem__ perform reads. Tokenization can also read or hash source data, depending on the source and tokenization path. .lazy operations inspect selection metadata and may copy or process supplied index arrays. Composition does not accumulate wrapper layers: a view of a view is still a single transform and retains its reader.

Parts

A LazyArray carries a partitioning of the array it wraps: a grid of boxes that a read is broken into. parts() walks those boxes as they fall through the view, yielding a Partition per box. Its paired projection describes the chunk-local read and where its cells land in the request; view carries that partition's transform. result() allocates one fresh output buffer, then reads each partition once through the selected reader into the final buffer or an owned temporary for fancy placement.

The part view's transform directly addresses its raw wrapped array. The paired projection deliberately retains the chunk-local frame. Parent materialization passes both in ReadContext, as does independent part.result() execution. Calling part.view.result() resolves the general view without its partition record and therefore uses projection=None. Use part.result() when scheduling partition reads.

The partitioning is discovered from the wrapped array at construction — first read_chunk_sizes (zarr's clipped per-axis sizes, sharding-aware), then chunks, read as per-axis sizes if its entries are sequences and as a uniform box shape if they are integers. Those attribute names belong to the wrapped array; this API refers only to parts. An array that advertises neither gets a single whole-array part, and resolving it reads the whole view through its selected reader in one pass.

with_parts replaces the partitioning without touching the data or the view:

view.with_parts((64, 64))       # uniform boxes, tail clipped
view.with_parts_per_axis(((3, 3, 1),))   # explicit per-axis sizes
view.unpartitioned()           # one whole-array part; resolve in one shot

Repartitioning changes how the read is divided, not what result() returns. Parts that do not align with the source's own boxes are permitted and can be useful for controlling per-read sizes or batching small reads. They can change I/O costs; the intended selected values remain the same for an unchanged source and a conforming reader. The full result buffer is still allocated.

Readers

Every wrapper carries a reader that owns the backend-specific request. The transform answers which values? and is independent of the backend; the reader answers how does this backend obtain them? and must preserve the complete transform exactly. Readers do not define indexing semantics, partitioning, scheduling, or result ownership. The conservative LazyArray(source) uses basic_reader, which needs only basic slicing. LazyArray.from_numpy(array) explicitly opts into numpy_reader for direct NumPy indexing. with_reader() replaces the reader without reading or changing the view metadata. The reader object is shared by all derived views and their parts. Consumers may materialize part views concurrently; LazyArray does not serialize calls, so a stateful reader must synchronize its own mutable state.

The built-in readers lower through NumPy system memory using NumPy conversion of source slices. Device arrays that refuse implicit conversion need a custom reader that explicitly transfers values into the output buffer.

Boxes and queries

is_box reports whether the current transform contains only constant and affine output maps. An index-array gather commonly produces a query, but singleton indices and later selections can remove its index-array maps and make it a box. bounding_box() gives a coordinate hull, and strides() gives stride magnitudes for a box. These describe storage coordinates, not the full result layout or traversal order. A query can fill its hull, and a singleton box can fill its hull even when its recorded stride exceeds one.

The positional dialect

Selections on LazyArray are positional, NumPy-style: index 0 is the first element of the current view, -1 is the last, boolean masks must match the view's shape, integer coordinates are bounds-checked, and slices are clipped to the view's extent.

This differs from the low-level IndexTransform literal-coordinate dialect: a transform can retain a nonzero domain origin, while LazyArray re-zeroes positions on each derived view. The current main Zarr Array does not expose this wrapper as an Array.lazy attribute; use LazyArray(array) explicitly.

Scalar integers drop axes. Non-boolean objects implementing SupportsIndex are accepted as scalar indices and in slice bounds; __int__ alone is not enough. For orthogonal and vectorized modes this wrapper applies scalar indices first, then the remaining advanced selection. Orthogonal indices form an outer product. Vectorized indexing accepts coordinate arrays or a shape-matching boolean mask. An ellipsis can retain unindexed axes ([..., i, j]), but explicit slice entries in vectorized selections are currently rejected. Scalar-first processing can also differ from NumPy's advanced-axis placement: for shape (2, 3, 4), lazy.vindex[0, ..., [1, 2]] has shape (3, 2), whereas NumPy's same selection has shape (2, 3). These modes do not implement every NumPy indexing form.

Materializing on fallback

LazyArray implements __array__ but deliberately implements neither __array_ufunc__ nor __array_function__. Many NumPy operations therefore materialize through __array__ and work on the resulting array: numpy.sum(view), numpy.add(view, 1) and numpy.stack([view, view]) all do, and so does numpy.ones(view.shape) + view, where the ndarray on the left dispatches. Metadata queries such as numpy.shape(view) and numpy.ndim(view) can use the exposed attributes without materializing; other unsupported operations may fail.

Python's arithmetic operators do not: view + 1 raises TypeError, because the wrapper defines no arithmetic dunders and an int has nothing to dispatch to. Both facts follow from the same intent — laziness here applies to indexing, not to building a deferred compute graph — and a LazyArray is not a drop-in for arithmetic on a large array either way. Use .lazy[...] to narrow the view first, or pass the wrapper to dask.array.from_array so that dask owns the compute graph.

Ownership

result() always allocates fresh system memory before reading through the selected reader. A numpy.ma source keeps its mask by receiving a masked output buffer; other source-specific array types do not survive materializing.

SelectFn module-attribute

SelectFn = Callable[[Any, SelectionMode], 'LazyArray']

__all__ module-attribute

__all__ = ['LazyArray', 'Partition']

ArrayLike

Bases: Protocol

The surface LazyArray needs from the array it wraps.

Source code in src/zarr_indexing/lazy_array.py
class ArrayLike(Protocol):
    """The surface `LazyArray` needs from the array it wraps."""

    @property
    def shape(self) -> tuple[int, ...]: ...
    @property
    def dtype(self) -> Any: ...
    def __getitem__(self, key: Any) -> Any: ...

dtype property

dtype: Any

shape property

shape: tuple[int, ...]

__getitem__

__getitem__(key: Any) -> Any
Source code in src/zarr_indexing/lazy_array.py
def __getitem__(self, key: Any) -> Any: ...

LazyArray

A lazily-indexable view over a system-memory/basic-indexing source.

Wrapping neither copies nor reads the wrapped array at construction time. Indexing through .lazy composes an IndexTransform and returns another LazyArray; result() materializes.

Selections use the positional NumPy dialect and reads are broken up along a partitioning discovered from the wrapped array. Every derived view retains its reader; that reader receives the complete projected transform once per part. See the module docstring, which also covers how the dialect differs from low-level literal transforms and which NumPy operations materialize the view.

This wrapper describes reads. It defines no __setitem__, so assigning into a view raises TypeError. Writing belongs to the consumer: plan the selection with plan_chunks and own the read-modify-write, since chunk atomicity and concurrent-writer policy are the backend's to decide, not an indexing plan's.

Parameters:

  • array (_WrappedArray) –

    The array to wrap. It must expose shape, dtype, and __getitem__ with basic (integer/slice) indexing; __setitem__ is not required, so a read-only source wraps as well as a writable one. Its partitioning, if it advertises one, is discovered here; use with_parts to choose a different one. This conservative constructor selects basic_reader; use from_numpy for a NumPy array or with_reader to select another backend adapter.

Examples:

>>> import numpy as np
>>> source = np.arange(12).reshape(3, 4)
>>> view = LazyArray.from_numpy(source).with_parts((2, 2)).lazy[1:, ::2]
>>> view.shape
(2, 2)
>>> view.result()
array([[ 4,  6],
       [ 8, 10]])
Source code in src/zarr_indexing/lazy_array.py
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
class LazyArray:
    """A lazily-indexable view over a system-memory/basic-indexing source.

    Wrapping neither copies nor reads the wrapped array at construction time.
    Indexing through `.lazy` composes an `IndexTransform` and returns another
    `LazyArray`; `result()` materializes.

    Selections use the **positional NumPy dialect** and reads are broken up
    along a **partitioning** discovered from the wrapped array. Every derived
    view retains its reader; that reader receives the complete projected
    transform once per part. See the module docstring, which also covers how the
    dialect differs from low-level literal transforms and which NumPy operations
    materialize the view.

    This wrapper describes **reads**. It defines no `__setitem__`, so
    assigning into a view raises `TypeError`. Writing belongs to the
    consumer: plan the selection with
    [`plan_chunks`][zarr_indexing.chunk_resolution.plan_chunks] and own the
    read-modify-write, since chunk atomicity and concurrent-writer policy are
    the backend's to decide, not an indexing plan's.

    Parameters
    ----------
    array
        The array to wrap. It must expose `shape`, `dtype`, and `__getitem__`
        with basic (integer/slice) indexing; `__setitem__` is not required, so
        a read-only source wraps as well as a writable one. Its partitioning,
        if it advertises one, is discovered here; use `with_parts` to choose
        a different one.
        This conservative constructor selects `basic_reader`; use `from_numpy`
        for a NumPy array or `with_reader` to select another backend adapter.

    Examples
    --------
    >>> import numpy as np
    >>> source = np.arange(12).reshape(3, 4)
    >>> view = LazyArray.from_numpy(source).with_parts((2, 2)).lazy[1:, ::2]
    >>> view.shape
    (2, 2)
    >>> view.result()
    array([[ 4,  6],
           [ 8, 10]])
    """

    __slots__ = ("_array", "_part_owner", "_parts", "_reader", "_transform", "_window")

    def __init__(self, array: _WrappedArray) -> None:
        """Wrap `array` without reading it; parameters are documented on the class.

        Reject `numpy.matrix`, normalize the shape, and inspect partition metadata.
        Shape conversion and transform construction can also reject invalid input.
        """
        if isinstance(array, np.matrix):
            # `np.matrix` keeps every result two-dimensional, so `m[1]` has shape
            # `(1, n)` where every other array-like gives `(n,)`. A view's shape
            # comes from the transform, which follows NumPy's rule, so the two
            # disagree on every rank-reducing selection. Refused at the door
            # rather than resolved into a shape the view did not promise.
            raise TypeError(
                "numpy.matrix cannot be wrapped: it never reduces rank, so a "
                "view's shape and its result would disagree. Convert it first, "
                "with numpy.asarray(m)."
            )
        shape = tuple(int(s) for s in array.shape)
        self._array = array
        self._window: tuple[slice, ...] | None = None
        self._transform = IndexTransform.from_shape(shape)
        self._parts = _discover_parts(array, shape)
        self._reader = basic_reader
        self._part_owner = _PartOwner()

    @classmethod
    def from_numpy(cls, array: np.ndarray[Any, Any]) -> LazyArray:
        """Wrap a NumPy array with its explicitly selected NumPy reader."""
        if not isinstance(cast(object, array), np.ndarray):
            raise TypeError(
                f"LazyArray.from_numpy requires a numpy.ndarray, got {type(array).__name__}"
            )
        return cls(array).with_reader(numpy_reader)

    @classmethod
    def _derive(
        cls,
        array: _WrappedArray,
        transform: IndexTransform,
        parts: tuple[DimensionGrid, ...] | None,
        window: tuple[slice, ...] | None,
        reader: Reader,
    ) -> LazyArray:
        """Build a wrapper sharing `array` but carrying a new transform or partitioning."""
        view = cls.__new__(cls)
        view._array = array
        # Views re-zero their coordinate system: the positional dialect means a
        # view's first element is at position 0 whatever it was sliced from.
        view._transform = transform.translate_domain_to((0,) * transform.input_rank)
        view._parts = parts
        view._window = window
        view._reader = reader
        view._part_owner = _PartOwner()
        return view

    @property
    def _base_shape(self) -> tuple[int, ...]:
        """The shape of what this wrapper treats as its base array."""
        if self._window is None:
            return tuple(int(s) for s in self._array.shape)
        return tuple(s.stop - s.start for s in self._window)

    # -- array-like surface -------------------------------------------------

    @property
    def array(self) -> _WrappedArray:
        """The wrapped array."""
        return self._array

    @property
    def base_shape(self) -> tuple[int, ...]:
        """The shape the partitioning is expressed in — not this view's shape.

        `with_parts` and `with_parts_per_axis` describe boxes of the array being
        read, not of the view reading it, so a narrowed view still partitions
        the extents named here. For a part's own `array`, this is the part's
        box, which is why the same call means different sizes there. Without
        somewhere to read it, the frame in force could only be inferred from an
        error message.
        """
        return self._base_shape

    @property
    def transform(self) -> IndexTransform:
        """The composed transform from this view's coordinates to storage."""
        return self._transform

    @property
    def shape(self) -> tuple[int, ...]:
        """The shape of this view — the transform's input domain, not the source's."""
        return self._transform.domain.shape

    @property
    def ndim(self) -> int:
        """Number of dimensions of this view — the transform's input rank."""
        return self._transform.input_rank

    @property
    def size(self) -> int:
        """Total number of elements in this view (the product of `shape`)."""
        return math.prod(self.shape)

    @property
    def dtype(self) -> Any:
        """The wrapped array's dtype; views never change it."""
        return self._array.dtype

    @property
    def reader(self) -> Reader:
        """The backend adapter used when this view materializes."""
        return self._reader

    def with_reader(self, reader: Reader) -> LazyArray:
        """Return the same metadata view resolved through `reader`."""
        if not callable(getattr(reader, "read_into", None)):
            raise TypeError(f"reader.read_into must be callable, got {type(reader).__name__}")
        return LazyArray._derive(
            self._array,
            self._transform,
            self._parts,
            self._window,
            reader,
        )

    # -- shape of the selection ---------------------------------------------

    @property
    def is_box(self) -> bool:
        """Whether this view selects a rectangular region rather than a point list.

        True exactly when the composed transform has no `ArrayMap` output maps.
        Basic indexing of a box stays a box. Advanced indexing can introduce
        index arrays, but singleton gathers and later selections may remove them.

        `bounding_box()` and `strides()` describe the touched coordinate region
        and stride magnitudes; use the transform for order, result axes, and
        repetitions. Neither this flag nor the hull alone proves dense coverage.

        Examples
        --------
        >>> import numpy as np
        >>> array = LazyArray.from_numpy(np.arange(12).reshape(3, 4))
        >>> (array.lazy[1:, ::2].is_box, array.lazy.oindex[[2, 0], :].is_box)
        (True, False)
        """
        return not any(isinstance(m, ArrayMap) for m in self._transform.output)

    def bounding_box(self) -> tuple[tuple[int, int], ...] | None:
        """The storage region this view touches, one interval per storage dimension.

        Defined for any selection, box or not, as the hull: the smallest
        `[inclusive_min, exclusive_max)` interval per dimension of the array
        this view reads from that contains every coordinate the selection
        reaches.

        A strided box can leave gaps, and a query hull can be arbitrarily loose:
        `oindex[[0, 999]]` spans a 1000-wide hull over two selected rows. Conversely,
        a query may cover every coordinate in its hull. Inspect the transform
        when coverage or traversal order matters.

        Returns
        -------
        tuple of (int, int), or None
            One interval per storage dimension, or `None` when the view is
            empty (`size == 0`) and so touches no coordinate at all, leaving no
            interval to report.

        Notes
        -----
        The coordinates directly address the raw `array` this view exposes.
        Consequently, partition views report source-global hulls;
        [`Partition.box`][zarr_indexing.lazy_array.Partition] separately gives
        the whole global partition cell rather than only the selected hull.

        Examples
        --------
        >>> import numpy as np
        >>> array = LazyArray.from_numpy(np.arange(12).reshape(3, 4))
        >>> array.lazy[1:, ::2].bounding_box()
        ((1, 3), (0, 3))
        >>> array.lazy.oindex[[2, 0], :].bounding_box()
        ((0, 3), (0, 4))
        >>> array.lazy[1:1].bounding_box() is None
        True
        """
        if self.size == 0:
            return None
        domain = self._transform.domain
        bounds: list[tuple[int, int]] = []
        for m in self._transform.output:
            if isinstance(m, ConstantMap):
                bounds.append((m.offset, m.offset + 1))
            elif isinstance(m, DimensionMap):
                d = m.input_dimension
                first = m.offset + m.stride * domain.inclusive_min[d]
                last = m.offset + m.stride * (domain.exclusive_max[d] - 1)
                bounds.append((min(first, last), max(first, last) + 1))
            else:
                coords = m.offset + m.stride * m.index_array
                bounds.append((int(coords.min()), int(coords.max()) + 1))
        return tuple(bounds)

    def strides(self) -> tuple[int, ...] | None:
        """The step between selected coordinates, one per storage dimension.

        Together with `bounding_box()`, this describes a box's per-axis coordinates:
        `bounding_box()` gives the interval per dimension, `strides()` gives the
        step per dimension. A stride of 1 means every cell of the hull along
        that dimension is selected; `k` means every `k`-th. Dimensions fixed by
        an integer index report 1 — they span a single coordinate.

        Returns
        -------
        tuple of int, or None
            One positive stride per storage dimension, or `None` when
            [`is_box`][zarr_indexing.lazy_array.LazyArray.is_box] is false: a
            query's coordinates are a lookup table and have no step. An empty
            box still reports its strides even though
            [`bounding_box`][zarr_indexing.lazy_array.LazyArray.bounding_box]
            returns `None`, because the step is a property of the selection's
            shape, not of the (empty) region it touches.

        Notes
        -----
        Magnitudes only. A reversing view (`lazy[::-1]`) selects the same set of
        coordinates as the equivalent forward view, so it reports the same
        bounding box and the same strides. The traversal direction is recorded
        in the transform, not in this description of the region touched. A
        consumer that needs the order reads the transform, or reverses the block
        it gets back.

        Examples
        --------
        >>> import numpy as np
        >>> array = LazyArray.from_numpy(np.arange(24).reshape(4, 6))
        >>> (array.lazy[1:, ::2].bounding_box(), array.lazy[1:, ::2].strides())
        (((1, 4), (0, 5)), (1, 2))
        >>> array.lazy[2, ::3].strides()
        (1, 3)
        >>> array.lazy.oindex[[2, 0], :].strides() is None
        True
        """
        if not self.is_box:
            return None
        return tuple(
            1 if isinstance(m, ConstantMap) else abs(m.stride) for m in self._transform.output
        )

    # -- partitioning -------------------------------------------------------

    def with_parts(self, parts: Sequence[int]) -> LazyArray:
        """Return the same view, read in uniform boxes of shape `parts`.

        One integer per dimension of `base_shape`, with the trailing box in each
        dimension clipped to the extent. The transform, the wrapped array, and
        therefore `result()` are all unchanged; only the boxes the read is
        broken into differ. Nothing is copied and nothing is read.

        For per-axis sizes see
        [`with_parts_per_axis`][zarr_indexing.lazy_array.LazyArray.with_parts_per_axis],
        and to read in one pass see
        [`unpartitioned`][zarr_indexing.lazy_array.LazyArray.unpartitioned].

        Parameters
        ----------
        parts
            The box shape, one integer per dimension of `base_shape`.

        Returns
        -------
        LazyArray
            The same view with a new partitioning.

        Raises
        ------
        ValueError
            If `parts` has the wrong length or contains a non-positive extent.
            Uniform part sizes must remain positive even for a zero-length
            axis; use `with_parts_per_axis` for the accepted explicit zero-axis
            spellings.

        Examples
        --------
        >>> import numpy as np
        >>> view = LazyArray.from_numpy(np.arange(12).reshape(3, 4))
        >>> [part.base_coords for part in view.with_parts((2, 3)).parts()]
        [(0, 0), (0, 1), (1, 0), (1, 1)]
        """
        entries = self._part_entries(parts, "with_parts")
        if any(isinstance(entry, Sequence) for entry in entries):
            raise ValueError(
                "with_parts takes one integer per dimension; for per-axis box "
                "sizes use with_parts_per_axis"
            )
        return self._with_grids(dimension_grids_from_chunks(entries, self._base_shape))

    def with_parts_per_axis(self, sizes: Sequence[Sequence[int]]) -> LazyArray:
        """Return the same view, read in boxes of explicitly listed sizes.

        The dask convention: one sequence of box extents per dimension of
        `base_shape`, each summing to that dimension's extent. Use it when the
        boxes are not uniform — a partitioning discovered from a store, or one
        whose last box differs by more than clipping.

        Parameters
        ----------
        sizes
            One sequence of box extents per dimension of `base_shape`.

        Returns
        -------
        LazyArray
            The same view with a new partitioning.

        Raises
        ------
        ValueError
            If `sizes` has the wrong length, contains a negative extent, uses a
            zero extent on a nonempty axis, or declares sizes that do not sum
            to `base_shape`. On a zero-length axis, `()`, `(0,)`, and repeated
            zeros all describe no chunks.

        Examples
        --------
        >>> import numpy as np
        >>> view = LazyArray.from_numpy(np.arange(12).reshape(3, 4))
        >>> [part.box for part in view.with_parts_per_axis(((1, 2), (4,))).parts()]
        [((0, 1), (0, 4)), ((1, 3), (0, 4))]
        """
        entries = self._part_entries(sizes, "with_parts_per_axis")
        return self._with_grids(dimension_grids_from_chunks(entries, self._base_shape))

    @staticmethod
    def _part_entries(parts: Sequence[Any], method: str) -> tuple[Any, ...]:
        """Materialize a partitioning argument, naming a non-iterable a ValueError.

        Both partitioning methods document ValueError for malformed input; a
        bare integer would otherwise surface as a TypeError from iteration.
        """
        try:
            return tuple(parts)
        except TypeError as error:
            raise ValueError(
                f"{method} takes one entry per dimension of base_shape; got {parts!r}"
            ) from error

    def unpartitioned(self) -> LazyArray:
        """Return the same view, read in one pass.

        `result()` still allocates its owned output buffer first, then calls the
        reader once with the whole projected transform. `parts()` still yields a
        single part covering everything.

        Returns
        -------
        LazyArray
            The same view with no partitioning.
        """
        return self._with_grids(None)

    def _with_grids(self, grids: tuple[DimensionGrid, ...] | None) -> LazyArray:
        return LazyArray._derive(self._array, self._transform, grids, self._window, self._reader)

    def parts(self) -> Iterator[Partition]:
        """Iterate the base partitioning, projected through this view.

        Single-use: this is a generator, so it is consumed by the first walk and
        a second `for` over the same object yields nothing. Call `parts()` again
        for a fresh walk, or keep a `list` of it if you need to revisit.

        Yields one [`Partition`][zarr_indexing.lazy_array.Partition] per box the
        view actually touches. The parts tile the view exactly and disjointly,
        and each can be resolved with `Partition.result()`: in another thread,
        in another order, or not at all. Those views share this
        view's reader, and `LazyArray` does not serialize calls, so a stateful
        reader must synchronize its own mutable state.

        A wrapper with no partitioning (see `with_parts`) yields a single part
        covering the whole array.

        Yields
        ------
        Partition
            One per touched box, in the resolver's own order.

        Examples
        --------
        >>> import numpy as np
        >>> view = LazyArray.from_numpy(np.arange(12).reshape(3, 4)).with_parts((2, 2))
        >>> part = next(view.lazy[:, 1:].parts())
        >>> (part.base_coords, part.view.shape, part.is_complete)
        ((0, 0), (2, 1), False)
        """
        base_shape = self._base_shape
        grids = self._parts if self._parts is not None else _whole_array_grids(base_shape)
        rank = len(base_shape)

        if self._window is None:
            plan_transform = self._transform
        else:
            plan_transform = self._transform.translate(tuple(-item.start for item in self._window))

        for projection in plan_chunks(plan_transform, grids):
            base_coords = projection.chunk_coords
            local = projection.chunk_transform
            origin = tuple(grid.chunk_offset(c) for grid, c in zip(grids, base_coords, strict=True))
            extent = tuple(grid.data_size(c) for grid, c in zip(grids, base_coords, strict=True))
            if origin == (0,) * rank and extent == base_shape:
                # The part is the whole base: lowering directly against the
                # source beats materializing a block that is the source.
                window = self._window
            elif self._window is None:
                window = tuple(slice(o, o + e) for o, e in zip(origin, extent, strict=True))
            else:
                window = tuple(
                    slice(w.start + o, w.start + o + e)
                    for w, o, e in zip(self._window, origin, extent, strict=True)
                )
            # The global box, computed from the origin directly rather than from
            # `window`: a part covering the whole base carries no window (so
            # nothing is pre-materialized) but still sits somewhere concrete.
            if self._window is None:
                global_origin = origin
            else:
                global_origin = tuple(
                    w.start + o for w, o in zip(self._window, origin, strict=True)
                )
            yield Partition(
                projection=projection,
                box=tuple((o, o + e) for o, e in zip(global_origin, extent, strict=True)),
                view=LazyArray._derive(
                    self._array,
                    local.translate(global_origin),
                    None,
                    window,
                    self._reader,
                ),
                out_selection=_partition_out_selection(projection.cell_transform),
                _owner=self._part_owner,
            )

    # -- indexing -----------------------------------------------------------

    @property
    def lazy(self) -> _LazyIndexer:
        """Lazy indexing: `lazy[...]`, `lazy.oindex[...]`, `lazy.vindex[...]`.

        Each returns a new `LazyArray` view; no data is read.
        """
        return _LazyIndexer(self._select)

    def _select(self, selection: Any, mode: SelectionMode) -> LazyArray:
        transform = self._transform
        if mode != "basic":
            # This frontend applies scalar integers first, dropping their axes,
            # then normalizes its supported advanced-indexing forms.
            scalar_selection, selection = split_scalar_axes(selection, transform.domain, mode)
            if scalar_selection is not None:
                transform = transform.select(scalar_selection, "basic")
                transform = transform.translate_domain_to((0,) * transform.input_rank)
        literal = normalize_positional_selection(selection, transform.domain, mode)
        if mode == "basic":
            # IndexTransform's basic path includes NumPy's `None`/newaxis.
            # `selection_to_transform` intentionally exposes a narrower basic
            # selection contract and rejects it.
            composed = transform[literal]
        else:
            composed = transform.select(literal, mode)
        return LazyArray._derive(self._array, composed, self._parts, self._window, self._reader)

    def __getitem__(self, selection: Any) -> Any:
        """Read a basic selection eagerly, like `numpy.ndarray.__getitem__`.

        Reads here are eager, not lazy, so that a `LazyArray` works as a duck
        array for consumers (dask's `from_array`, `numpy.asarray`) that expect
        indexing to produce data. Use `.lazy[...]` for the lazy form.
        """
        return self._select(selection, "basic").result()

    def result(self, *, parts: Sequence[Partition] | None = None) -> Any:
        """Materialize this view.

        Every result starts as a fresh system-memory buffer. Each touched
        partition is read through the selected reader directly into its
        rectangular destination, or into an owned dense temporary before fancy
        placement. Empty views allocate without reading the source.

        Parameters
        ----------
        parts
            A reusable sequence previously returned by this exact view's
            `parts()` method. Supplying it reuses that partition plan instead
            of constructing another one. The parts must tile the view exactly.

        Returns
        -------
        numpy.ndarray
            An array of shape `self.shape`, identical whatever partitioning is
            in force, always in fresh system memory. A view with a zero-rank
            domain returns a zero-dimensional array, not a scalar.

        Raises
        ------
        ValueError
            If supplied parts were prepared by another view, or do not tile
            this view exactly. The output buffer is uninitialized where nothing
            was written, so a bad plan is reported rather than returned.
        AssertionError
            If this library's own partition walk fails to cover the view — a
            bug in zarr-indexing, never a consequence of the caller's input.
        """
        prepared_parts = None if parts is None else tuple(parts)
        if prepared_parts is not None and any(
            # Module-private provenance deliberately crosses the two public
            # wrapper types without becoming part of either public surface.
            part._owner is not self._part_owner  # pyright: ignore[reportPrivateUsage]
            for part in prepared_parts
        ):
            raise ValueError("prepared parts do not belong to this view")

        out_shape = self.shape
        if prepared_parts is not None:
            _validate_prepared_parts(prepared_parts, out_shape)
        out = self._output_buffer(out_shape)
        size = math.prod(out_shape)
        if size == 0:
            return out

        if prepared_parts is None and self._parts is None:
            _invoke_reader(self._reader, self._array, ReadContext(self._transform), out)
            return out

        written = 0
        selected_parts = self.parts() if prepared_parts is None else prepared_parts
        for part in selected_parts:
            # Counted before the scatter, so a part addressing the wrong
            # number of axes is named rather than reported as a broadcast
            # failure against the buffer.
            written += _out_selection_cell_count(part.out_selection, out_shape)
            direct = all(isinstance(selector, slice) for selector in part.out_selection)
            if direct:
                destination = out if len(part.out_selection) == 0 else out[part.out_selection]
            else:
                destination = part.view._output_buffer(part.view.shape)
            _invoke_reader(
                self._reader,
                self._array,
                ReadContext(part.view.transform, part.projection),
                destination,
            )
            if not direct:
                out[part.out_selection] = destination
        if written != size:
            # The buffer is uninitialized where no part wrote, so a partition
            # walk that does not tile the view exactly would otherwise hand
            # back process memory dressed as data. The parts are disjoint by
            # contract, so counting the cells each addresses is enough:
            # a gap alone undercounts and an overlap alone overcounts. This
            # count cannot detect a compensating gap and overlap; absence of overlap
            # still relies on the planner contract.
            if prepared_parts is not None:
                raise ValueError(
                    "prepared parts do not tile the view exactly: "
                    f"they addressed {written} of the view's {size} cells"
                )
            raise AssertionError(
                f"the partition walk addressed {written} of the view's {size} "
                "cells; this is a bug in zarr-indexing's partition walk"
            )
        return out

    def _output_buffer(self, out_shape: tuple[int, ...]) -> Any:
        """The buffer `result()` scatters parts into.

        Deliberately uninitialized: every cell is written by exactly one part,
        as required by the planner contract. `result()` checks the total count;
        caller-supplied parts additionally undergo coverage validation. A masked source gets a
        masked buffer so that reader writes preserve the mask; other source-
        specific array types do not survive materializing.
        """
        dtype = np.dtype(self.dtype)
        if isinstance(self._array, np.ma.MaskedArray):
            return np.ma.masked_all(out_shape, dtype=dtype)
        return np.empty(out_shape, dtype=dtype)

    # -- protocols ----------------------------------------------------------

    def __array__(self, dtype: Any = None, copy: bool | None = None) -> Any:
        """Materialize the view as a NumPy array.

        The result never shares memory with the wrapped array, whatever `copy`
        asks for: `result()` already allocates, so `copy=True` gets an array the
        caller owns. `copy=None` need not copy again when the dtype already
        matches; requesting another dtype can allocate a conversion buffer. `copy=False` is refused, because materializing means reading
        — this API always materializes into its own result buffer.
        """
        if copy is False:
            raise ValueError(
                "a LazyArray cannot be converted to a NumPy array without a "
                "copy: a view is a description of a read, and the values only "
                "exist once the read is made"
            )
        return np.asarray(self.result(), dtype=dtype)

    def __dask_tokenize__(self) -> Any:
        """Tokenize the source and serialized transform.

        The transform contributes a digest of JSON produced by `to_json()`, so
        its JSON is not embedded in the returned token, but is allocated while
        computing the digest. Equal serialized transforms and equal source
        tokens produce equal tokens; arbitrary semantically equivalent mappings
        are not guaranteed to serialize identically.

        Source tokenization can read or hash data and need not be deterministic
        on every fallback path; see `_wrapped_token`. The reader and partitioning
        are omitted under the contract that they preserve values. Cache users
        must also account for source mutation and the source's token semantics.
        """
        canonical = json.dumps(self._transform.to_json(), sort_keys=True)
        return (
            type(self).__qualname__,
            _wrapped_token(self._array),
            hashlib.sha256(canonical.encode()).hexdigest(),
        )

    def __len__(self) -> int:
        """The length of the first axis, as for a NumPy array; `TypeError` on a 0-d view."""
        if self.ndim == 0:
            raise TypeError("len() of unsized object")
        return self.shape[0]

    def __iter__(self) -> Iterator[Any]:
        """Iterate eagerly over the first axis, like a NumPy array.

        The rank check happens in `__iter__` itself rather than in the
        generator, so `iter(view)` on a zero-rank view raises immediately as
        NumPy's does, instead of waiting for the first `next`.
        """
        if self.ndim == 0:
            raise TypeError("iteration over a 0-d array")
        return (self[position] for position in range(self.shape[0]))

    # NumPy's own conversions decide what a size-1 (or wrong-sized) view means,
    # including which exception it raises, so these delegate rather than
    # reimplement. Each materializes the view first.
    def __bool__(self) -> bool:
        return bool(self.result())

    def __int__(self) -> int:
        return int(self.result())

    def __float__(self) -> float:
        return float(self.result())

    def __index__(self) -> int:
        return operator.index(self.result())

    def __repr__(self) -> str:
        wrapped = type(self._array).__name__
        described = [f"{wrapped} shape={self.shape} dtype={self.dtype}"]
        if not _is_identity_transform(self._transform, self._base_shape):
            described.append(f"view={self._transform.selection_repr}")
        return f"<LazyArray {' '.join(described)}>"

__slots__ class-attribute instance-attribute

__slots__ = (
    "_array",
    "_part_owner",
    "_parts",
    "_reader",
    "_transform",
    "_window",
)

array property

array: _WrappedArray

The wrapped array.

base_shape property

base_shape: tuple[int, ...]

The shape the partitioning is expressed in — not this view's shape.

with_parts and with_parts_per_axis describe boxes of the array being read, not of the view reading it, so a narrowed view still partitions the extents named here. For a part's own array, this is the part's box, which is why the same call means different sizes there. Without somewhere to read it, the frame in force could only be inferred from an error message.

dtype property

dtype: Any

The wrapped array's dtype; views never change it.

is_box property

is_box: bool

Whether this view selects a rectangular region rather than a point list.

True exactly when the composed transform has no ArrayMap output maps. Basic indexing of a box stays a box. Advanced indexing can introduce index arrays, but singleton gathers and later selections may remove them.

bounding_box() and strides() describe the touched coordinate region and stride magnitudes; use the transform for order, result axes, and repetitions. Neither this flag nor the hull alone proves dense coverage.

Examples:

>>> import numpy as np
>>> array = LazyArray.from_numpy(np.arange(12).reshape(3, 4))
>>> (array.lazy[1:, ::2].is_box, array.lazy.oindex[[2, 0], :].is_box)
(True, False)

lazy property

lazy: _LazyIndexer

Lazy indexing: lazy[...], lazy.oindex[...], lazy.vindex[...].

Each returns a new LazyArray view; no data is read.

ndim property

ndim: int

Number of dimensions of this view — the transform's input rank.

reader property

reader: Reader

The backend adapter used when this view materializes.

shape property

shape: tuple[int, ...]

The shape of this view — the transform's input domain, not the source's.

size property

size: int

Total number of elements in this view (the product of shape).

transform property

transform: IndexTransform

The composed transform from this view's coordinates to storage.

__array__

__array__(
    dtype: Any = None, copy: bool | None = None
) -> Any

Materialize the view as a NumPy array.

The result never shares memory with the wrapped array, whatever copy asks for: result() already allocates, so copy=True gets an array the caller owns. copy=None need not copy again when the dtype already matches; requesting another dtype can allocate a conversion buffer. copy=False is refused, because materializing means reading — this API always materializes into its own result buffer.

Source code in src/zarr_indexing/lazy_array.py
def __array__(self, dtype: Any = None, copy: bool | None = None) -> Any:
    """Materialize the view as a NumPy array.

    The result never shares memory with the wrapped array, whatever `copy`
    asks for: `result()` already allocates, so `copy=True` gets an array the
    caller owns. `copy=None` need not copy again when the dtype already
    matches; requesting another dtype can allocate a conversion buffer. `copy=False` is refused, because materializing means reading
    — this API always materializes into its own result buffer.
    """
    if copy is False:
        raise ValueError(
            "a LazyArray cannot be converted to a NumPy array without a "
            "copy: a view is a description of a read, and the values only "
            "exist once the read is made"
        )
    return np.asarray(self.result(), dtype=dtype)

__bool__

__bool__() -> bool
Source code in src/zarr_indexing/lazy_array.py
def __bool__(self) -> bool:
    return bool(self.result())

__dask_tokenize__

__dask_tokenize__() -> Any

Tokenize the source and serialized transform.

The transform contributes a digest of JSON produced by to_json(), so its JSON is not embedded in the returned token, but is allocated while computing the digest. Equal serialized transforms and equal source tokens produce equal tokens; arbitrary semantically equivalent mappings are not guaranteed to serialize identically.

Source tokenization can read or hash data and need not be deterministic on every fallback path; see _wrapped_token. The reader and partitioning are omitted under the contract that they preserve values. Cache users must also account for source mutation and the source's token semantics.

Source code in src/zarr_indexing/lazy_array.py
def __dask_tokenize__(self) -> Any:
    """Tokenize the source and serialized transform.

    The transform contributes a digest of JSON produced by `to_json()`, so
    its JSON is not embedded in the returned token, but is allocated while
    computing the digest. Equal serialized transforms and equal source
    tokens produce equal tokens; arbitrary semantically equivalent mappings
    are not guaranteed to serialize identically.

    Source tokenization can read or hash data and need not be deterministic
    on every fallback path; see `_wrapped_token`. The reader and partitioning
    are omitted under the contract that they preserve values. Cache users
    must also account for source mutation and the source's token semantics.
    """
    canonical = json.dumps(self._transform.to_json(), sort_keys=True)
    return (
        type(self).__qualname__,
        _wrapped_token(self._array),
        hashlib.sha256(canonical.encode()).hexdigest(),
    )

__float__

__float__() -> float
Source code in src/zarr_indexing/lazy_array.py
def __float__(self) -> float:
    return float(self.result())

__getitem__

__getitem__(selection: Any) -> Any

Read a basic selection eagerly, like numpy.ndarray.__getitem__.

Reads here are eager, not lazy, so that a LazyArray works as a duck array for consumers (dask's from_array, numpy.asarray) that expect indexing to produce data. Use .lazy[...] for the lazy form.

Source code in src/zarr_indexing/lazy_array.py
def __getitem__(self, selection: Any) -> Any:
    """Read a basic selection eagerly, like `numpy.ndarray.__getitem__`.

    Reads here are eager, not lazy, so that a `LazyArray` works as a duck
    array for consumers (dask's `from_array`, `numpy.asarray`) that expect
    indexing to produce data. Use `.lazy[...]` for the lazy form.
    """
    return self._select(selection, "basic").result()

__index__

__index__() -> int
Source code in src/zarr_indexing/lazy_array.py
def __index__(self) -> int:
    return operator.index(self.result())

__init__

__init__(array: _WrappedArray) -> None

Wrap array without reading it; parameters are documented on the class.

Reject numpy.matrix, normalize the shape, and inspect partition metadata. Shape conversion and transform construction can also reject invalid input.

Source code in src/zarr_indexing/lazy_array.py
def __init__(self, array: _WrappedArray) -> None:
    """Wrap `array` without reading it; parameters are documented on the class.

    Reject `numpy.matrix`, normalize the shape, and inspect partition metadata.
    Shape conversion and transform construction can also reject invalid input.
    """
    if isinstance(array, np.matrix):
        # `np.matrix` keeps every result two-dimensional, so `m[1]` has shape
        # `(1, n)` where every other array-like gives `(n,)`. A view's shape
        # comes from the transform, which follows NumPy's rule, so the two
        # disagree on every rank-reducing selection. Refused at the door
        # rather than resolved into a shape the view did not promise.
        raise TypeError(
            "numpy.matrix cannot be wrapped: it never reduces rank, so a "
            "view's shape and its result would disagree. Convert it first, "
            "with numpy.asarray(m)."
        )
    shape = tuple(int(s) for s in array.shape)
    self._array = array
    self._window: tuple[slice, ...] | None = None
    self._transform = IndexTransform.from_shape(shape)
    self._parts = _discover_parts(array, shape)
    self._reader = basic_reader
    self._part_owner = _PartOwner()

__int__

__int__() -> int
Source code in src/zarr_indexing/lazy_array.py
def __int__(self) -> int:
    return int(self.result())

__iter__

__iter__() -> Iterator[Any]

Iterate eagerly over the first axis, like a NumPy array.

The rank check happens in __iter__ itself rather than in the generator, so iter(view) on a zero-rank view raises immediately as NumPy's does, instead of waiting for the first next.

Source code in src/zarr_indexing/lazy_array.py
def __iter__(self) -> Iterator[Any]:
    """Iterate eagerly over the first axis, like a NumPy array.

    The rank check happens in `__iter__` itself rather than in the
    generator, so `iter(view)` on a zero-rank view raises immediately as
    NumPy's does, instead of waiting for the first `next`.
    """
    if self.ndim == 0:
        raise TypeError("iteration over a 0-d array")
    return (self[position] for position in range(self.shape[0]))

__len__

__len__() -> int

The length of the first axis, as for a NumPy array; TypeError on a 0-d view.

Source code in src/zarr_indexing/lazy_array.py
def __len__(self) -> int:
    """The length of the first axis, as for a NumPy array; `TypeError` on a 0-d view."""
    if self.ndim == 0:
        raise TypeError("len() of unsized object")
    return self.shape[0]

__repr__

__repr__() -> str
Source code in src/zarr_indexing/lazy_array.py
def __repr__(self) -> str:
    wrapped = type(self._array).__name__
    described = [f"{wrapped} shape={self.shape} dtype={self.dtype}"]
    if not _is_identity_transform(self._transform, self._base_shape):
        described.append(f"view={self._transform.selection_repr}")
    return f"<LazyArray {' '.join(described)}>"

bounding_box

bounding_box() -> tuple[tuple[int, int], ...] | None

The storage region this view touches, one interval per storage dimension.

Defined for any selection, box or not, as the hull: the smallest [inclusive_min, exclusive_max) interval per dimension of the array this view reads from that contains every coordinate the selection reaches.

A strided box can leave gaps, and a query hull can be arbitrarily loose: oindex[[0, 999]] spans a 1000-wide hull over two selected rows. Conversely, a query may cover every coordinate in its hull. Inspect the transform when coverage or traversal order matters.

Returns:

  • tuple of (int, int), or None

    One interval per storage dimension, or None when the view is empty (size == 0) and so touches no coordinate at all, leaving no interval to report.

Notes

The coordinates directly address the raw array this view exposes. Consequently, partition views report source-global hulls; Partition.box separately gives the whole global partition cell rather than only the selected hull.

Examples:

>>> import numpy as np
>>> array = LazyArray.from_numpy(np.arange(12).reshape(3, 4))
>>> array.lazy[1:, ::2].bounding_box()
((1, 3), (0, 3))
>>> array.lazy.oindex[[2, 0], :].bounding_box()
((0, 3), (0, 4))
>>> array.lazy[1:1].bounding_box() is None
True
Source code in src/zarr_indexing/lazy_array.py
def bounding_box(self) -> tuple[tuple[int, int], ...] | None:
    """The storage region this view touches, one interval per storage dimension.

    Defined for any selection, box or not, as the hull: the smallest
    `[inclusive_min, exclusive_max)` interval per dimension of the array
    this view reads from that contains every coordinate the selection
    reaches.

    A strided box can leave gaps, and a query hull can be arbitrarily loose:
    `oindex[[0, 999]]` spans a 1000-wide hull over two selected rows. Conversely,
    a query may cover every coordinate in its hull. Inspect the transform
    when coverage or traversal order matters.

    Returns
    -------
    tuple of (int, int), or None
        One interval per storage dimension, or `None` when the view is
        empty (`size == 0`) and so touches no coordinate at all, leaving no
        interval to report.

    Notes
    -----
    The coordinates directly address the raw `array` this view exposes.
    Consequently, partition views report source-global hulls;
    [`Partition.box`][zarr_indexing.lazy_array.Partition] separately gives
    the whole global partition cell rather than only the selected hull.

    Examples
    --------
    >>> import numpy as np
    >>> array = LazyArray.from_numpy(np.arange(12).reshape(3, 4))
    >>> array.lazy[1:, ::2].bounding_box()
    ((1, 3), (0, 3))
    >>> array.lazy.oindex[[2, 0], :].bounding_box()
    ((0, 3), (0, 4))
    >>> array.lazy[1:1].bounding_box() is None
    True
    """
    if self.size == 0:
        return None
    domain = self._transform.domain
    bounds: list[tuple[int, int]] = []
    for m in self._transform.output:
        if isinstance(m, ConstantMap):
            bounds.append((m.offset, m.offset + 1))
        elif isinstance(m, DimensionMap):
            d = m.input_dimension
            first = m.offset + m.stride * domain.inclusive_min[d]
            last = m.offset + m.stride * (domain.exclusive_max[d] - 1)
            bounds.append((min(first, last), max(first, last) + 1))
        else:
            coords = m.offset + m.stride * m.index_array
            bounds.append((int(coords.min()), int(coords.max()) + 1))
    return tuple(bounds)

from_numpy classmethod

from_numpy(array: ndarray[Any, Any]) -> LazyArray

Wrap a NumPy array with its explicitly selected NumPy reader.

Source code in src/zarr_indexing/lazy_array.py
@classmethod
def from_numpy(cls, array: np.ndarray[Any, Any]) -> LazyArray:
    """Wrap a NumPy array with its explicitly selected NumPy reader."""
    if not isinstance(cast(object, array), np.ndarray):
        raise TypeError(
            f"LazyArray.from_numpy requires a numpy.ndarray, got {type(array).__name__}"
        )
    return cls(array).with_reader(numpy_reader)

parts

parts() -> Iterator[Partition]

Iterate the base partitioning, projected through this view.

Single-use: this is a generator, so it is consumed by the first walk and a second for over the same object yields nothing. Call parts() again for a fresh walk, or keep a list of it if you need to revisit.

Yields one Partition per box the view actually touches. The parts tile the view exactly and disjointly, and each can be resolved with Partition.result(): in another thread, in another order, or not at all. Those views share this view's reader, and LazyArray does not serialize calls, so a stateful reader must synchronize its own mutable state.

A wrapper with no partitioning (see with_parts) yields a single part covering the whole array.

Yields:

  • Partition

    One per touched box, in the resolver's own order.

Examples:

>>> import numpy as np
>>> view = LazyArray.from_numpy(np.arange(12).reshape(3, 4)).with_parts((2, 2))
>>> part = next(view.lazy[:, 1:].parts())
>>> (part.base_coords, part.view.shape, part.is_complete)
((0, 0), (2, 1), False)
Source code in src/zarr_indexing/lazy_array.py
def parts(self) -> Iterator[Partition]:
    """Iterate the base partitioning, projected through this view.

    Single-use: this is a generator, so it is consumed by the first walk and
    a second `for` over the same object yields nothing. Call `parts()` again
    for a fresh walk, or keep a `list` of it if you need to revisit.

    Yields one [`Partition`][zarr_indexing.lazy_array.Partition] per box the
    view actually touches. The parts tile the view exactly and disjointly,
    and each can be resolved with `Partition.result()`: in another thread,
    in another order, or not at all. Those views share this
    view's reader, and `LazyArray` does not serialize calls, so a stateful
    reader must synchronize its own mutable state.

    A wrapper with no partitioning (see `with_parts`) yields a single part
    covering the whole array.

    Yields
    ------
    Partition
        One per touched box, in the resolver's own order.

    Examples
    --------
    >>> import numpy as np
    >>> view = LazyArray.from_numpy(np.arange(12).reshape(3, 4)).with_parts((2, 2))
    >>> part = next(view.lazy[:, 1:].parts())
    >>> (part.base_coords, part.view.shape, part.is_complete)
    ((0, 0), (2, 1), False)
    """
    base_shape = self._base_shape
    grids = self._parts if self._parts is not None else _whole_array_grids(base_shape)
    rank = len(base_shape)

    if self._window is None:
        plan_transform = self._transform
    else:
        plan_transform = self._transform.translate(tuple(-item.start for item in self._window))

    for projection in plan_chunks(plan_transform, grids):
        base_coords = projection.chunk_coords
        local = projection.chunk_transform
        origin = tuple(grid.chunk_offset(c) for grid, c in zip(grids, base_coords, strict=True))
        extent = tuple(grid.data_size(c) for grid, c in zip(grids, base_coords, strict=True))
        if origin == (0,) * rank and extent == base_shape:
            # The part is the whole base: lowering directly against the
            # source beats materializing a block that is the source.
            window = self._window
        elif self._window is None:
            window = tuple(slice(o, o + e) for o, e in zip(origin, extent, strict=True))
        else:
            window = tuple(
                slice(w.start + o, w.start + o + e)
                for w, o, e in zip(self._window, origin, extent, strict=True)
            )
        # The global box, computed from the origin directly rather than from
        # `window`: a part covering the whole base carries no window (so
        # nothing is pre-materialized) but still sits somewhere concrete.
        if self._window is None:
            global_origin = origin
        else:
            global_origin = tuple(
                w.start + o for w, o in zip(self._window, origin, strict=True)
            )
        yield Partition(
            projection=projection,
            box=tuple((o, o + e) for o, e in zip(global_origin, extent, strict=True)),
            view=LazyArray._derive(
                self._array,
                local.translate(global_origin),
                None,
                window,
                self._reader,
            ),
            out_selection=_partition_out_selection(projection.cell_transform),
            _owner=self._part_owner,
        )

result

result(*, parts: Sequence[Partition] | None = None) -> Any

Materialize this view.

Every result starts as a fresh system-memory buffer. Each touched partition is read through the selected reader directly into its rectangular destination, or into an owned dense temporary before fancy placement. Empty views allocate without reading the source.

Parameters:

  • parts (Sequence[Partition] | None, default: None ) –

    A reusable sequence previously returned by this exact view's parts() method. Supplying it reuses that partition plan instead of constructing another one. The parts must tile the view exactly.

Returns:

  • ndarray

    An array of shape self.shape, identical whatever partitioning is in force, always in fresh system memory. A view with a zero-rank domain returns a zero-dimensional array, not a scalar.

Raises:

  • ValueError

    If supplied parts were prepared by another view, or do not tile this view exactly. The output buffer is uninitialized where nothing was written, so a bad plan is reported rather than returned.

  • AssertionError

    If this library's own partition walk fails to cover the view — a bug in zarr-indexing, never a consequence of the caller's input.

Source code in src/zarr_indexing/lazy_array.py
def result(self, *, parts: Sequence[Partition] | None = None) -> Any:
    """Materialize this view.

    Every result starts as a fresh system-memory buffer. Each touched
    partition is read through the selected reader directly into its
    rectangular destination, or into an owned dense temporary before fancy
    placement. Empty views allocate without reading the source.

    Parameters
    ----------
    parts
        A reusable sequence previously returned by this exact view's
        `parts()` method. Supplying it reuses that partition plan instead
        of constructing another one. The parts must tile the view exactly.

    Returns
    -------
    numpy.ndarray
        An array of shape `self.shape`, identical whatever partitioning is
        in force, always in fresh system memory. A view with a zero-rank
        domain returns a zero-dimensional array, not a scalar.

    Raises
    ------
    ValueError
        If supplied parts were prepared by another view, or do not tile
        this view exactly. The output buffer is uninitialized where nothing
        was written, so a bad plan is reported rather than returned.
    AssertionError
        If this library's own partition walk fails to cover the view — a
        bug in zarr-indexing, never a consequence of the caller's input.
    """
    prepared_parts = None if parts is None else tuple(parts)
    if prepared_parts is not None and any(
        # Module-private provenance deliberately crosses the two public
        # wrapper types without becoming part of either public surface.
        part._owner is not self._part_owner  # pyright: ignore[reportPrivateUsage]
        for part in prepared_parts
    ):
        raise ValueError("prepared parts do not belong to this view")

    out_shape = self.shape
    if prepared_parts is not None:
        _validate_prepared_parts(prepared_parts, out_shape)
    out = self._output_buffer(out_shape)
    size = math.prod(out_shape)
    if size == 0:
        return out

    if prepared_parts is None and self._parts is None:
        _invoke_reader(self._reader, self._array, ReadContext(self._transform), out)
        return out

    written = 0
    selected_parts = self.parts() if prepared_parts is None else prepared_parts
    for part in selected_parts:
        # Counted before the scatter, so a part addressing the wrong
        # number of axes is named rather than reported as a broadcast
        # failure against the buffer.
        written += _out_selection_cell_count(part.out_selection, out_shape)
        direct = all(isinstance(selector, slice) for selector in part.out_selection)
        if direct:
            destination = out if len(part.out_selection) == 0 else out[part.out_selection]
        else:
            destination = part.view._output_buffer(part.view.shape)
        _invoke_reader(
            self._reader,
            self._array,
            ReadContext(part.view.transform, part.projection),
            destination,
        )
        if not direct:
            out[part.out_selection] = destination
    if written != size:
        # The buffer is uninitialized where no part wrote, so a partition
        # walk that does not tile the view exactly would otherwise hand
        # back process memory dressed as data. The parts are disjoint by
        # contract, so counting the cells each addresses is enough:
        # a gap alone undercounts and an overlap alone overcounts. This
        # count cannot detect a compensating gap and overlap; absence of overlap
        # still relies on the planner contract.
        if prepared_parts is not None:
            raise ValueError(
                "prepared parts do not tile the view exactly: "
                f"they addressed {written} of the view's {size} cells"
            )
        raise AssertionError(
            f"the partition walk addressed {written} of the view's {size} "
            "cells; this is a bug in zarr-indexing's partition walk"
        )
    return out

strides

strides() -> tuple[int, ...] | None

The step between selected coordinates, one per storage dimension.

Together with bounding_box(), this describes a box's per-axis coordinates: bounding_box() gives the interval per dimension, strides() gives the step per dimension. A stride of 1 means every cell of the hull along that dimension is selected; k means every k-th. Dimensions fixed by an integer index report 1 — they span a single coordinate.

Returns:

  • tuple of int, or None

    One positive stride per storage dimension, or None when is_box is false: a query's coordinates are a lookup table and have no step. An empty box still reports its strides even though bounding_box returns None, because the step is a property of the selection's shape, not of the (empty) region it touches.

Notes

Magnitudes only. A reversing view (lazy[::-1]) selects the same set of coordinates as the equivalent forward view, so it reports the same bounding box and the same strides. The traversal direction is recorded in the transform, not in this description of the region touched. A consumer that needs the order reads the transform, or reverses the block it gets back.

Examples:

>>> import numpy as np
>>> array = LazyArray.from_numpy(np.arange(24).reshape(4, 6))
>>> (array.lazy[1:, ::2].bounding_box(), array.lazy[1:, ::2].strides())
(((1, 4), (0, 5)), (1, 2))
>>> array.lazy[2, ::3].strides()
(1, 3)
>>> array.lazy.oindex[[2, 0], :].strides() is None
True
Source code in src/zarr_indexing/lazy_array.py
def strides(self) -> tuple[int, ...] | None:
    """The step between selected coordinates, one per storage dimension.

    Together with `bounding_box()`, this describes a box's per-axis coordinates:
    `bounding_box()` gives the interval per dimension, `strides()` gives the
    step per dimension. A stride of 1 means every cell of the hull along
    that dimension is selected; `k` means every `k`-th. Dimensions fixed by
    an integer index report 1 — they span a single coordinate.

    Returns
    -------
    tuple of int, or None
        One positive stride per storage dimension, or `None` when
        [`is_box`][zarr_indexing.lazy_array.LazyArray.is_box] is false: a
        query's coordinates are a lookup table and have no step. An empty
        box still reports its strides even though
        [`bounding_box`][zarr_indexing.lazy_array.LazyArray.bounding_box]
        returns `None`, because the step is a property of the selection's
        shape, not of the (empty) region it touches.

    Notes
    -----
    Magnitudes only. A reversing view (`lazy[::-1]`) selects the same set of
    coordinates as the equivalent forward view, so it reports the same
    bounding box and the same strides. The traversal direction is recorded
    in the transform, not in this description of the region touched. A
    consumer that needs the order reads the transform, or reverses the block
    it gets back.

    Examples
    --------
    >>> import numpy as np
    >>> array = LazyArray.from_numpy(np.arange(24).reshape(4, 6))
    >>> (array.lazy[1:, ::2].bounding_box(), array.lazy[1:, ::2].strides())
    (((1, 4), (0, 5)), (1, 2))
    >>> array.lazy[2, ::3].strides()
    (1, 3)
    >>> array.lazy.oindex[[2, 0], :].strides() is None
    True
    """
    if not self.is_box:
        return None
    return tuple(
        1 if isinstance(m, ConstantMap) else abs(m.stride) for m in self._transform.output
    )

unpartitioned

unpartitioned() -> LazyArray

Return the same view, read in one pass.

result() still allocates its owned output buffer first, then calls the reader once with the whole projected transform. parts() still yields a single part covering everything.

Returns:

  • LazyArray

    The same view with no partitioning.

Source code in src/zarr_indexing/lazy_array.py
def unpartitioned(self) -> LazyArray:
    """Return the same view, read in one pass.

    `result()` still allocates its owned output buffer first, then calls the
    reader once with the whole projected transform. `parts()` still yields a
    single part covering everything.

    Returns
    -------
    LazyArray
        The same view with no partitioning.
    """
    return self._with_grids(None)

with_parts

with_parts(parts: Sequence[int]) -> LazyArray

Return the same view, read in uniform boxes of shape parts.

One integer per dimension of base_shape, with the trailing box in each dimension clipped to the extent. The transform, the wrapped array, and therefore result() are all unchanged; only the boxes the read is broken into differ. Nothing is copied and nothing is read.

For per-axis sizes see with_parts_per_axis, and to read in one pass see unpartitioned.

Parameters:

  • parts (Sequence[int]) –

    The box shape, one integer per dimension of base_shape.

Returns:

  • LazyArray

    The same view with a new partitioning.

Raises:

  • ValueError

    If parts has the wrong length or contains a non-positive extent. Uniform part sizes must remain positive even for a zero-length axis; use with_parts_per_axis for the accepted explicit zero-axis spellings.

Examples:

>>> import numpy as np
>>> view = LazyArray.from_numpy(np.arange(12).reshape(3, 4))
>>> [part.base_coords for part in view.with_parts((2, 3)).parts()]
[(0, 0), (0, 1), (1, 0), (1, 1)]
Source code in src/zarr_indexing/lazy_array.py
def with_parts(self, parts: Sequence[int]) -> LazyArray:
    """Return the same view, read in uniform boxes of shape `parts`.

    One integer per dimension of `base_shape`, with the trailing box in each
    dimension clipped to the extent. The transform, the wrapped array, and
    therefore `result()` are all unchanged; only the boxes the read is
    broken into differ. Nothing is copied and nothing is read.

    For per-axis sizes see
    [`with_parts_per_axis`][zarr_indexing.lazy_array.LazyArray.with_parts_per_axis],
    and to read in one pass see
    [`unpartitioned`][zarr_indexing.lazy_array.LazyArray.unpartitioned].

    Parameters
    ----------
    parts
        The box shape, one integer per dimension of `base_shape`.

    Returns
    -------
    LazyArray
        The same view with a new partitioning.

    Raises
    ------
    ValueError
        If `parts` has the wrong length or contains a non-positive extent.
        Uniform part sizes must remain positive even for a zero-length
        axis; use `with_parts_per_axis` for the accepted explicit zero-axis
        spellings.

    Examples
    --------
    >>> import numpy as np
    >>> view = LazyArray.from_numpy(np.arange(12).reshape(3, 4))
    >>> [part.base_coords for part in view.with_parts((2, 3)).parts()]
    [(0, 0), (0, 1), (1, 0), (1, 1)]
    """
    entries = self._part_entries(parts, "with_parts")
    if any(isinstance(entry, Sequence) for entry in entries):
        raise ValueError(
            "with_parts takes one integer per dimension; for per-axis box "
            "sizes use with_parts_per_axis"
        )
    return self._with_grids(dimension_grids_from_chunks(entries, self._base_shape))

with_parts_per_axis

with_parts_per_axis(
    sizes: Sequence[Sequence[int]],
) -> LazyArray

Return the same view, read in boxes of explicitly listed sizes.

The dask convention: one sequence of box extents per dimension of base_shape, each summing to that dimension's extent. Use it when the boxes are not uniform — a partitioning discovered from a store, or one whose last box differs by more than clipping.

Parameters:

Returns:

  • LazyArray

    The same view with a new partitioning.

Raises:

  • ValueError

    If sizes has the wrong length, contains a negative extent, uses a zero extent on a nonempty axis, or declares sizes that do not sum to base_shape. On a zero-length axis, (), (0,), and repeated zeros all describe no chunks.

Examples:

>>> import numpy as np
>>> view = LazyArray.from_numpy(np.arange(12).reshape(3, 4))
>>> [part.box for part in view.with_parts_per_axis(((1, 2), (4,))).parts()]
[((0, 1), (0, 4)), ((1, 3), (0, 4))]
Source code in src/zarr_indexing/lazy_array.py
def with_parts_per_axis(self, sizes: Sequence[Sequence[int]]) -> LazyArray:
    """Return the same view, read in boxes of explicitly listed sizes.

    The dask convention: one sequence of box extents per dimension of
    `base_shape`, each summing to that dimension's extent. Use it when the
    boxes are not uniform — a partitioning discovered from a store, or one
    whose last box differs by more than clipping.

    Parameters
    ----------
    sizes
        One sequence of box extents per dimension of `base_shape`.

    Returns
    -------
    LazyArray
        The same view with a new partitioning.

    Raises
    ------
    ValueError
        If `sizes` has the wrong length, contains a negative extent, uses a
        zero extent on a nonempty axis, or declares sizes that do not sum
        to `base_shape`. On a zero-length axis, `()`, `(0,)`, and repeated
        zeros all describe no chunks.

    Examples
    --------
    >>> import numpy as np
    >>> view = LazyArray.from_numpy(np.arange(12).reshape(3, 4))
    >>> [part.box for part in view.with_parts_per_axis(((1, 2), (4,))).parts()]
    [((0, 1), (0, 4)), ((1, 3), (0, 4))]
    """
    entries = self._part_entries(sizes, "with_parts_per_axis")
    return self._with_grids(dimension_grids_from_chunks(entries, self._base_shape))

with_reader

with_reader(reader: Reader) -> LazyArray

Return the same metadata view resolved through reader.

Source code in src/zarr_indexing/lazy_array.py
def with_reader(self, reader: Reader) -> LazyArray:
    """Return the same metadata view resolved through `reader`."""
    if not callable(getattr(reader, "read_into", None)):
        raise TypeError(f"reader.read_into must be callable, got {type(reader).__name__}")
    return LazyArray._derive(
        self._array,
        self._transform,
        self._parts,
        self._window,
        reader,
    )

Partition dataclass

One box of a LazyArray's partitioning, as it falls through the view.

Yielded by LazyArray.parts. The parts of a view tile it exactly and disjointly: assembling every part.result() at its out_selection reproduces the whole view's result(), with the same reader context on both execution paths. Parts can be resolved concurrently when the source and reader permit it. Derived parts retain the same reader object; a shared stateful reader owns synchronization for concurrent calls.

A consumer that needs the plan before materialization can prepare it once and reuse the same part records for both scheduling and assembly. The frozen records do not snapshot the mutable source or reader:

parts = tuple(view.parts())
schedule(part.base_coords for part in parts)
values = view.result(parts=parts)

Prepared parts are owned by the exact view that created them and must tile it completely. Passing parts from another view, even an equivalent one, is rejected without reading; omitting a part is likewise rejected rather than returning a partly initialized result.

Attributes:

  • projection (ChunkProjection) –

    The source-independent description of this part. Its paired chunk_transform and cell_transform share one compact synthetic domain, mapping each selected cell to chunk-local storage and request coordinates respectively. This is the authoritative placement model; base_coords and is_complete are conveniences derived from it.

  • base_coords (tuple[int, ...]) –

    Which box of the base partitioning this is, one coordinate per dimension of the wrapped array.

  • box (tuple[tuple[int, int], ...]) –

    The box itself, in the global storage coordinates of the wrapped array: one [inclusive_min, exclusive_max) interval per dimension. It describes the whole partition cell, while view.bounding_box() is the global hull of only the selected values in that cell. For a nested or repartitioned view this box may be narrower than projection.chunk_domain.

  • view (LazyArray) –

    A LazyArray covering exactly the cells of the view that live in this box. Its transform directly addresses its raw wrapped array; only the projection's chunk_transform is chunk-local. Use Partition.result() to read with this partition's projection through the selected reader. Named view rather than array because LazyArray.array is the opposite thing — the raw wrapped source — and the two sat next to each other meaning inverses.

  • out_selection (tuple[Any, ...]) –

    Where Partition.result() belongs in an array of the whole view's shape — a NumPy index tuple with one entry per dimension of the view, usable directly as out[part.out_selection] = ....

  • is_complete (bool) –

    Whether the view covers the whole box. Useful to a writer deciding whether coverage is complete; it does not by itself establish buffer order, storage-unit alignment, or concurrent-write safety. Fancy projections report False because their coverage is deliberately unknown until duplicate-aware proof is added.

Examples:

Assembling every part's result at its out_selection reproduces the view:

>>> import numpy as np
>>> source = np.arange(12).reshape(3, 4)
>>> view = LazyArray.from_numpy(source).with_parts((2, 2))
>>> out = np.empty(view.shape, dtype=view.dtype)
>>> for part in view.parts():
...     out[part.out_selection] = part.result()
>>> bool((out == source).all())
True
Source code in src/zarr_indexing/lazy_array.py
@dataclass(frozen=True, kw_only=True)
class Partition:
    """One box of a `LazyArray`'s partitioning, as it falls through the view.

    Yielded by [`LazyArray.parts`][zarr_indexing.lazy_array.LazyArray.parts].
    The parts of a view tile it exactly and disjointly: assembling every
    `part.result()` at its `out_selection` reproduces the whole view's
    `result()`, with the same reader context on both execution paths. Parts can be
    resolved concurrently when the source and reader permit it.
    Derived parts retain the same reader object; a shared stateful reader owns
    synchronization for concurrent calls.

    A consumer that needs the plan before materialization can prepare it once
    and reuse the same part records for both scheduling and assembly. The frozen
    records do not snapshot the mutable source or reader:

    ```python
    parts = tuple(view.parts())
    schedule(part.base_coords for part in parts)
    values = view.result(parts=parts)
    ```

    Prepared parts are owned by the exact view that created them and must tile
    it completely. Passing parts from another view, even an equivalent one, is
    rejected without reading; omitting a part is likewise rejected rather than
    returning a partly initialized result.

    Attributes
    ----------
    projection
        The source-independent description of this part. Its paired
        `chunk_transform` and `cell_transform` share one compact synthetic
        domain, mapping each selected cell to chunk-local storage and request
        coordinates respectively. This is the authoritative placement model;
        `base_coords` and `is_complete` are conveniences derived from it.
    base_coords
        Which box of the base partitioning this is, one coordinate per dimension
        of the wrapped array.
    box
        The box itself, in the global storage coordinates of the wrapped
        array: one `[inclusive_min, exclusive_max)` interval per dimension. It
        describes the whole partition cell, while `view.bounding_box()` is the
        global hull of only the selected values in that cell. For a nested or
        repartitioned view this box may be narrower than
        `projection.chunk_domain`.
    view
        A `LazyArray` covering exactly the cells of the view that live in this
        box. Its transform directly addresses its raw wrapped `array`; only the
        projection's `chunk_transform` is chunk-local. Use `Partition.result()` to read
        with this partition's projection through the selected reader. Named `view` rather than
        `array` because `LazyArray.array` is the opposite thing — the raw
        wrapped source — and the two sat next to each other meaning inverses.
    out_selection
        Where `Partition.result()` belongs in an array of the whole view's shape — a
        NumPy index tuple with one entry per dimension of the view, usable
        directly as `out[part.out_selection] = ...`.
    is_complete
        Whether the view covers the whole box. Useful to a writer deciding
        whether coverage is complete; it does not by itself establish buffer order,
        storage-unit alignment, or concurrent-write safety. Fancy projections
        report `False` because their coverage is deliberately `unknown` until
        duplicate-aware proof is added.

    Examples
    --------
    Assembling every part's result at its `out_selection` reproduces the view:

    >>> import numpy as np
    >>> source = np.arange(12).reshape(3, 4)
    >>> view = LazyArray.from_numpy(source).with_parts((2, 2))
    >>> out = np.empty(view.shape, dtype=view.dtype)
    >>> for part in view.parts():
    ...     out[part.out_selection] = part.result()
    >>> bool((out == source).all())
    True
    """

    projection: ChunkProjection
    box: tuple[tuple[int, int], ...]
    view: LazyArray
    out_selection: tuple[Any, ...]
    _owner: _PartOwner | None = field(default=None, repr=False, compare=False)

    @property
    def base_coords(self) -> tuple[int, ...]:
        """Coordinates of this partition in the selected base grid."""
        return self.projection.chunk_coords

    @property
    def is_complete(self) -> bool:
        """Whether the projection proves it covers the entire selected cell."""
        return self.projection.coverage == "full"

    def result(self) -> Any:
        """Materialize this partition with its existing projection.

        Returns
        -------
        numpy.ndarray
            The selected values in fresh system memory, with this part's view
            shape and dtype. Assign the result at `out_selection` to assemble
            the parent view. The reader receives the same global transform and
            chunk projection as in parent materialization.

        Notes
        -----
        This uses the part's reader and source without replanning. Backend
        exceptions propagate unchanged. Concurrent calls require a source and
        reader that support concurrent access.
        """
        # Share the view allocator so independent reads preserve masked-array output.
        out = self.view._output_buffer(self.view.shape)  # pyright: ignore[reportPrivateUsage]
        if math.prod(self.view.shape) != 0:
            _invoke_reader(
                self.view.reader,
                self.view.array,
                ReadContext(self.view.transform, self.projection),
                out,
            )
        return out

base_coords property

base_coords: tuple[int, ...]

Coordinates of this partition in the selected base grid.

box instance-attribute

box: tuple[tuple[int, int], ...]

is_complete property

is_complete: bool

Whether the projection proves it covers the entire selected cell.

out_selection instance-attribute

out_selection: tuple[Any, ...]

projection instance-attribute

projection: ChunkProjection

view instance-attribute

view: LazyArray

__init__

__init__(
    *,
    projection: ChunkProjection,
    box: tuple[tuple[int, int], ...],
    view: LazyArray,
    out_selection: tuple[Any, ...],
    _owner: _PartOwner | None = None,
) -> None

result

result() -> Any

Materialize this partition with its existing projection.

Returns:

  • ndarray

    The selected values in fresh system memory, with this part's view shape and dtype. Assign the result at out_selection to assemble the parent view. The reader receives the same global transform and chunk projection as in parent materialization.

Notes

This uses the part's reader and source without replanning. Backend exceptions propagate unchanged. Concurrent calls require a source and reader that support concurrent access.

Source code in src/zarr_indexing/lazy_array.py
def result(self) -> Any:
    """Materialize this partition with its existing projection.

    Returns
    -------
    numpy.ndarray
        The selected values in fresh system memory, with this part's view
        shape and dtype. Assign the result at `out_selection` to assemble
        the parent view. The reader receives the same global transform and
        chunk projection as in parent materialization.

    Notes
    -----
    This uses the part's reader and source without replanning. Backend
    exceptions propagate unchanged. Concurrent calls require a source and
    reader that support concurrent access.
    """
    # Share the view allocator so independent reads preserve masked-array output.
    out = self.view._output_buffer(self.view.shape)  # pyright: ignore[reportPrivateUsage]
    if math.prod(self.view.shape) != 0:
        _invoke_reader(
            self.view.reader,
            self.view.array,
            ReadContext(self.view.transform, self.projection),
            out,
        )
    return out