worldarray 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {worldarray-0.2.0 → worldarray-0.3.0}/CHANGELOG.md +23 -0
  2. {worldarray-0.2.0 → worldarray-0.3.0}/PKG-INFO +14 -1
  3. {worldarray-0.2.0 → worldarray-0.3.0}/README.md +13 -0
  4. {worldarray-0.2.0 → worldarray-0.3.0}/docs/spec.md +32 -17
  5. {worldarray-0.2.0 → worldarray-0.3.0}/pyproject.toml +1 -1
  6. {worldarray-0.2.0 → worldarray-0.3.0}/src/worldarray/__init__.py +12 -4
  7. {worldarray-0.2.0 → worldarray-0.3.0}/src/worldarray/array.py +46 -4
  8. worldarray-0.3.0/src/worldarray/grid.py +492 -0
  9. {worldarray-0.2.0 → worldarray-0.3.0}/src/worldarray/store.py +5 -0
  10. {worldarray-0.2.0 → worldarray-0.3.0}/src/worldarray/units.py +21 -0
  11. worldarray-0.3.0/tests/test_grid.py +385 -0
  12. {worldarray-0.2.0 → worldarray-0.3.0}/uv.lock +1 -1
  13. {worldarray-0.2.0 → worldarray-0.3.0}/.github/workflows/ci.yml +0 -0
  14. {worldarray-0.2.0 → worldarray-0.3.0}/.github/workflows/docs.yml +0 -0
  15. {worldarray-0.2.0 → worldarray-0.3.0}/.github/workflows/publish.yml +0 -0
  16. {worldarray-0.2.0 → worldarray-0.3.0}/.gitignore +0 -0
  17. {worldarray-0.2.0 → worldarray-0.3.0}/LICENSE +0 -0
  18. {worldarray-0.2.0 → worldarray-0.3.0}/docs/changelog.md +0 -0
  19. {worldarray-0.2.0 → worldarray-0.3.0}/docs/index.md +0 -0
  20. {worldarray-0.2.0 → worldarray-0.3.0}/mkdocs.yml +0 -0
  21. {worldarray-0.2.0 → worldarray-0.3.0}/src/worldarray/context.py +0 -0
  22. {worldarray-0.2.0 → worldarray-0.3.0}/src/worldarray/geometry.py +0 -0
  23. {worldarray-0.2.0 → worldarray-0.3.0}/src/worldarray/levels.py +0 -0
  24. {worldarray-0.2.0 → worldarray-0.3.0}/src/worldarray/placement.py +0 -0
  25. {worldarray-0.2.0 → worldarray-0.3.0}/src/worldarray/resample.py +0 -0
  26. {worldarray-0.2.0 → worldarray-0.3.0}/src/worldarray/retry.py +0 -0
  27. {worldarray-0.2.0 → worldarray-0.3.0}/src/worldarray/write.py +0 -0
  28. {worldarray-0.2.0 → worldarray-0.3.0}/tests/conftest.py +0 -0
  29. {worldarray-0.2.0 → worldarray-0.3.0}/tests/fixtures/metadata.json +0 -0
  30. {worldarray-0.2.0 → worldarray-0.3.0}/tests/fixtures/write.json +0 -0
  31. {worldarray-0.2.0 → worldarray-0.3.0}/tests/test_access.py +0 -0
  32. {worldarray-0.2.0 → worldarray-0.3.0}/tests/test_geometry.py +0 -0
  33. {worldarray-0.2.0 → worldarray-0.3.0}/tests/test_metadata.py +0 -0
  34. {worldarray-0.2.0 → worldarray-0.3.0}/tests/test_read.py +0 -0
  35. {worldarray-0.2.0 → worldarray-0.3.0}/tests/test_resample.py +0 -0
  36. {worldarray-0.2.0 → worldarray-0.3.0}/tests/test_write.py +0 -0
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 (2026-10-22)
4
+
5
+ Grids and blocks, built to spec draft 3 (Behaviours 17 to 19). Nothing on the read or
6
+ write side changes.
7
+
8
+ - `blocks(shape, block_shape, context=0, box=None, clip=True)`: the array (or a voxel
9
+ box, which may reach past the array) cut into write boxes that tile it with no overlap
10
+ and no gap, each with a read box grown by `context` (one int, one per axis, or
11
+ `(before, after)` pairs) and clipped to the array when `clip`; C order, `len`,
12
+ `blocks[id]` in O(1), `index_of(begin)`; a `Block` carries `index`, `id`, `begin`,
13
+ `end`, `read_begin`, `read_end`, `at_start` and `at_end` per axis and `inner`, the write
14
+ box's slices inside the read box. Plain picklable records; nothing is scheduled.
15
+ `vol.blocks()` defaults the block to `write_shape`, so default blocks are aligned writes.
16
+ `vol.write_chunk(index, data)` writes one whole unit.
17
+ - `Grid(shape, voxel_size, translation, axes, units, chunks, shards)`: a `WorldArray`
18
+ without a store, with `box`, `corner`, `index_box`, `chunks_covering`, `write_shape`,
19
+ `blocks` and `meta(dtype)`; `Grid.from_meta(meta, voxel_size=)` is an existing array's
20
+ grid or the resample target grid over it. `create(path, grid=g, dtype=)` creates from it.
21
+ - `level_factors(voxel_size, ratio=1.5, ...)`: the anisotropy-aware pyramid schedule
22
+ mesh-n-bone and cellmap-analyze share, per level.
23
+ - `units.abbreviation`, and `store.join`, `store.parent`, `store.basename` as public
24
+ URL-aware path helpers.
25
+
3
26
  ## 0.2.0 (2026-10-22)
4
27
 
5
28
  Metadata and placed creation, built to spec draft 3 (Behaviours 10 to 16). Nothing on the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: worldarray
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Read zarr, N5 and Neuroglancer precomputed volumes in world coordinates through tensorstore: correct voxel placement, pyramid levels, padded region reads, resampling. numpy + tensorstore only.
5
5
  Project-URL: Homepage, https://janeliascicomp.github.io/worldarray/
6
6
  Project-URL: Documentation, https://janeliascicomp.github.io/worldarray/
@@ -156,6 +156,11 @@ s1 = wa.open_pyramid("out.zarr", mode="r+").add_level("s1", factors=2) # place
156
156
  and only two answers to "it exists": raise, or adopt an identical array after checking;
157
157
  whole-unit writes from many processes with no coordination; `add_level` as metadata plus
158
158
  an empty array; chunk keys for every format. Nothing is ever deleted or overwritten.
159
+ - **Plans outputs and blocks** (0.3): a store-less `Grid` with the same geometry as an
160
+ array, so an output is planned, cut into blocks and only then created; `blocks` tiling an
161
+ array or a box into write boxes with context-grown read boxes, in C order, as plain
162
+ picklable records a cluster job takes by id; the anisotropy-aware pyramid schedule.
163
+ Nothing is scheduled or run.
159
164
 
160
165
  ## The placement rule
161
166
 
@@ -202,6 +207,14 @@ wa.ome_multiscales(axes, units, levels, version="0.5", type=None) ; wa.level_pla
202
207
  wa.level_shape(shape, factors, origin=0) ; wa.chunk_key(index, format, ...) ; wa.parse_chunk_key(key, format, ...)
203
208
  wa.ensure_group_entry(group_path, name, voxel_size, translation, axes, units="nanometer") # list an array you made yourself
204
209
  wa.units.ome_name("nm") -> "nanometer" ; wa.write.codecs_of(path) -> (codecs, shards)
210
+
211
+ # grids and blocks (0.3)
212
+ g = wa.Grid(shape, voxel_size, translation, axes=None, units="nanometer", chunks=None, shards=None) # or Grid.from_meta(meta, voxel_size=)
213
+ wa.create(path, grid=g, dtype="uint64")
214
+ for b in wa.blocks(shape, block_shape, context=0, box=None, clip=True): # also vol.blocks() and g.blocks()
215
+ b.begin, b.end, b.read_begin, b.read_end, b.inner, b.at_end, b.id # plain records; blocks[id] is O(1)
216
+ wa.level_factors(voxel_size, ratio=1.5, max_levels=None, stop_shape=None, shape=None)
217
+ wa.units.abbreviation("nanometer") ; wa.store.join(path, "s0") ; wa.store.parent(path) ; wa.store.basename(path)
205
218
  ```
206
219
 
207
220
  `WorldArray`: `ts`, `meta`, `grid`, `shape`, `dtype`, `axes`, `voxel_size`, `translation`,
@@ -97,6 +97,11 @@ s1 = wa.open_pyramid("out.zarr", mode="r+").add_level("s1", factors=2) # place
97
97
  and only two answers to "it exists": raise, or adopt an identical array after checking;
98
98
  whole-unit writes from many processes with no coordination; `add_level` as metadata plus
99
99
  an empty array; chunk keys for every format. Nothing is ever deleted or overwritten.
100
+ - **Plans outputs and blocks** (0.3): a store-less `Grid` with the same geometry as an
101
+ array, so an output is planned, cut into blocks and only then created; `blocks` tiling an
102
+ array or a box into write boxes with context-grown read boxes, in C order, as plain
103
+ picklable records a cluster job takes by id; the anisotropy-aware pyramid schedule.
104
+ Nothing is scheduled or run.
100
105
 
101
106
  ## The placement rule
102
107
 
@@ -143,6 +148,14 @@ wa.ome_multiscales(axes, units, levels, version="0.5", type=None) ; wa.level_pla
143
148
  wa.level_shape(shape, factors, origin=0) ; wa.chunk_key(index, format, ...) ; wa.parse_chunk_key(key, format, ...)
144
149
  wa.ensure_group_entry(group_path, name, voxel_size, translation, axes, units="nanometer") # list an array you made yourself
145
150
  wa.units.ome_name("nm") -> "nanometer" ; wa.write.codecs_of(path) -> (codecs, shards)
151
+
152
+ # grids and blocks (0.3)
153
+ g = wa.Grid(shape, voxel_size, translation, axes=None, units="nanometer", chunks=None, shards=None) # or Grid.from_meta(meta, voxel_size=)
154
+ wa.create(path, grid=g, dtype="uint64")
155
+ for b in wa.blocks(shape, block_shape, context=0, box=None, clip=True): # also vol.blocks() and g.blocks()
156
+ b.begin, b.end, b.read_begin, b.read_end, b.inner, b.at_end, b.id # plain records; blocks[id] is O(1)
157
+ wa.level_factors(voxel_size, ratio=1.5, max_levels=None, stop_shape=None, shape=None)
158
+ wa.units.abbreviation("nanometer") ; wa.store.join(path, "s0") ; wa.store.parent(path) ; wa.store.basename(path)
146
159
  ```
147
160
 
148
161
  `WorldArray`: `ts`, `meta`, `grid`, `shape`, `dtype`, `axes`, `voxel_size`, `translation`,
@@ -765,24 +765,38 @@ Behaviours 10 to 16 are 0.2; 17 to 19 are 0.3.
765
765
  17. **Blocks tile, context grows, nothing runs.** `blocks(shape, block_shape, context, box,
766
766
  clip)` yields, in C order, blocks whose write boxes `[begin, end)` partition `[0,
767
767
  shape)`, or `box` (a voxel box, the grid anchored at its begin) when given, with no
768
- overlap and no gap, the last block on an axis clipped. A block's read box is its write
769
- box grown by `context` on each side, where `context` is one int, one per axis, or a
770
- `(before, after)` pair per axis, clipped to the array when `clip=True` (the default;
771
- cellmap-analyze's `create_block` edge rule) and left unclipped when `clip=False` for a
772
- padded `read_index`. `len(blocks)`, `blocks[b.id]`, `blocks.index_of(b.begin)` and
768
+ overlap and no gap, the last block on an axis clipped to the box. `box` may reach past
769
+ the array on any side (a ROI rounded outward to a factor multiple, mesh-n-bone's halo
770
+ mode): the write boxes tile it exactly, outside the array too. A block's read box is
771
+ its write box grown by `context` on each side, where `context` is one int, one per
772
+ axis, or a `(before, after)` pair per axis, clipped to the **array** (never to `box`,
773
+ never a write box) when `clip=True` (the default; cellmap-analyze's `create_block` edge
774
+ rule) and left unclipped when `clip=False` for a padded `read_index` (its
775
+ `neg_with_edge_pos` mode, which reads past the array for fill). A block also carries
776
+ `at_start` and `at_end` per axis, whether it is the first or last block on that axis,
777
+ so a caller adds context at the array's far faces itself, and `inner`, the slices of
778
+ the write box inside the read box, correct also when `clip=False` lets `read_begin`
779
+ go below 0. `len(blocks)`, `blocks[b.id]` (O(1)), `blocks.index_of(b.begin)` and
773
780
  `b.index` agree. `vol.blocks()` defaults `block_shape` to `write_shape`, so default
774
781
  blocks are aligned writes. Nothing is scheduled: the iterable and its blocks are plain,
775
- picklable records. `write_chunk(index, data)` writes the unit at `index` whole;
776
- `data.shape` must equal that unit's clipped shape.
782
+ picklable records, and the iterable pickles without its blocks, so a worker takes an
783
+ id. One grid per call: a consumer that writes several levels from one block derives
784
+ the per-level boxes itself (mesh-n-bone's direct mode, two lines). `write_chunk(index,
785
+ data)` writes the unit at `index` whole; `data.shape` must equal that unit's clipped
786
+ shape.
777
787
 
778
788
  18. **A `Grid` is a `WorldArray` without a store.** `Grid(shape, voxel_size, translation,
779
789
  axes, units, chunks, shards)` answers `box`, `corner`, `index_box`, `chunks_covering`,
780
790
  `write_shape` and `blocks` exactly as a `WorldArray` of those facts does, and
781
- `g.meta(dtype)` equals `wa.create(path, grid=g, dtype=dtype).meta`, so a pipeline plans
791
+ `g.meta(dtype)` agrees with `wa.create(path, grid=g, dtype=dtype).meta` in every
792
+ field a grid has: shape, dtype, chunk and shard shape, axes, units, voxel size and
793
+ translation (a grid has no path, format, documents or fill value), so a pipeline plans
782
794
  its output before anything exists. `Grid.from_meta(meta, voxel_size=t)` is Behaviour
783
795
  4's target grid over that array: anchored at its corner with `ceil(n * s / t)` voxels,
784
- so a `read(voxel_size=t)` of the source fills it exactly; this is the output grid
785
- cellmap-analyze's commands compute today in the integer frame.
796
+ so a `read(voxel_size=t)` of the source fills it exactly, and its `box` is the target
797
+ grid's extent, which may reach past the source's own `box` by less than one target
798
+ voxel; this is the output grid cellmap-analyze's commands compute today in the integer
799
+ frame.
786
800
 
787
801
  19. **Schedules and helpers.** `level_factors(voxel_size, ratio, max_levels, stop_shape,
788
802
  shape)` returns per level the per-axis factors, 2 on every axis whose current voxel size
@@ -1017,11 +1031,12 @@ over funlib geometry, by this spec's own assignment.
1017
1031
  attributes? If so, should the package offer `funlib_attrs(voxel_size, translation, axes,
1018
1032
  units)`, writing `offset` as the corner, funlib's meaning, for `create(attrs=)`, or do
1019
1033
  the repos keep writing their own?
1020
- 6. **The block iterator's shape** (Behaviour 17) is written from cellmap-analyze's
1021
- `create_block` (directional context, the edge rule, a fourth axis) and mesh-n-bone's
1022
- super-chunk grids (factor-aligned strides per level) without either session confirming
1023
- it. Confirm with both before 0.3 is built. mesh-n-bone's direct mode, where one task
1024
- writes several levels at once, may need per-level grids that `blocks` does not give.
1034
+ 6. **The block iterator's shape** (Behaviour 17). Resolved 2026-10-22: both sessions
1035
+ confirmed it. mesh-n-bone asked that `box` may reach past the array and that `clip`
1036
+ clips read boxes only, and derives its per-level boxes from one grid itself;
1037
+ cellmap-analyze asked for `at_start`/`at_end` per axis and `inner`, and confirmed that
1038
+ `(before, after)` pairs cover its context modes and that `clip=False` with a padded
1039
+ `read_index` is its edge-reading mode. All are in Behaviour 17.
1025
1040
  7. **Sidecar attributes.** Resolved 2026-10-22: registration-station keeps `fs` and
1026
1041
  `fix_origin_um` on the group through `group_attrs` and the part attributes (`y0`, `y1`,
1027
1042
  `y0_core`, `y1_core`, `feather_vox`) on the array through `attrs`, all known at creation;
@@ -1056,8 +1071,8 @@ over funlib geometry, by this spec's own assignment.
1056
1071
  13. **World boxes on blocks.** cellmap-flow reads its input at one voxel size by a world
1057
1072
  box grown by a context in nanometres and writes at another; `blocks` (0.3) gives index
1058
1073
  boxes on one grid. `Grid.blocks` could carry world boxes too (`b.box`, `b.read_box` as
1059
- `Box`, a context in world units). Two lines on cellmap-flow's side today; a candidate
1060
- for 0.4.
1074
+ `Box`, a context in world units). Two lines on cellmap-flow's side today. Not planned
1075
+ (2026-10-22): daisy does cellmap-flow's tiling and stays; reopen if cellmap-flow drops it.
1061
1076
 
1062
1077
  ## 8. Constraints for whoever builds on this
1063
1078
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "worldarray"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Read zarr, N5 and Neuroglancer precomputed volumes in world coordinates through tensorstore: correct voxel placement, pyramid levels, padded region reads, resampling. numpy + tensorstore only."
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -2,16 +2,19 @@
2
2
 
3
3
  Open zarr v2, zarr v3, N5 and Neuroglancer precomputed arrays and pyramids by path, know
4
4
  where each sits (voxel size, the centre of voxel 0, units, axes), and read voxel boxes or
5
- world boxes as numpy arrays, padded where the box leaves the data. ``docs/spec.md`` is the
6
- contract; the README is its first five sections.
5
+ world boxes as numpy arrays, padded where the box leaves the data; create placed OME-NGFF
6
+ levels and write them from any number of jobs; plan an output grid and cut it into blocks
7
+ before anything exists. ``docs/spec.md`` is the contract; the README is its first five
8
+ sections.
7
9
  """
8
10
 
9
11
  from __future__ import annotations
10
12
 
11
- from . import store, units, write
13
+ from . import grid, store, units, write
12
14
  from .array import Pyramid, WorldArray, create, metadata, open, open_pyramid, open_tensorstore
13
15
  from .context import capped_context, capped_context_spec, shared_context
14
16
  from .geometry import Box, index_range
17
+ from .grid import Block, Grid, blocks, level_factors
15
18
  from .levels import select_level
16
19
  from .placement import ArrayMeta, corner_to_centre, default_axes
17
20
  from .retry import Retry
@@ -25,19 +28,24 @@ from .write import (
25
28
  parse_chunk_key,
26
29
  )
27
30
 
28
- __version__ = "0.2.0"
31
+ __version__ = "0.3.0"
29
32
 
30
33
  __all__ = [
31
34
  "ArrayMeta",
35
+ "Block",
32
36
  "Box",
37
+ "Grid",
33
38
  "Node",
39
+ "blocks",
34
40
  "chunk_key",
35
41
  "corner_to_centre",
36
42
  "create",
37
43
  "default_axes",
38
44
  "detect",
39
45
  "ensure_group_entry",
46
+ "grid",
40
47
  "index_range",
48
+ "level_factors",
41
49
  "level_placement",
42
50
  "level_shape",
43
51
  "list_chunk_keys",
@@ -551,6 +551,34 @@ class WorldArray:
551
551
  )
552
552
  self.write_index(begin, end, data, partial=partial)
553
553
 
554
+ def blocks(self, block_shape=None, context=0, box=None, clip: bool = True):
555
+ """The array cut into blocks (Behaviour 17): write boxes tiling the stored grid (or a
556
+ voxel ``box``) with ``block_shape`` (default ``write_shape``, so the default blocks are
557
+ aligned writes), read boxes grown by ``context`` and clipped to the array when
558
+ ``clip``. Plain records, nothing scheduled."""
559
+ from .grid import blocks as _blocks
560
+
561
+ return _blocks(
562
+ self._grid.shape,
563
+ self.write_shape if block_shape is None else block_shape,
564
+ context=context,
565
+ box=box,
566
+ clip=clip,
567
+ )
568
+
569
+ def write_chunk(self, index: Sequence[int], data: np.ndarray) -> None:
570
+ """Write the whole write unit at ``index`` on the storage grid (the shard of a sharded
571
+ array); ``data`` has that unit's shape, clipped at the array's end."""
572
+ unit, shape = self.write_shape, self._grid.shape
573
+ index = tuple(int(i) for i in index)
574
+ if len(index) != len(shape):
575
+ raise ValueError(f"{self.path}: index needs one entry per axis of {shape}")
576
+ begin = tuple(i * u for i, u in zip(index, unit))
577
+ if any(i < 0 or b >= n for i, b, n in zip(index, begin, shape)):
578
+ raise ValueError(f"{self.path}: no write unit at {index}")
579
+ end = tuple(min(b + u, n) for b, u, n in zip(begin, unit, shape))
580
+ self.write_index(begin, end, data)
581
+
554
582
  def read_index(
555
583
  self,
556
584
  begin: Sequence[int],
@@ -1051,11 +1079,12 @@ class Pyramid:
1051
1079
 
1052
1080
  def create(
1053
1081
  path: str,
1054
- shape: Sequence[int],
1055
- dtype,
1056
- voxel_size,
1057
- translation,
1082
+ shape: Sequence[int] | None = None,
1083
+ dtype=None,
1084
+ voxel_size=None,
1085
+ translation=None,
1058
1086
  *,
1087
+ grid=None,
1059
1088
  axes: Sequence[str] | None = None,
1060
1089
  units: Units = "nanometer",
1061
1090
  chunks: Sequence[int] | None = None,
@@ -1079,6 +1108,19 @@ def create(
1079
1108
  raises ``FileExistsError``; with ``"adopt"`` an identical array (shape, dtype, chunks,
1080
1109
  shards, placement) is opened for writing and a different one raises ``ValueError``
1081
1110
  naming every difference. Returns a writable ``WorldArray``."""
1111
+ if grid is not None:
1112
+ # a Grid planned the output (Behaviour 18): its facts are the array's, chunks and
1113
+ # shards overridable; the placement arguments must not also be given
1114
+ if any(v is not None for v in (shape, voxel_size, translation, axes)):
1115
+ raise ValueError("pass grid= or shape, voxel_size, translation and axes, not both")
1116
+ shape, voxel_size, translation = grid.shape, grid.voxel_size, grid.translation
1117
+ axes, units = grid.axes, grid.units
1118
+ chunks = grid.chunks if chunks is None else chunks
1119
+ shards = grid.shards if shards is None else shards
1120
+ if shape is None or dtype is None or voxel_size is None or translation is None:
1121
+ raise ValueError(
1122
+ "create needs shape, dtype, voxel_size and translation, or grid= and dtype"
1123
+ )
1082
1124
  if chunks is None:
1083
1125
  raise ValueError("chunks is required: the package never picks a chunk shape")
1084
1126
  ndim = len(shape)