|
57 | 57 | from zarr.core.indexing import ( |
58 | 58 | BasicIndexer, |
59 | 59 | ChunkProjection, |
| 60 | + CoordinateIndexer, |
60 | 61 | SelectorTuple, |
61 | 62 | SliceDimIndexer, |
62 | 63 | _lexicographic_order, |
@@ -399,6 +400,25 @@ def to_dict_vectorized(self) -> dict[tuple[int, ...], Buffer | None]: |
399 | 400 | return result |
400 | 401 |
|
401 | 402 |
|
| 403 | +def _drops_only_unit_axes(shape: tuple[int, ...], full: tuple[int, ...]) -> bool: |
| 404 | + """Return whether ``shape`` is ``full`` with zero or more length-1 axes removed. |
| 405 | +
|
| 406 | + ``(2, 2)`` is ``(2, 1, 2)`` minus its unit axis, and ``(1, 2)`` is |
| 407 | + ``(1, 2, 1)`` minus its last; ``(2, 2)`` is not ``(4,)``, and |
| 408 | + ``(3, 2, 1)`` is not ``(3, 2)`` because it adds an axis. |
| 409 | + """ |
| 410 | + remaining = iter(full) |
| 411 | + for size in shape: |
| 412 | + for full_size in remaining: |
| 413 | + if full_size == size: |
| 414 | + break |
| 415 | + if full_size != 1: |
| 416 | + return False |
| 417 | + else: |
| 418 | + return False |
| 419 | + return all(full_size == 1 for full_size in remaining) |
| 420 | + |
| 421 | + |
402 | 422 | @dataclass(frozen=True) |
403 | 423 | class ShardingCodec( |
404 | 424 | ArrayBytesCodec, ArrayBytesCodecPartialDecodeMixin, ArrayBytesCodecPartialEncodeMixin |
@@ -791,23 +811,11 @@ def _encode_partial_sync( |
791 | 811 | Loads the existing shard, merges the written region into the affected |
792 | 812 | inner chunks, and rewrites the whole shard. |
793 | 813 | """ |
794 | | - shard_shape = shard_spec.shape |
795 | 814 | chunks_per_shard = self._get_chunks_per_shard(shard_spec) |
796 | 815 | chunk_spec = self._get_chunk_spec(shard_spec) |
797 | 816 | inner_transform = self._get_inner_chunk_transform(shard_spec) |
798 | 817 |
|
799 | | - shard_indexer = get_indexer( |
800 | | - selection, |
801 | | - shape=shard_shape, |
802 | | - chunk_grid=ChunkGrid.from_sizes(shard_shape, self.chunk_shape), |
803 | | - ) |
804 | | - # A coordinate indexer flattens the selection, so its projections address |
805 | | - # `value` as 1-D while the caller shaped it like `sel_shape`. Mirrors the |
806 | | - # reshape `_encode_partial_single` applies on the async path. |
807 | | - sel_shape = getattr(shard_indexer, "sel_shape", None) |
808 | | - if sel_shape is not None and value.shape == sel_shape: |
809 | | - value = value.reshape(shard_indexer.shape) |
810 | | - indexer = list(shard_indexer) |
| 818 | + indexer, value = self._get_shard_indexer_and_value(selection, shard_spec, value) |
811 | 819 |
|
812 | 820 | is_complete = self._is_complete_shard_write(indexer, chunks_per_shard) |
813 | 821 |
|
@@ -1359,23 +1367,10 @@ async def _encode_partial_single( |
1359 | 1367 | selection: SelectorTuple, |
1360 | 1368 | shard_spec: ArraySpec, |
1361 | 1369 | ) -> None: |
1362 | | - shard_shape = shard_spec.shape |
1363 | | - chunk_shape = self.chunk_shape |
1364 | 1370 | chunks_per_shard = self._get_chunks_per_shard(shard_spec) |
1365 | 1371 | chunk_spec = self._get_chunk_spec(shard_spec) |
1366 | 1372 |
|
1367 | | - shard_indexer = get_indexer( |
1368 | | - selection, |
1369 | | - shape=shard_shape, |
1370 | | - chunk_grid=ChunkGrid.from_sizes(shard_shape, chunk_shape), |
1371 | | - ) |
1372 | | - # A coordinate indexer flattens the selection, so its projections address |
1373 | | - # `shard_array` as 1-D while the caller shaped it like `sel_shape`. This |
1374 | | - # mirrors the reshape `_decode_partial_single` applies on the way out. |
1375 | | - sel_shape = getattr(shard_indexer, "sel_shape", None) |
1376 | | - if sel_shape is not None and shard_array.shape == sel_shape: |
1377 | | - shard_array = shard_array.reshape(shard_indexer.shape) |
1378 | | - indexer = list(shard_indexer) |
| 1373 | + indexer, shard_array = self._get_shard_indexer_and_value(selection, shard_spec, shard_array) |
1379 | 1374 |
|
1380 | 1375 | if self._is_complete_shard_write(indexer, chunks_per_shard): |
1381 | 1376 | shard_dict = dict.fromkeys(lexicographic_order_coords(chunks_per_shard)) |
@@ -1433,6 +1428,45 @@ async def _encode_shard_dict( |
1433 | 1428 | index_bytes, buffers, buffer_prototype, chunks_per_shard=chunks_per_shard |
1434 | 1429 | ) |
1435 | 1430 |
|
| 1431 | + def _get_shard_indexer_and_value( |
| 1432 | + self, selection: SelectorTuple, shard_spec: ArraySpec, value: NDBuffer |
| 1433 | + ) -> tuple[list[ChunkProjection], NDBuffer]: |
| 1434 | + """Index ``selection`` over the inner chunk grid, flattening ``value`` to match. |
| 1435 | +
|
| 1436 | + ``get_indexer`` classifies a tuple of integer arrays as a coordinate |
| 1437 | + selection, and a ``CoordinateIndexer`` addresses the value buffer as |
| 1438 | + 1-D. An ``OrthogonalIndexer`` with two or more array-indexed axes hands |
| 1439 | + down an ``np.ix_`` tuple, so ``sel_shape`` is N-D, while the caller |
| 1440 | + shaped ``value`` like the orthogonal result: the broadcast shape minus |
| 1441 | + the integer-indexed axes, which ``np.ix_`` keeps as length-1 axes. That |
| 1442 | + value has the element count and C order of the flattened projections, |
| 1443 | + so ravel it. |
| 1444 | +
|
| 1445 | + Only that value shape is ravelled. A mask or coordinate selection |
| 1446 | + arrives with a 1-D ``sel_shape`` and a value that must already be flat, |
| 1447 | + and an orthogonal value with an axis the selection does not have is |
| 1448 | + invalid. Both are left alone so the write fails the same way it does |
| 1449 | + on an unsharded array; the shard-level selection alone cannot tell |
| 1450 | + orthogonal from mask indexing, the value shape can. Scalars pass |
| 1451 | + through and are broadcast downstream. |
| 1452 | +
|
| 1453 | + The partial-decode paths apply the inverse reshape, to |
| 1454 | + ``indexer.sel_shape``, on the way out. |
| 1455 | + """ |
| 1456 | + shard_shape = shard_spec.shape |
| 1457 | + indexer = get_indexer( |
| 1458 | + selection, |
| 1459 | + shape=shard_shape, |
| 1460 | + chunk_grid=ChunkGrid.from_sizes(shard_shape, self.chunk_shape), |
| 1461 | + ) |
| 1462 | + if ( |
| 1463 | + isinstance(indexer, CoordinateIndexer) |
| 1464 | + and len(value.shape) > 1 |
| 1465 | + and _drops_only_unit_axes(value.shape, indexer.sel_shape) |
| 1466 | + ): |
| 1467 | + value = value.reshape(indexer.shape) |
| 1468 | + return list(indexer), value |
| 1469 | + |
1436 | 1470 | def _is_total_shard( |
1437 | 1471 | self, all_chunk_coords: set[tuple[int, ...]], chunks_per_shard: tuple[int, ...] |
1438 | 1472 | ) -> bool: |
|
0 commit comments