Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,50 @@
# Changelog

## Unreleased
## 0.2.0 (2026-10-22)

- Specification only, no code change: `docs/spec.md` draft 3 adds the contract for 0.2
(metadata and placed creation: the OME 0.4/0.5 document builder, the level rule, chunk
keys, `create`, opening for write, aligned writes, `add_level`; Behaviours 10 to 16) and
0.3 (grids and blocks: `Grid`, `blocks`, `write_chunk`, `level_factors`, public path
helpers; Behaviours 17 to 19), written from the five writer sessions' answers and a
census of the I/O code the five repos still carry. The package stays read-only until
0.2 ships.
Metadata and placed creation, built to spec draft 3 (Behaviours 10 to 16). Nothing on the
read side changes.

- `ome_multiscales(axes, units, levels, version)` builds the complete OME-NGFF 0.4 or 0.5
group document with one explicit placement per level; `level_placement` and
`level_shape` derive a level from factors and an origin (Convention 1's rule inverted,
so a level built from a ROI origin is placed from it; `level_shape` absorbs float-noise
factors such as `0.3 / 0.1`). Units are written in OME's names (`"nm"` becomes
`"nanometer"`; `units.ome_name`), an unknown unit as given; the multiscale's `type` is
written when given. Expected documents are pinned in `tests/fixtures/write.json`, shared
with chunkmirage's TypeScript builder, each case with its resolved per-axis units and
types, fifteen cases.
- `create(path, shape, dtype, voxel_size, translation, chunks=..., ...)` writes the group
document (made when absent, else its one dataset entry added or replaced, every other
attribute kept) and then the array, as zarr v2 with OME 0.4 or zarr v3 with OME 0.5
following the group's format. `chunks` is required, codecs are explicit with a zstd
level 3 default, shards are a zarr v3 option. `exists` is `"error"` or `"adopt"` (an
identical array is opened for writing, a different one raises naming every difference,
a creation lost to another process is adopted after a short backoff). Nothing is ever
deleted, truncated or overwritten. Write units equal to the fill value are not stored
unless `store_fill=True`, and a stored unit written so loses its key, as zarr-python's
`write_empty_chunks=False` does. `create` also writes a bare group document for every
missing ancestor down from the `.zarr` root and follows the nearest existing ancestor's
zarr version, so a level made in a zarr v2 container is zarr v2 with OME 0.4 and the
hierarchy browses (cellmap-analyze, cellmap-flow). Adopting an identical array writes no
group document; after a write the document is read again and a lost entry is written
again, then raises (registration-station's concurrent block jobs).
- `ensure_group_entry` is public: list an array the package did not create, such as a
symlinked `s0` (mesh-n-bone), or a level filled before it is listed.
- `open(path, mode="r+")` opens an existing array for writing; a `WorldArray` pickles by
its arguments and its context's spec and reopens in the receiving process, a capped
context staying capped (mesh-n-bone's workers). `write.codecs_of` takes a path.
- `write_index(begin, end, data)` writes whole write units only (the shard when sharded,
else the chunk; the clipped last unit allowed), so writers in different processes need
no coordination; `partial=True` does tensorstore's read-modify-write. `write(box, data)`
maps a world box by the centre rule first. No cast, no padding, no resampling on write.
- `open_pyramid(path, mode="r+").add_level(name, factors=, origin=)` creates a level's
entry and empty array, placed and sized from the level below with its dtype, chunks,
shards, codecs and fill value copied; the downsampling itself stays with the caller.
- `chunk_key` and `parse_chunk_key` for zarr v2, zarr v3 (both encodings, sharded arrays
keyed by shard), N5 and precomputed, as free functions and on `WorldArray`;
`list_chunks` uses the same parser. `write_shape`, `ArrayMeta.shard_shape`,
`chunks_covering(write=True)`.
## 0.1.1 (2026-10-22)

- funlib `voxel_size`/`offset` on the parent group place an array that carries none, for
Expand Down
36 changes: 32 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ resampled when no level matches, fast enough for a model server or a thousand cl
workers, with retries for flaky object stores and thread caps for shared hosts. No one
package did all of that, so every tool wrote its own reader, and the readers drifted apart. worldarray is that reader, written once, with the rules stated and tested.

Its dependencies are `tensorstore` and `numpy`. It is read-only in this version. The
Its dependencies are `tensorstore` and `numpy`. Since 0.2 it also writes the one thing
every tool wrote for itself and got wrong in its own way: the OME-NGFF metadata of a placed
array or pyramid level, and the array itself, created once and filled by aligned writes
from any number of jobs. It never deletes, overwrites or downsamples. The
[specification](https://janeliascicomp.github.io/worldarray/spec/) is the contract it is
built to; every rule below is a test.

Expand Down Expand Up @@ -56,6 +59,13 @@ vol.read_index((0, 0, 0), (64, 64, 64), padding=0)

pyr = wa.open_pyramid(path) # every level, opened on first use
pyr.select((16, 16, 16), mode="floor") # the level to read at 16 nm

# writing: a placed level, its OME-NGFF group document written for it (zarr v3 + OME 0.5 here)
out = wa.create("out.zarr/s0", shape=(512, 512, 512), dtype="uint64",
voxel_size=(8, 8, 8), translation=(4, 4, 4), chunks=(64, 64, 64))
out.write_index((0, 0, 0), (64, 64, 64), block) # whole chunks only: many jobs, no locks
out.write(box, block) # or by world box
s1 = wa.open_pyramid("out.zarr", mode="r+").add_level("s1", factors=2) # placed and sized; you fill it
```

## What it does
Expand All @@ -80,6 +90,13 @@ pyr.select((16, 16, 16), mode="floor") # the level to read at 16 nm
cancelled future on timeout.
- **Carries the geometry**: `Box` with `intersect`, `union`, `contains`, `grow`, `shift`,
`snap` to a grid, and the voxel indices a world box selects.
- **Writes placed arrays** (0.2): the OME-NGFF 0.4 or 0.5 document from voxel size, centre
translation, axes and units, with each level's placement explicit or derived from
factors and an origin; `create` following the container's format and writing the group
documents an output needs down from its `.zarr` root, with explicit chunks and codecs,
and only two answers to "it exists": raise, or adopt an identical array after checking;
whole-unit writes from many processes with no coordination; `add_level` as metadata plus
an empty array; chunk keys for every format. Nothing is ever deleted or overwritten.

## The placement rule

Expand All @@ -93,7 +110,7 @@ moved half a voxel on reading:
| OME-NGFF `translation` | centre | as is |
| COSEM `transform.translate` | centre | as is |
| `offset` beside `resolution` / `voxel_size` (`offset` missing: 0) | corner | `offset + voxel_size / 2` |
| N5 `pixelResolution` or `resolution` with `downsamplingFactors f`, no offset | BigDataViewer | `(f - 1) / 2 * base` |
| N5 `pixelResolution` or `resolution` with `downsamplingFactors f`, no offset | corner at the origin at every level, as Neuroglancer draws N5 | `f / 2 * base` |
| precomputed `voxel_offset` | corner | `(voxel_offset + 1/2) * resolution` |
| nothing | a plain array | voxels of 1, corner at the origin |

Expand All @@ -117,13 +134,24 @@ wa.open_tensorstore(path, level=None, context=None)
wa.shared_context(cache_bytes=0) ; wa.capped_context(cache_bytes=0, concurrency=1, network=True) ; wa.Retry(...)
wa.list_chunk_keys(path, separator=".") # chunk indices from keys alone, no metadata needed
wa.Box ; wa.units.TABLE

# writing (0.2)
wa.create(path, shape, dtype, voxel_size, translation, *, axes=None, units="nanometer", chunks=..., shards=None,
fill_value=0, codecs="zstd", format=None, attrs=None, group_attrs=None, exists="error", store_fill=False) -> WorldArray
wa.open(path, mode="r+") ; wa.open_pyramid(path, mode="r+").add_level(name, factors=, origin=0, ...)
wa.ome_multiscales(axes, units, levels, version="0.5", type=None) ; wa.level_placement(voxel_size, translation, factors, origin=0)
wa.level_shape(shape, factors, origin=0) ; wa.chunk_key(index, format, ...) ; wa.parse_chunk_key(key, format, ...)
wa.ensure_group_entry(group_path, name, voxel_size, translation, axes, units="nanometer") # list an array you made yourself
wa.units.ome_name("nm") -> "nanometer" ; wa.write.codecs_of(path) -> (codecs, shards)
```

`WorldArray`: `ts`, `meta`, `grid`, `shape`, `dtype`, `axes`, `voxel_size`, `translation`,
`corner`, `box`, `levels`, `level_name`, `level_path`, `source_voxel_size`, `resampled`;
`corner`, `box`, `levels`, `level_name`, `level_path`, `source_voxel_size`, `resampled`,
`writable`, `write_shape`;
`read(box, voxel_size=, padding=, method=, mask=, anchor=, transform=, post_transform=, select=, strict=)`,
`read_index(begin, end, padding=, mask=, select=, transform=)`, `index_box(box)`,
`chunks_covering(begin, end)`, `list_chunks()`, `reopen(context=)`.
`chunks_covering(begin, end, write=)`, `list_chunks()`, `chunk_key(index)`, `parse_chunk_key(key)`,
`write_index(begin, end, data, partial=)`, `write(box, data, partial=)`, `reopen(context=)`.

A box is a `(begin, end)` pair of floats in your units, half-open per axis, over the
spatial axes or every axis; any object with `.begin`/`.end` or `.offset`/`.shape` also
Expand Down
Loading
Loading