feat(zarr-indexing): LazyArray — generic lazy indexing over array-API arrays - #267
Draft
d-v-b wants to merge 3 commits into
Draft
feat(zarr-indexing): LazyArray — generic lazy indexing over array-API arrays#267d-v-b wants to merge 3 commits into
d-v-b wants to merge 3 commits into
Conversation
… arrays Wrap any array-API-like array (NumPy, zarr, CuPy, ...) and give it a `.lazy` accessor that composes IndexTransforms instead of reading data. `result()` resolves a composed view: chunk by chunk through `iter_chunk_transforms` when the wrapped array declares chunks, otherwise as a single pass of array operations. `__getitem__` stays eager so a LazyArray is a drop-in duck array. Selections use positional NumPy semantics, deliberately different from `zarr.Array.lazy`'s literal TensorStore dialect; `zarr_indexing.boundary` is the generic translation layer between the two. `zarr_indexing.grid` gains `EdgeDimensionGrid` and `dimension_grids_from_chunks` so a caller with no zarr chunk grid can still resolve chunk-aware. Assisted-by: ClaudeCode:claude-fable-5
Assisted-by: ClaudeCode:claude-fable-5
…t in LazyArray Three defects the review found, all reachable from the documented idioms. 1. An integer index applied to an axis a fancy selection had already indexed was unresolvable: the composed ArrayMap became all-singleton but kept an `input_dimension` naming the axis the integer had just removed, which after renumbering aliased a live DimensionMap's axis. `_apply_basic_indexing` now collapses such a map to a ConstantMap — the long-deferred degenerate-collapse rule — and `array_map_dependent_axis` (promoted from a private helper, and exported) answers `None` for "depends on no axis" rather than falling back to the stale binding. 2. Scalar integers now drop their axis in every mode, as NumPy does: `oindex[0]` has the shape of `x[0]`, and `oindex[0, 1, 2]` and `vindex[0, 1, 2]` are zero-rank. They are split off at the boundary and applied as a basic step before the advanced one, rather than widened into length-1 index arrays that kept the axis. 3. A vectorized selection whose coordinate arrays are not leading (`vindex[..., i, j]`, `vindex[..., mask]`) resolved correctly unchunked but raised a shape mismatch on every real chunking: the domain put the gathered dimensions first while the per-chunk gather left them where NumPy puts them. `_apply_vindex` now follows NumPy's placement rule, `_intersect_correlated` computes scatter offsets against the real buffer layout, and the chunked resolver realigns each block's gathered axis — including for NumPy's integer-counts-as-advanced corner, which a ConstantMap in the chunk selection triggers. Also: both resolvers agree that a zero-rank result is a 0-d array; a discovered but unusable `chunks` attribute on a foreign object falls back to whole-array reads while an explicit `chunks=` stays strict; and a non-sequence chunk entry names the two conventions instead of leaking a TypeError. Tests: the oracle matrix gains scalar-after-fancy chains in both orders, non-leading vindex including the `[..., mask]` idiom, and a seeded 400-chain randomized equivalence sweep per source flavour. Verified offline over 36,000 chains across six seeds and four flavours with zero divergences. Assisted-by: ClaudeCode:claude-fable-5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 AI text below 🤖
Adds a generic
LazyArraywrapper topackages/zarr-indexing. Nothing insrc/zarris touched.What it is
LazyArraywraps any array-API-like array — NumPy, zarr, CuPy, anything withshape,dtype, and__getitem__— forwards the array-API attributes, and adds a.lazyaccessor for TensorStore-style deferred indexing:.lazy[...],.lazy.oindex[...], and.lazy.vindex[...]compose anIndexTransformand return a newLazyArray.__getitem__(no.lazy) is eager — mirroringzarr.Array's design, and what makes the wrapper a drop-in duck array fordask.array.from_array.Design
Positional dialect, and why it differs from zarr's. The transform algebra speaks literal domain coordinates: after
v = arr.lazy[10:50],v's first element is at coordinate 10, and a negative index is genuinely negative rather than counted from the end (TensorStore's convention).zarr.Array.lazydeliberately exposes that dialect, because a zarr view's coordinates are meant to stay comparable with the parent array's.LazyArraycannot afford that: it is a duck array, so it has to behave like the thing it wraps. Every view re-zeroes its domain and selections are positional NumPy semantics — index 0 is the first element of the current view, negatives wrap, boolean scalars are rejected (mindingisinstance(True, int)), masks must match the view shape exactly, and integer arrays are bounds-checked positionally. The newzarr_indexing.boundarymodule is the generic (zarr-free) translation from positions to literal coordinates beforeselection_to_transform. It duplicates whatzarr/core/array.pydoes today for its own boundary; consolidating zarr onto it is left to a follow-up, noted in the module docstring.Chunk discovery, in order: explicit
chunks=(either convention) >array.read_chunk_sizes(zarr's dask-convention property, sharding-aware; read through a guardedgetattrsince zarr raises anAttributeError-flavouredLazyViewErroron views) >array.chunks, disambiguated by element type (tuple-of-tuples = dask sizes, tuple-of-ints = uniform chunk shape) > none, treated as a single whole-array chunk.Two resolution strategies.
iter_chunk_transforms(transform, grids); read each touched chunk once with plain basic slicing, convert the local transform viasub_transform_to_selections, and scatter into the output buffer (flat scatter for correlated maps). This mirrorszarr.core.array._get_selection_via_transform, reimplemented minimally for in-memory blocks. The buffer is NumPy for v1 because the resolver's scatter indices are host-side; the device caveat is documented.__getitem__, orthogonalArrayMaps become successive per-axistake(via__array_namespace__when available), and correlated maps flatten the correlated axes, gather once, and reshape back. Constant maps become integer selections.The two strategies must agree, which the test matrix enforces directly.
New in
zarr_indexing.grid:EdgeDimensionGrid, a concreteDimensionGridLikebuilt from an explicit tuple of per-chunk sizes (offsets by cumsum, lookups bysearchsorted, so the grid need not be regular), plusdimension_grids_from_chunks(chunks, shape), which normalizes either chunk convention — validating that dask-convention sizes sum to the shape, and expanding a uniform chunk shape with a clipped tail chunk.Tests
packages/zarr-indexing/tests/test_lazy_array.py— one parametrized oracle plus one test per error case, per the repo's testing philosophy.oindex(unsorted + duplicates + multi-axis, negative, boolean axis),vindex(coordinates, broadcast pair, negatives, full-shape mask), and composed chains (basic-then-oindex, oindex-then-basic-on-another-axis, basic-then-basic, basic-then-vindex). Crossing the same cases over chunked and chunk-free wrappers is what verifies the two resolution strategies agree.EdgeDimensionGrid: all fourDimensionGridLikemethods against hand-computed values, including a size-1 axis, a single-chunk axis, and an irregular grid; plusdimension_grids_from_chunksnormalization and its threeValueErrors.shape/ndim/size/dtype/len/repr, view shape coming from the transform rather than the source, chunk discovery agreeing across all four flavours, view-aligned.chunksfor basic-slice views, and.chunks is Nonefor fancy views.importorskip):da.from_array(LazyArray(zarr_array))computes correctly with no translation ceremony, and the wrapper's.chunkshands straight back to dask. Verified locally with dask installed; it skips in CI since dask is not in the repo's test group.np.array(..., copy=False).379 passed, 1 skipped (tensorstore) for the package suite.
just lint,just typecheck(pyright, 0 errors), andjust docs-check(strict) are all clean, as are the prek hooks.Logistics
Stacked on #249 — base branch is
feat/zarr-indexing-package. Targets the 0.2.0 milestone; it does not block the 0.1.0 publish.