Repository navigation
feat(indexing): make LazyArray indexing lazy by default - #4350
Merged
Merged
Conversation
Documentation build overview
26 files changed ·
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4350 +/- ##
=======================================
Coverage 94.34% 94.34%
=======================================
Files 92 92
Lines 12948 12948
=======================================
Hits 12216 12216
Misses 732 732 🚀 New features to boost your workflow:
|
Add synchronous writes through composed selections and an explicit eager adapter for array consumers. Cover deferred reads, source mutation, masks, aliases, and repeated destinations. Assisted-by: Codex:GPT-6 Rebased-onto: upstream/main after zarr-developers#4345-zarr-developers#4349 merged; conflicts resolved with ClaudeCode:claude-fable-5-1
The non-affine write fallback assigned one element at a time, which on a chunked source is one chunk read-modify-write per element: a 50-row orthogonal write into a 1000x1000 zarr array with 100x100 chunks cost 49,999 chunk writes and 27 s against 100 writes and 10 ms natively. `write_into` now scatters in bulk. NumPy sources get one fancy assignment. A readable source with a write grid, which `LazyArray.write` discovers from `write_chunk_sizes` then `chunks`, is written one cell at a time: the cell's touched hull is read once, updated in memory with last-occurrence-wins semantics, and written back with one basic slice. The same write into zarr now costs 100 chunk writes. Sources with no grid, transforms the planner cannot factor, and sources that cannot be read keep the per-element path, which never reads. Also: order the rank check before the value copy while still validating values for an empty selection; keep the payload of a masked zero-rank affine write, which `np.flip` with no axes was turning into the `masked` singleton; note that writes bypass the reader and pin that with a test; note that iterated elements are views; add API pages for the writer and eager adapter; and describe the shipped indexing surface in the unreleased `LazyArray` changelog fragment instead of its removed `.lazy` accessor. Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
d-v-b
force-pushed
the
codex/indexing-lazy-default
branch
from
September 16, 2026 07:27
a6f9949 to
70db76d
Compare
`LazyArray` re-zeroed the domain of every derived view, so a view forgot which coordinates it was cut from and placement needed a side channel. TensorStore keeps literal coordinates on views; NumPy users expect positional keys. These are separable: the key dialect says how a key is read, the domain says what the view remembers. Views now keep their literal domain: `a[10:20]` has domain `[10, 20)` and `a[10:20][2:5]` has `[12, 15)`, while `a[10:20][0]` is still the first element because positional keys are normalized against the domain's origin, which `normalize_positional_selection` already did. Two literal keys join the NumPy ones, as in TensorStore's `__getitem__`: an `IndexDomain` restricts the view to coordinates of its own domain (empty intervals outside it are refused too) and an `IndexTransform` composes onto it. Box partitions keep the request's coordinates, so a part view's domain is a sub-domain of its parent's; a part placed by index arrays keeps a fresh zero-origin domain, and `out_selection` is the placement in both cases. A reversed view shows the negative origin the algebra already produced. Two frames stay zero-origin by construction so no consumer has to subtract an origin: `ReadContext` re-bases its transform, and `parts()` re-bases each projection's `cell_transform` from the request's literal domain to positions in the view's result buffer, which is what `Partition.projection` has always documented. A table-driven test states the expected literal domain for every indexing form and nested chain, checked against the transform algebra. Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dexing Make the one rule about indexing a LazyArray prominent: NumPy keys are positions relative to the view, an `IndexDomain` key names absolute coordinates of the view's domain, an `IndexTransform` key composes onto it, no key type has two readings, and every view keeps its absolute domain whichever key produced it. A guide section with a runnable snippet carries the table and the pandas `ix` / TensorStore comparison; the module docstring, README, and API page point at it. Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
d-v-b
marked this pull request as ready for review
September 16, 2026 09:26
1 task
d-v-b
added a commit
that referenced
this pull request
Sep 29, 2026
* chore(zarr-indexing): render misc fragments and link entries to pull requests Towncrier's built-in misc type sets showcontent = false, so the package's misc fragments (#4346, #4364, #4406) would render as bare links. Restate the five fragment types as zarr-metadata does, with misc showing its content. Fragments are numbered by the merging PR, so link them under /pull/ as zarr-metadata does. Assisted-by: ClaudeCode:claude-opus-5-5 * docs(zarr-indexing): backfill the 0.1.0, 0.2.0 and 0.2.1 release notes The three releases were tagged without running towncrier, so CHANGELOG.md stayed empty and their fragments stayed in changes/, where the next build would have announced them again as new. Build each release from the fragments its tag shipped: #3906's (renumbered #4196, the PR that merged it) for 0.1.0, #4222's for 0.2.0, and a new #4252 entry for 0.2.1, whose only change was the docs build fix. Dates are the tag dates. Assisted-by: ClaudeCode:claude-opus-5-5 * chore(zarr-indexing): build 0.3.0 changelog Consume the pending fragments into CHANGELOG.md for the zarr_indexing-v0.3.0 release. Minor, not patch: #4350 makes LazyArray indexing lazy by default, so iterating a view yields views rather than values, and views keep their literal domain instead of re-zeroing after each selection; #4310 rejects hand-built diagonal transforms with ValueError. Assisted-by: ClaudeCode:claude-opus-5-5 * chore(zarr-indexing): date the 0.3.0 release 2026-09-29 Assisted-by: ClaudeCode:claude-opus-5-5
d-v-b
added a commit
to d-v-b/zarr-python
that referenced
this pull request
Sep 30, 2026
…ebase Main removed the S101 exclusion for runtime code (zarr-developers#4364), made the package's pytest configuration standalone (zarr-developers#4406), and dropped the `.lazy` accessor (zarr-developers#4350). Narrow the basic-selection branch structurally instead of asserting, raise on benchmark self-checks, drive the codec integration tests through zarr's `sync` helper rather than the pytest-asyncio marker the standalone config does not declare, and index the view directly in the execution property test. Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
This PR applies a range of improvements to the lazy indexing API, bringing it into closer alignment with tensorstore, although we still use relative indexing when indexing with plain python objects.
🤖 AI text below 🤖
LazyArraycurrently requires.lazyat each selection step; ordinary indexing materializes immediately. This PR makes the wrapper lazy by default, matching TensorStore:view[...],view.oindex[...], andview.vindex[...]return composed views, and iteration yields lazy first-axis views.result()and NumPy conversion materialize values. The.lazyaccessor is removed.Writes
Adds synchronous
view.write(values)and assignment through composed views (view[...] =,view.oindex[...] =,view.vindex[...] =). Writes update the original source, snapshot the RHS before mutation, and apply repeated destinations in C order with the last value winning, the same contract TensorStore uses. Out-of-range coordinates raise before anything is written.Writes are planned against the source's write grid, discovered from
write_chunk_sizesand thenchunks, so sharded arrays are written by shard:Storage round trips therefore scale with touched chunks rather than selected elements. On a 1000×1000 int64 zarr array with 100×100 chunks:
Writes bypass the reader, so a caching reader is not invalidated; this is documented and pinned by a test. Writes are not transactional, and backend errors can leave partial writes. No asynchronous execution engine is introduced.
Literal domains, relative keys
Views keep their literal domain instead of re-zeroing after every selection, as TensorStore views do:
a[10:20]has domain[10, 20),a[10:20][2:5]has[12, 15), and a reversed view carries the negative origin the algebra already produced. NumPy keys stay positional, normalized against the domain's origin, soa[10:20][0]is still the first element.The type of a key decides whether it is relative or absolute. NumPy keys (slices, integers,
...,None, index arrays, masks) are positions relative to the view. AnIndexDomainkey names absolute coordinates of the view's domain, restricted to a box; empty intervals outside the view are refused too. AnIndexTransformkey composes onto the view, so any selection can be spelled in absolute coordinates by indexing the identity over the view's domain. No key type has two readings, and whichever key produced a view,view.transform.domainis absolute. This is documented in a new guide section with a runnable snippet, in the module docstring, and in the README.Box partitions keep the request's coordinates, so a part view's domain is a sub-domain of its parent's; a part placed by index arrays keeps a fresh zero-origin domain, and
out_selectionis the placement in both cases. Two frames stay zero-origin by construction so no consumer subtracts an origin:ReadContextre-bases its transform, andparts()re-bases each projection'scell_transformto positions in the view's result buffer. A table-driven test states the expected literal domain for every indexing form and nested chain.Eager adapter
A separate
EagerArrayAdaptersupplies eager indexing for consumers such as Dask'sfrom_array. It delegates shape, rank, dtype, NumPy conversion, and tokenization to the view and materializes each indexed block. Documentation, examples, and existing tests use the revised interface. The unreleasedLazyArraychangelog fragment now describes the shipped surface.This changes the experimental
zarr-indexingwrapper; it does not add or change azarr.Array.lazyaccessor or claim full NumPy advanced-indexing compatibility.Validation
zarr-indexingsuite with Dask installed, including doctests: 1,692 passed.🤖 Generated with Claude Code