zarr_indexing.transform
An IndexTransform is a function between coordinate spaces, and its field
names follow the function, not the data:
| the API says | in array terms |
|---|---|
input space (domain, input_rank) |
request coordinates — the result being built |
output space (output, one map per dimension) |
source coordinates — where values are read |
output is not data: it is the rule, per source dimension, for producing
coordinates. Values flow source → request, against the arrow. The
guide
demonstrates each output map form against its NumPy counterpart.
zarr_indexing.transform ¶
Index transforms — composable, lazy coordinate mappings.
An IndexTransform pairs an input domain (the coordinates a user sees)
with a tuple of output maps (the output coordinates those inputs map to).
One output map per output dimension. See output_map.py for the three
output map types.
Key operations:
-
Indexing (
transform[2:8],.oindex[idx],.vindex[idx]) — produces a new transform with a narrower input domain and adjusted output maps. No I/O occurs. This is how lazy slicing works. -
intersect(output_domain) — restrict to output coordinates within a region. This is chunk resolution: "which of my coordinates fall in this chunk?"
-
translate(shift) — shift all output coordinates. This makes coordinates chunk-local: "express my coordinates relative to the chunk origin."
-
transform.compose(inner)— chain two transforms into one.
The transform is the atomic unit that connects user-facing indexing to
chunk-level I/O. A wrapper holds one — LazyArray starts from the identity —
and .lazy[...] composes a new transform lazily rather than reading. Reading
resolves the transform against the chunk grid via intersect + translate.
IndexTransform
dataclass
¶
A composable mapping from input coordinates to output coordinates.
An IndexTransform has:
domain: anIndexDomaindescribing the valid input coordinates (the result's coordinate range, possibly with non-zero origin).output: a tuple of output maps (one per output dimension), each describing which output coordinates the inputs touch.
In array-indexing terms: domain describes the coordinates of the result
array an indexing operation produces, and output is the rule relating
each result coordinate to a coordinate in the source. Note the direction —
the transform's input side is the result, its output side addresses the
source; the coordinate mapping runs opposite to the data flow.
Indexing an existing transform composes a new one without I/O.
Examples:
The operation "every other element of a 100-element array, starting at
index 0" — array[::2] — is a 50-cell domain whose cell i reads
output coordinate 2 * i:
>>> domain = IndexDomain.from_shape((50,))
>>> output = (DimensionMap(input_dimension=0, offset=0, stride=2),)
>>> transform = IndexTransform(domain=domain, output=output)
>>> transform.apply((0,)), transform.apply((1,)), transform.apply((49,))
((0,), (2,), (98,))
The selection compiler derives the identical transform from the source's shape and the slice:
Source code in src/zarr_indexing/transform.py
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 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 | |
domain
instance-attribute
¶
domain: IndexDomain
The input domain: the request coordinates this transform accepts.
index_array_structure
property
¶
index_array_structure: Literal[
"none", "orthogonal", "general"
]
Classify how a transform's index arrays relate to its input axes.
Returns:
-
`"none"` when no output map is an `ArrayMap`; `"orthogonal"` when every– -
`ArrayMap` varies over exactly one input axis, each its own (an outer– -
product, one independent gather per axis); `"general"` otherwise —– -
correlated (`vindex`) maps sharing their non-singleton axes, maps produced– -
by composing fancy steps, maps sharing an input axis (a diagonal gather),– -
and empty or hand-built all-singleton maps whose shape names no axis. The– -
orthogonal resolvers narrow one axis at a time and are only sound for– -
`"orthogonal"`; everything else takes the pointwise path that collapses– -
the joint block. Everything is read off the index arrays' shapes.–
Examples:
oindex arrays each vary over their own axis (an outer product):
vindex arrays are correlated — they share the broadcast axis:
oindex
property
¶
Accessor for the orthogonal (outer-product) indexing dialect.
transform.oindex[sel] applies each index array independently per
dimension and returns a new transform.
output
instance-attribute
¶
output: tuple[OutputIndexMap, ...]
One output map per output dimension, each producing that dimension's coordinate.
selection_repr
property
¶
selection_repr: str
Compact domain string, e.g. '{ [2, 8), [0, 10) }'.
Follows TensorStore's IndexDomain notation: each dimension shown
as [inclusive_min, exclusive_max) with stride annotation if not 1.
Constant (integer-indexed) dimensions show as a single value.
Array-indexed dimensions show the set of selected coordinates.
vindex
property
¶
Accessor for the vectorized (coordinate/mask) indexing dialect.
transform.vindex[sel] broadcasts all index arrays together, NumPy
fancy-indexing style, and returns a new transform.
__eq__ ¶
Value equality. ArrayMap compares its index array element-wise, so
a transform holding one can be compared at all — the generated __eq__
raised ValueError: the truth value of an array ... is ambiguous.
Source code in src/zarr_indexing/transform.py
__getitem__ ¶
__getitem__(selection: Any) -> IndexTransform
Compose a basic selection (int, slice, ellipsis, newaxis) into a new transform.
No I/O occurs. Integers and slice bounds are literal domain coordinates
(TensorStore convention): negative values are not counted from the end,
and out-of-domain values raise BoundsCheckError. Integer indices drop
their input dimension; None inserts a size-1 dimension.
Source code in src/zarr_indexing/transform.py
__post_init__ ¶
Source code in src/zarr_indexing/transform.py
__repr__ ¶
__repr__() -> str
Source code in src/zarr_indexing/transform.py
apply ¶
Map one coordinate of domain to the source coordinate that fills it.
In array-indexing terms: point names a cell of the result array, and
the returned tuple — each output map evaluated at point — names the
source-array cell its value is read from: the coordinate arrow,
running result to source.
Parameters:
Returns:
Raises:
-
ValueError–If
pointdoes not have exactly one coordinate per input dimension. -
TypeError–If the coordinates do not have an integer dtype.
-
BoundsCheckError–If a coordinate lies outside the input domain.
-
OverflowError–If a mapped output coordinate cannot be represented by
np.intp.
Examples:
The [::2] transform reads result cell i from source coordinate
2 * i, so cell 3 of the result holds source[6]:
Source code in src/zarr_indexing/transform.py
apply_many ¶
Map a batch of domain coordinates to the source coordinates that fill them.
The vectorized form of apply: each row of points names a result
cell, and the corresponding output row names the source-array cell
its value is read from.
Parameters:
-
points(ArrayLike) –Integer coordinates with shape
batch_shape + (input_rank,).
Returns:
Raises:
-
ValueError–If
pointshas no trailing coordinate axis or that axis does not contain exactly one coordinate per input dimension. -
TypeError–If the coordinates do not have an integer dtype.
-
BoundsCheckError–If a coordinate lies outside the input domain.
-
OverflowError–If a mapped output coordinate cannot be represented by
np.intp.
Examples:
Three result cells of the [::2] transform, located in one call:
>>> transform = IndexTransform.from_shape((100,))[::2]
>>> transform.apply_many(np.array([[0], [1], [49]])).tolist()
[[0], [2], [98]]
Source code in src/zarr_indexing/transform.py
compose ¶
compose(inner: IndexTransform) -> IndexTransform
Chain inner onto this transform, yielding one direct transform.
This transform maps its own input coordinates to inner's input
coordinates, and inner maps those onward; the result maps this
transform's input coordinates straight to inner's output
coordinates. Composition is what keeps a view of a view a single
description rather than a stack of layers, and it is exact: index
arrays are evaluated at the new coordinates rather than accumulated.
The precondition is that this transform's output rank equals inner's
input rank; a mismatch, or coordinates leaving inner's domain, raises.
Examples:
Chained indexing — source[2:5], then [::-1] on the result —
collapses to one transform (a reversed axis keeps literal coordinates,
so the composed domain is [-4, -1)):
>>> inner = IndexTransform.from_shape((10,))[2:5]
>>> outer = IndexTransform.identity(inner.domain)[::-1]
>>> chained = outer.compose(inner)
>>> chained == inner[::-1]
True
>>> [chained.apply((i,)) for i in (-4, -3, -2)]
[(4,), (3,), (2,)]
Source code in src/zarr_indexing/transform.py
from_json
classmethod
¶
from_json(data: IndexTransformJSON) -> IndexTransform
Construct from a canonical (or canonicalizable) ndsel transform body.
The body is first run through the message layer (normalize_ndsel) so
that omitted fields — identity output, default bounds and labels —
are filled and validated, then lowered to the engine representation.
Lower-rank index_arrays are widened to the full input rank on the way
in.
Examples:
>>> body = IndexTransform.from_shape((6,))[1:5:2].to_json()
>>> transform = IndexTransform.from_json(body)
>>> transform.domain.shape
(2,)
>>> transform.to_json() == body # the round trip is exact
True
Source code in src/zarr_indexing/transform.py
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 | |
from_shape
classmethod
¶
from_shape(shape: tuple[int, ...]) -> IndexTransform
The identity transform over a zero-origin domain of the given shape.
identity
classmethod
¶
identity(domain: IndexDomain) -> IndexTransform
The identity transform over domain: every result cell reads the source at its own address.
Source code in src/zarr_indexing/transform.py
intersect ¶
intersect(
output_domain: IndexDomain,
) -> (
tuple[
IndexTransform,
dict[int, ndarray[Any, dtype[intp]]]
| ndarray[Any, dtype[intp]]
| None,
]
| None
)
Keep only the cells whose source coordinates fall inside output_domain.
Chunk resolution is the canonical caller: intersecting a request with one chunk's box keeps the cells that chunk can serve.
Returns (restricted_transform, out_indices) or None if empty.
out_indices carries the surviving output positions: None when all
positions survive (ConstantMap/DimensionMap only), a single integer array
for one ArrayMap (or correlated/vectorized ArrayMaps), or a dict keyed by
output dimension for >= 2 orthogonal ArrayMaps (an outer product).
Source code in src/zarr_indexing/transform.py
inverted ¶
inverted() -> IndexTransform
Return the restricted, exactly representable inverse transform.
Inversion is defined for square transforms containing only constants and unique unit-stride dimension maps. Any input dimension not named by a dimension map must have singleton extent, so its coordinate can be recovered as a constant.
Returns:
-
IndexTransform–A new transform mapping output coordinates back to input coordinates.
Raises:
-
ValueError–If this transform does not have a representable inverse, including when input labels cannot be transferred to unlabeled output dimensions.
Source code in src/zarr_indexing/transform.py
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 | |
select ¶
select(
selection: Any,
mode: Literal[
"basic", "orthogonal", "vectorized"
] = "basic",
) -> IndexTransform
Convert a user selection into a composed IndexTransform.
Negative indices are treated as literal coordinates (TensorStore convention). The caller (Array layer) is responsible for converting numpy-style negative indices before calling this function.
Examples:
The mode picks the dialect; the result is the composed self the
corresponding accessor builds:
>>> t = IndexTransform.from_shape((10,))
>>> t.select(slice(2, 8)) == t[2:8]
True
>>> s = t.select(([9, 0, 0],), mode="orthogonal")
>>> s.apply((0,)), s.apply((1,)), s.apply((2,))
((9,), (0,), (0,))
Source code in src/zarr_indexing/transform.py
to_json ¶
to_json() -> IndexTransformJSON
Convert to the canonical ndsel transform body (spec section 4.3).
The result is fully explicit: input_rank, fully written bounds and
labels, and an output carrying offset/stride on every affine and
array map. It is field-for-field a TensorStore IndexTransform minus
the kind discriminator, so it loads directly into
tensorstore.IndexTransform(json=...).
Examples:
>>> body = IndexTransform.from_shape((6,))[1:5:2].to_json()
>>> (body["input_inclusive_min"], body["input_exclusive_max"])
([0], [2])
>>> body["output"]
[{'offset': 1, 'stride': 2, 'input_dimension': 0}]
Source code in src/zarr_indexing/transform.py
translate ¶
translate(shift: tuple[int, ...]) -> IndexTransform
Shift the source coordinates every cell reads by shift, per dimension.
The domain is untouched: the result keeps its cells, and each one reads from a shifted source address — for example, making a chunk's global addresses chunk-local by translating by the chunk's negated origin.
Source code in src/zarr_indexing/transform.py
translate_domain_by ¶
translate_domain_by(
shift: tuple[int, ...],
) -> IndexTransform
Shift the input domain by shift, preserving which cells are addressed.
TensorStore's translate_by: the domain moves, and every output map is
re-offset so that new coordinate c addresses the cell that c - shift
addressed before. ArrayMaps are indexed positionally over the domain, so
their index arrays are unchanged.
Source code in src/zarr_indexing/transform.py
translate_domain_to ¶
translate_domain_to(
origins: tuple[int, ...],
) -> IndexTransform
Move the input domain so its per-dimension origins equal origins.
TensorStore's translate_to; translate_domain_to((0,) * rank)
re-zeros a view's coordinate system without changing which cells it
addresses.