worldarray 0.3.1__tar.gz → 0.4.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.
- {worldarray-0.3.1 → worldarray-0.4.0}/CHANGELOG.md +27 -2
- {worldarray-0.3.1 → worldarray-0.4.0}/PKG-INFO +7 -6
- {worldarray-0.3.1 → worldarray-0.4.0}/README.md +6 -5
- {worldarray-0.3.1 → worldarray-0.4.0}/docs/spec.md +72 -17
- {worldarray-0.3.1 → worldarray-0.4.0}/pyproject.toml +1 -1
- {worldarray-0.3.1 → worldarray-0.4.0}/src/worldarray/__init__.py +1 -1
- {worldarray-0.3.1 → worldarray-0.4.0}/src/worldarray/array.py +103 -11
- {worldarray-0.3.1 → worldarray-0.4.0}/src/worldarray/write.py +10 -1
- {worldarray-0.3.1 → worldarray-0.4.0}/tests/test_read.py +6 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/tests/test_resample.py +42 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/tests/test_write.py +51 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/uv.lock +1 -1
- {worldarray-0.3.1 → worldarray-0.4.0}/.github/workflows/ci.yml +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/.github/workflows/docs.yml +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/.github/workflows/publish.yml +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/.gitignore +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/LICENSE +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/docs/changelog.md +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/docs/index.md +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/mkdocs.yml +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/src/worldarray/context.py +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/src/worldarray/geometry.py +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/src/worldarray/grid.py +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/src/worldarray/levels.py +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/src/worldarray/placement.py +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/src/worldarray/resample.py +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/src/worldarray/retry.py +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/src/worldarray/store.py +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/src/worldarray/units.py +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/tests/conftest.py +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/tests/fixtures/metadata.json +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/tests/fixtures/write.json +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/tests/test_access.py +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/tests/test_geometry.py +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/tests/test_grid.py +0 -0
- {worldarray-0.3.1 → worldarray-0.4.0}/tests/test_metadata.py +0 -0
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
cellmap-analyze's three asks, so it can call worldarray directly with no wrapper, and two
|
|
6
|
+
of cellmap-flow's. No placement change and no unit table change.
|
|
7
|
+
|
|
8
|
+
- `vol.read_onto(grid, begin=None, end=None, ...)` reads voxels `[begin, end)` of a
|
|
9
|
+
`Grid`'s spatial axes from the array, exactly that shape (Behaviour 20). It is the
|
|
10
|
+
`read` of the grid's index box at the grid's voxel size anchored at its corner, so at
|
|
11
|
+
the array's own voxel size nothing is resampled, whatever the sub-voxel offset, and
|
|
12
|
+
otherwise the array is resampled onto the grid. The grid's units are converted.
|
|
13
|
+
- A pickled handle, and `reopen`, use the path resolved against the working directory at
|
|
14
|
+
open, so a worker that changes directory before unpickling opens the same store; it
|
|
15
|
+
used to raise `FileNotFoundError` for a relative path.
|
|
16
|
+
- `write_index`, `write` and `write_chunk` take a scalar, which fills the box. It is
|
|
17
|
+
converted to the dtype only when its value survives (`0` into `uint8`; `300`, `-1`, `0.5`
|
|
18
|
+
and NaN raise), and the fill value over whole units leaves them absent.
|
|
19
|
+
- `vol.source_translation`: the stored level's translation in `units`, beside
|
|
20
|
+
`source_voxel_size`, under resample and relabel too (cellmap-flow's `stored_placement`).
|
|
21
|
+
- `create(exists="adopt")` also compares the keys of `attrs` with the existing array's
|
|
22
|
+
attributes and raises naming each that differs or is missing; keys not given are not
|
|
23
|
+
compared. An adopt given different `attrs` used to keep the old ones silently, as funlib's
|
|
24
|
+
`prepare_ds` kept an old offset (cellmap-flow's `_refuse_another_region`).
|
|
25
|
+
|
|
3
26
|
## 0.3.1 (2026-10-22)
|
|
4
27
|
|
|
5
28
|
- `source_voxel_size` reports the stored level's voxel size under `on_mismatch="relabel"`,
|
|
@@ -10,8 +33,10 @@
|
|
|
10
33
|
`Grid.from_meta(meta, voxel_size=t).box` already was; it was the stored data's extent, so
|
|
11
34
|
`read(box)` could return fewer voxels than `shape` (cellmap-flow's 7 voxels of 8 nm at
|
|
12
35
|
24 nm: box ended at 56 nm, now 72). No voxel moves.
|
|
13
|
-
- The unit table knows kilometres (`kilometer`, `kilometre`, `km`,
|
|
14
|
-
convert, abbreviate to `km` and are written as OME's `kilometer`
|
|
36
|
+
- **Unit table change.** The unit table knows kilometres (`kilometer`, `kilometre`, `km`,
|
|
37
|
+
1e3 m), so they convert, abbreviate to `km` and are written as OME's `kilometer`
|
|
38
|
+
(chunkmirage's ask). This should have been a minor release (spec section 8): cellmap-flow
|
|
39
|
+
exports the table and its CI failed until it moved its pin to `>=0.3.1`.
|
|
15
40
|
- README: the placement rule gains the corner formula, a worked example, why the centre
|
|
16
41
|
is the model, and how the read rule follows from it.
|
|
17
42
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: worldarray
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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/
|
|
@@ -109,6 +109,7 @@ six = wa.open(path, voxel_size=(6, 6, 6), on_mismatch="resample")
|
|
|
109
109
|
|
|
110
110
|
# one volume read onto another volume's grid, for masks, contact sites, statistics
|
|
111
111
|
onto = raw.read(box, voxel_size=labels.voxel_size, anchor=labels.corner)
|
|
112
|
+
onto = raw.read_onto(grid, begin, end) # voxels [begin, end) of a Grid, exactly that shape
|
|
112
113
|
|
|
113
114
|
# index space, native units: what an engine or a viewer wants
|
|
114
115
|
vol = wa.open(path, units=None)
|
|
@@ -159,8 +160,8 @@ s1 = wa.open_pyramid("out.zarr", mode="r+").add_level("s1", factors=2) # place
|
|
|
159
160
|
- **Plans outputs and blocks** (0.3): a store-less `Grid` with the same geometry as an
|
|
160
161
|
array, so an output is planned, cut into blocks and only then created; `blocks` tiling an
|
|
161
162
|
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.
|
|
163
|
+
picklable records a cluster job takes by id; the anisotropy-aware pyramid schedule; every
|
|
164
|
+
input read onto that grid by its voxel indices (0.4). Nothing is scheduled or run.
|
|
164
165
|
|
|
165
166
|
## The placement rule
|
|
166
167
|
|
|
@@ -227,12 +228,12 @@ wa.units.abbreviation("nanometer") ; wa.store.join(path, "s0") ; wa.store.parent
|
|
|
227
228
|
```
|
|
228
229
|
|
|
229
230
|
`WorldArray`: `ts`, `meta`, `grid`, `shape`, `dtype`, `axes`, `voxel_size`, `translation`,
|
|
230
|
-
`corner`, `box`, `levels`, `level_name`, `level_path`, `source_voxel_size`, `resampled`,
|
|
231
|
+
`corner`, `box`, `levels`, `level_name`, `level_path`, `source_voxel_size`, `source_translation`, `resampled`,
|
|
231
232
|
`writable`, `write_shape`;
|
|
232
233
|
`read(box, voxel_size=, padding=, method=, mask=, anchor=, transform=, post_transform=, select=, strict=)`,
|
|
233
|
-
`read_index(begin, end, padding=, mask=, select=, transform=)`, `index_box(box)`,
|
|
234
|
+
`read_index(begin, end, padding=, mask=, select=, transform=)`, `read_onto(grid, begin=, end=, ...)`, `index_box(box)`,
|
|
234
235
|
`chunks_covering(begin, end, write=)`, `list_chunks()`, `chunk_key(index)`, `parse_chunk_key(key)`,
|
|
235
|
-
`write_index(begin, end, data, partial=)
|
|
236
|
+
`write_index(begin, end, data, partial=)` and `write(box, data, partial=)` (`data` an array or a scalar), `reopen(context=)`.
|
|
236
237
|
|
|
237
238
|
A box is a `(begin, end)` pair of floats in your units, half-open per axis, over the
|
|
238
239
|
spatial axes or every axis; any object with `.begin`/`.end` or `.offset`/`.shape` also
|
|
@@ -50,6 +50,7 @@ six = wa.open(path, voxel_size=(6, 6, 6), on_mismatch="resample")
|
|
|
50
50
|
|
|
51
51
|
# one volume read onto another volume's grid, for masks, contact sites, statistics
|
|
52
52
|
onto = raw.read(box, voxel_size=labels.voxel_size, anchor=labels.corner)
|
|
53
|
+
onto = raw.read_onto(grid, begin, end) # voxels [begin, end) of a Grid, exactly that shape
|
|
53
54
|
|
|
54
55
|
# index space, native units: what an engine or a viewer wants
|
|
55
56
|
vol = wa.open(path, units=None)
|
|
@@ -100,8 +101,8 @@ s1 = wa.open_pyramid("out.zarr", mode="r+").add_level("s1", factors=2) # place
|
|
|
100
101
|
- **Plans outputs and blocks** (0.3): a store-less `Grid` with the same geometry as an
|
|
101
102
|
array, so an output is planned, cut into blocks and only then created; `blocks` tiling an
|
|
102
103
|
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.
|
|
104
|
+
picklable records a cluster job takes by id; the anisotropy-aware pyramid schedule; every
|
|
105
|
+
input read onto that grid by its voxel indices (0.4). Nothing is scheduled or run.
|
|
105
106
|
|
|
106
107
|
## The placement rule
|
|
107
108
|
|
|
@@ -168,12 +169,12 @@ wa.units.abbreviation("nanometer") ; wa.store.join(path, "s0") ; wa.store.parent
|
|
|
168
169
|
```
|
|
169
170
|
|
|
170
171
|
`WorldArray`: `ts`, `meta`, `grid`, `shape`, `dtype`, `axes`, `voxel_size`, `translation`,
|
|
171
|
-
`corner`, `box`, `levels`, `level_name`, `level_path`, `source_voxel_size`, `resampled`,
|
|
172
|
+
`corner`, `box`, `levels`, `level_name`, `level_path`, `source_voxel_size`, `source_translation`, `resampled`,
|
|
172
173
|
`writable`, `write_shape`;
|
|
173
174
|
`read(box, voxel_size=, padding=, method=, mask=, anchor=, transform=, post_transform=, select=, strict=)`,
|
|
174
|
-
`read_index(begin, end, padding=, mask=, select=, transform=)`, `index_box(box)`,
|
|
175
|
+
`read_index(begin, end, padding=, mask=, select=, transform=)`, `read_onto(grid, begin=, end=, ...)`, `index_box(box)`,
|
|
175
176
|
`chunks_covering(begin, end, write=)`, `list_chunks()`, `chunk_key(index)`, `parse_chunk_key(key)`,
|
|
176
|
-
`write_index(begin, end, data, partial=)
|
|
177
|
+
`write_index(begin, end, data, partial=)` and `write(box, data, partial=)` (`data` an array or a scalar), `reopen(context=)`.
|
|
177
178
|
|
|
178
179
|
A box is a `(begin, end)` pair of floats in your units, half-open per axis, over the
|
|
179
180
|
spatial axes or every axis; any object with `.begin`/`.end` or `.offset`/`.shape` also
|
|
@@ -149,8 +149,9 @@ vol.meta # ArrayMeta in the store's own units
|
|
|
149
149
|
vol.grid # ArrayMeta in the requested units, every axis
|
|
150
150
|
vol.shape, vol.dtype, vol.axes, vol.spatial, vol.level_name, vol.levels, vol.remote
|
|
151
151
|
# shape is the stored shape, or with a resample target the target voxels covering the data
|
|
152
|
-
vol.source_voxel_size, vol.
|
|
153
|
-
|
|
152
|
+
vol.source_voxel_size, vol.source_translation, vol.resampled
|
|
153
|
+
# the stored level's voxel size and translation (0.4), also under resample and relabel; whether
|
|
154
|
+
# reads resample to a target
|
|
154
155
|
vol.list_chunks() # the chunk indices that exist in storage, by listing keys (zarr v2/v3, N5, unsharded precomputed)
|
|
155
156
|
vol.path # what the caller passed (a group, say); vol.level_path is the array actually opened
|
|
156
157
|
vol.voxel_size # tuple[float] in `units`, spatial axes, the array's order (zyx for a volume)
|
|
@@ -285,7 +286,8 @@ vol = wa.create(
|
|
|
285
286
|
attrs=None, # extra array attributes, merged into the array's own
|
|
286
287
|
group_attrs=None, # extra group attributes, merged; no one else's key is dropped or rewritten (Behaviour 12)
|
|
287
288
|
exists="error", # "error": anything already at `path` raises | "adopt": an identical array (shape, dtype, chunks,
|
|
288
|
-
# shards, placement within rtol 1e-6
|
|
289
|
+
# shards, placement within rtol 1e-6, and from 0.4 every key of `attrs` holding the value given)
|
|
290
|
+
# is opened for writing; a different one raises naming each
|
|
289
291
|
# difference. There is no third value. An adopt writes no group document when the entry
|
|
290
292
|
# already says the same (Behaviour 12)
|
|
291
293
|
store_fill=False, # True stores write units equal to fill_value instead of leaving them absent
|
|
@@ -337,6 +339,20 @@ wa.store.join(path, *parts) ; wa.store.parent(path) ; wa.store.basename(path)
|
|
|
337
339
|
wa.units.abbreviation(unit) -> str # the OME short form, "nm" for any spelling of nanometre the table knows
|
|
338
340
|
```
|
|
339
341
|
|
|
342
|
+
### 0.4: reading onto a grid
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
vol.read_onto(grid, begin=None, end=None, padding=None, method=None, mask=False, retries=None, transform=None,
|
|
346
|
+
post_transform=None, select=None)
|
|
347
|
+
# voxels [begin, end) of a Grid's spatial axes (all of it when None), read from this array:
|
|
348
|
+
# exactly that shape, by the centre rule at the array's voxel size, else resampled onto the
|
|
349
|
+
# grid anchored at its corner (Behaviour 20)
|
|
350
|
+
vol.write_index(begin, end, 0) ; vol.write(box, 0) ; vol.write_chunk(index, 0)
|
|
351
|
+
# a scalar fills the box, converted to the dtype only when its value survives (Behaviour 13)
|
|
352
|
+
vol.source_translation # the stored level's translation in `units`, beside source_voxel_size, under resample and relabel
|
|
353
|
+
wa.create(..., attrs={...}, exists="adopt") # an adopt also compares the keys of `attrs` (Behaviour 11)
|
|
354
|
+
```
|
|
355
|
+
|
|
340
356
|
## 3. Conventions
|
|
341
357
|
|
|
342
358
|
These are the rules the package exists to hold in one place. They match chunkmirage's
|
|
@@ -682,9 +698,11 @@ Behaviours 10 to 16 are 0.2; 17 to 19 are 0.3.
|
|
|
682
698
|
compression; cellmap-flow's blosc stays blosc by passing it. There is no overwrite: with
|
|
683
699
|
`exists="error"` any document already at `path` raises `FileExistsError`; with
|
|
684
700
|
`exists="adopt"` an array whose shape, dtype, chunks, shards and placement match
|
|
685
|
-
(placement within `rtol 1e-6`)
|
|
686
|
-
|
|
687
|
-
and
|
|
701
|
+
(placement within `rtol 1e-6`), and whose attributes hold every key of `attrs` with the
|
|
702
|
+
value given as JSON stores it (keys not given are someone else's and not compared), is
|
|
703
|
+
opened for writing and its dataset entry ensured, and one that differs raises
|
|
704
|
+
`ValueError` listing every differing field and key with the value found and the value
|
|
705
|
+
wanted (cellmap-flow's `_refuse_another_region`, registration-station's
|
|
688
706
|
missing check, which let a shorter last band's chunk grid win a creation race). A
|
|
689
707
|
creation that loses a race, the store reporting the array present after the probe said
|
|
690
708
|
absent, is re-read with backoff, 5 attempts at `0.5 s * attempt`, and then treated as an
|
|
@@ -697,7 +715,9 @@ Behaviours 10 to 16 are 0.2; 17 to 19 are 0.3.
|
|
|
697
715
|
being the one it replaces. `create` returns a writable `WorldArray`;
|
|
698
716
|
`wa.open(path, mode="r+")` opens an existing one with every rule of Conventions 3 to 5
|
|
699
717
|
unchanged, and a handle pickles by its arguments and its context's spec, so each worker
|
|
700
|
-
process opens its own cheaply
|
|
718
|
+
process opens its own cheaply (a local path goes resolved against the working directory
|
|
719
|
+
at open, so a worker that changes directory before unpickling, as cellmap-analyze's dask
|
|
720
|
+
workers do, opens the same store; `reopen` uses it too), on the shared context when the handle had one, else on
|
|
701
721
|
a context rebuilt once per process from the same spec, so a capped context stays
|
|
702
722
|
capped (mesh-n-bone's per-process cache and thread caps).
|
|
703
723
|
|
|
@@ -722,7 +742,12 @@ Behaviours 10 to 16 are 0.2; 17 to 19 are 0.3.
|
|
|
722
742
|
|
|
723
743
|
13. **Aligned writes need no coordination.** `write_index(begin, end, data)` requires
|
|
724
744
|
`data.shape == end - begin`, `data.dtype == vol.dtype` (no silent cast; `ValueError`),
|
|
725
|
-
|
|
745
|
+
or `data` a scalar (a number or a 0-d array) that fills the box: it is converted to the
|
|
746
|
+
dtype only when its value survives, so an integer or bool array takes whole numbers in
|
|
747
|
+
its range and a float array any number it does not overflow, rounded to its precision
|
|
748
|
+
(`0` into `uint8` is written; `300`, `-1`, `0.5` and NaN raise), and a scalar equal to
|
|
749
|
+
`fill_value` over whole units leaves them absent as an all-fill array does (cellmap-analyze
|
|
750
|
+
writes a constant for every empty or masked-out block). The box must be a union of whole write units: every bound a multiple of
|
|
726
751
|
`write_shape` on its axis or the array's end on that axis, so the clipped last unit is
|
|
727
752
|
allowed; otherwise `ValueError` names the axis, the bound and the unit size. A unit
|
|
728
753
|
written whole is one put, so writers in different processes writing different units
|
|
@@ -809,6 +834,22 @@ Behaviours 10 to 16 are 0.2; 17 to 19 are 0.3.
|
|
|
809
834
|
returns the OME short form for any spelling the table knows and raises for one it does
|
|
810
835
|
not.
|
|
811
836
|
|
|
837
|
+
20. **Reading onto a grid.** `vol.read_onto(grid, begin, end)` returns voxels `[begin, end)`
|
|
838
|
+
of a `Grid`'s spatial axes, all of it when `begin` and `end` are None, read from this
|
|
839
|
+
array, with exactly `end - begin` voxels on those axes; the array's other axes are read
|
|
840
|
+
as `read` reads them (whole, or pinned by `select`), the open-time `padding`, `method`,
|
|
841
|
+
`retries` and `transform` apply, and `mask` marks the grid voxels that held data. It is
|
|
842
|
+
`read(Box.from_index(begin, end, grid voxel size, grid translation), voxel_size=grid
|
|
843
|
+
voxel size, anchor=grid corner)`, so Behaviours 2 and 4 decide every voxel: at the
|
|
844
|
+
array's own voxel size (within level selection's rtol 1e-6) nothing is resampled,
|
|
845
|
+
whatever the offset between the grids, and each grid voxel takes the stored voxel whose
|
|
846
|
+
centre lies in it, never an interpolation, with `method="linear"` too; at any other
|
|
847
|
+
voxel size the array is resampled onto the grid, anchored at its corner. Adjoining
|
|
848
|
+
`[begin, end)` reads therefore agree with one read across both. The grid's spatial axes
|
|
849
|
+
must be named as the array's are (`ValueError` otherwise), and its units are converted
|
|
850
|
+
to the array's `units`. This is cellmap-analyze's per-operation grid: blocks come from
|
|
851
|
+
one `Grid`, every input is read onto it, and outputs are created on it.
|
|
852
|
+
|
|
812
853
|
## 5. Tests (behavioural; synthetic stores in `tmp_path`; one optional network test)
|
|
813
854
|
|
|
814
855
|
Conformance, one tiny store each, checking `voxel_size`, `translation`, `axes`,
|
|
@@ -898,8 +939,10 @@ factor; missing ancestor documents down from the `.zarr` root and none above it,
|
|
|
898
939
|
ancestor's format followed and a contradiction refused, an array in the way refused; an
|
|
899
940
|
adopt writing no document and a changed placement writing one; a lost entry written again
|
|
900
941
|
and the error after the attempts; `ensure_group_entry` on a foreign array, idempotent; a
|
|
901
|
-
pickled handle keeping a capped context's spec and sharing the shared one
|
|
902
|
-
path; an all-fill write removing its own key
|
|
942
|
+
pickled handle keeping a capped context's spec and sharing the shared one, and a relative
|
|
943
|
+
path reopening after a `chdir`; `codecs_of` by path; an all-fill write removing its own key;
|
|
944
|
+
a scalar filling a box through `write_index`, `write` and `write_chunk`, refused when it
|
|
945
|
+
does not fit the dtype.
|
|
903
946
|
|
|
904
947
|
Grids (0.3): `blocks` partitioning with and without a box, `context` as an int, per axis
|
|
905
948
|
and as before/after pairs, clipping on and off, `len`, `blocks[id]` and `index_of`;
|
|
@@ -908,6 +951,14 @@ and as before/after pairs, clipping on and off, `len`, `blocks[id]` and `index_o
|
|
|
908
951
|
reproducing mesh-n-bone's anisotropic table (8, 8, 30 nm: z stays at 1 until y and x
|
|
909
952
|
reach 32) and all 2s for isotropic data; `abbreviation` for every spelling in the table.
|
|
910
953
|
|
|
954
|
+
Reading onto a grid (0.4): `read_onto` at the array's voxel size returning stored values
|
|
955
|
+
under a sub-voxel offset with `method="linear"`, and the next voxel past a half-voxel one;
|
|
956
|
+
onto a finer grid equal to the `read` it is defined as, in micrometres too, adjoining
|
|
957
|
+
reads agreeing, the mask marking grid voxels before the data; its own grid at voxel sizes
|
|
958
|
+
3.54, 4 and 5.3 returning the stored array exactly; a grid with other axis names refused.
|
|
959
|
+
`source_translation` under relabel and resample and in micrometres; an adopt keeping keys
|
|
960
|
+
it was not given and refusing a different or missing one, naming both values.
|
|
961
|
+
|
|
911
962
|
Target: about 600 lines of test cases for 0.1 and about 300 more for 0.2 and 0.3, the
|
|
912
963
|
store builders in `conftest.py` aside; no wording pins; no `/nrs`; no cluster.
|
|
913
964
|
|
|
@@ -1084,10 +1135,14 @@ tests, minimal reusable code. The README is sections 1 to 5 of this file; a beha
|
|
|
1084
1135
|
is not stated here is not implemented without adding the statement first.
|
|
1085
1136
|
|
|
1086
1137
|
Versioning. Every consumer pins a floating range and its CI installs the newest version in
|
|
1087
|
-
it
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1138
|
+
it within the hour of a release, so the version number says what a release can break. A
|
|
1139
|
+
**patch** release brings the code back to what this spec says, or reads more stores or
|
|
1140
|
+
reads them faster; it places nothing differently and adds nothing public. A **minor**
|
|
1141
|
+
release is anything else: it may move voxels or add API, and an entry added to, renamed in
|
|
1142
|
+
or removed from the unit table is API (`wa.units.TABLE` is public, and cellmap-flow exports
|
|
1143
|
+
it to its browser app behind a freshness test). Both were once shipped as patches and both
|
|
1144
|
+
turned cellmap-flow's CI red on every open PR within the hour: 0.1.1 moved offset-less N5
|
|
1145
|
+
pyramids by half a base voxel, and 0.3.1 added kilometres. A changelog entry starts with
|
|
1146
|
+
**Placement change** when voxels move and **Unit table change** when the table does, and the
|
|
1147
|
+
release message to each consumer repeats those lines; nothing before 1.0 promises more.
|
|
1148
|
+
Consumers that cannot absorb a move between CI runs pin `>=0.x,<0.(x+1)`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "worldarray"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.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" }
|
|
@@ -36,7 +36,15 @@ from .placement import (
|
|
|
36
36
|
read_meta_light,
|
|
37
37
|
)
|
|
38
38
|
from .retry import Retry, read_with_retries
|
|
39
|
-
from .store import
|
|
39
|
+
from .store import (
|
|
40
|
+
Node,
|
|
41
|
+
detect,
|
|
42
|
+
is_remote,
|
|
43
|
+
join,
|
|
44
|
+
normalize_path,
|
|
45
|
+
open_array,
|
|
46
|
+
precomputed_scale_index,
|
|
47
|
+
)
|
|
40
48
|
|
|
41
49
|
logger = logging.getLogger(__name__)
|
|
42
50
|
|
|
@@ -90,6 +98,9 @@ class WorldArray:
|
|
|
90
98
|
if mode not in ("r", "r+"):
|
|
91
99
|
raise ValueError("mode is 'r' (read) or 'r+' (read and write)")
|
|
92
100
|
self.path = path
|
|
101
|
+
# where later opens find it (reopen, a pickle, the pyramid): a local path resolved
|
|
102
|
+
# against the working directory now, so a worker that changes directory opens the same store
|
|
103
|
+
self._where = normalize_path(path)
|
|
93
104
|
self._mode = mode
|
|
94
105
|
self._store_fill = bool(store_fill)
|
|
95
106
|
self.padding = padding
|
|
@@ -139,8 +150,9 @@ class WorldArray:
|
|
|
139
150
|
self._meta = self._rescaled_to_base(base_voxel_size, base_units)
|
|
140
151
|
space, time = _unit_targets(units)
|
|
141
152
|
self._grid = self._meta.in_units(space=space, time=time, strict=strict_units)
|
|
142
|
-
# the stored level's spatial
|
|
153
|
+
# the stored level's spatial placement, before any relabel renames the grid
|
|
143
154
|
self._stored_voxel_size = tuple(self._grid.voxel_size[i] for i in self._grid.spatial)
|
|
155
|
+
self._stored_translation = tuple(self._grid.translation[i] for i in self._grid.spatial)
|
|
144
156
|
|
|
145
157
|
self._target: tuple[float, ...] | None = None
|
|
146
158
|
if voxel_size is not None:
|
|
@@ -305,6 +317,13 @@ class WorldArray:
|
|
|
305
317
|
is, whether reads resample it or ``on_mismatch="relabel"`` renames its grid."""
|
|
306
318
|
return self._stored_voxel_size
|
|
307
319
|
|
|
320
|
+
@property
|
|
321
|
+
def source_translation(self) -> tuple[float, ...]:
|
|
322
|
+
"""The stored level's translation (the centre of its voxel 0) on the spatial axes, in
|
|
323
|
+
``units``: where the level is, whether reads resample it or ``on_mismatch="relabel"``
|
|
324
|
+
renames its grid."""
|
|
325
|
+
return self._stored_translation
|
|
326
|
+
|
|
308
327
|
@property
|
|
309
328
|
def resampled(self) -> bool:
|
|
310
329
|
"""Whether reads resample to a target voxel size (``on_mismatch="resample"``)."""
|
|
@@ -373,7 +392,7 @@ class WorldArray:
|
|
|
373
392
|
def levels_pyramid(self) -> Pyramid:
|
|
374
393
|
if self._pyramid is None:
|
|
375
394
|
self._pyramid = Pyramid(
|
|
376
|
-
self.
|
|
395
|
+
self._where, context=self.context, units=self.units, select=self._select
|
|
377
396
|
)
|
|
378
397
|
return self._pyramid
|
|
379
398
|
|
|
@@ -422,14 +441,15 @@ class WorldArray:
|
|
|
422
441
|
**overrides,
|
|
423
442
|
}
|
|
424
443
|
return WorldArray(
|
|
425
|
-
self.
|
|
444
|
+
self._where, context=context if context is not None else self.context, **opts
|
|
426
445
|
)
|
|
427
446
|
|
|
428
447
|
def __reduce__(self):
|
|
429
448
|
"""A handle pickles by its arguments and its context's recipe, and reopens in the
|
|
430
449
|
receiving process (Behaviour 11): a worker opens its own store, cheaply, on a shared
|
|
431
450
|
context when the handle had one, else on a context rebuilt from the same spec, so a
|
|
432
|
-
capped context stays capped.
|
|
451
|
+
capped context stays capped. A local path goes as resolved at open, so a worker that
|
|
452
|
+
changed directory still finds the store."""
|
|
433
453
|
opts = {
|
|
434
454
|
**self._opts,
|
|
435
455
|
"padding": self.padding,
|
|
@@ -439,7 +459,7 @@ class WorldArray:
|
|
|
439
459
|
"retries": self.retries,
|
|
440
460
|
"transform": self.transform,
|
|
441
461
|
}
|
|
442
|
-
return (_reopen, (self.
|
|
462
|
+
return (_reopen, (self._where, opts, recipe_of(self.context)))
|
|
443
463
|
|
|
444
464
|
# --- index space ------------------------------------------------------------
|
|
445
465
|
|
|
@@ -520,7 +540,9 @@ class WorldArray:
|
|
|
520
540
|
or the array's end), so writers in different processes need no coordination; a
|
|
521
541
|
unit written whole is one put. ``partial=True`` lifts the check and does tensorstore's
|
|
522
542
|
read-modify-write of the touched units, unsafe while anything else writes the same
|
|
523
|
-
unit.
|
|
543
|
+
unit. ``data`` may be a scalar, which fills the box: it is converted to the array's
|
|
544
|
+
dtype only when the value survives (``0`` into ``uint8``, not ``300`` or ``0.5``).
|
|
545
|
+
No other cast, no padding, no resampling; the write commits before returning."""
|
|
524
546
|
if not self.writable:
|
|
525
547
|
raise ValueError(
|
|
526
548
|
f"{self.path} was opened read-only; open it with mode='r+' or create it"
|
|
@@ -535,9 +557,11 @@ class WorldArray:
|
|
|
535
557
|
if len(begin) != len(shape) or len(end) != len(shape):
|
|
536
558
|
raise ValueError(f"{self.path}: begin and end need one entry per axis of {shape}")
|
|
537
559
|
wanted = tuple(e - b for b, e in zip(begin, end))
|
|
538
|
-
if data.
|
|
560
|
+
if data.ndim == 0:
|
|
561
|
+
data = self._scalar(data)
|
|
562
|
+
elif data.shape != wanted:
|
|
539
563
|
raise ValueError(f"{self.path}: data has shape {data.shape}; the box is {wanted}")
|
|
540
|
-
|
|
564
|
+
elif data.dtype != self.dtype:
|
|
541
565
|
raise ValueError(
|
|
542
566
|
f"{self.path}: data is {data.dtype}, the array {self.dtype}; nothing is cast on write"
|
|
543
567
|
)
|
|
@@ -555,6 +579,22 @@ class WorldArray:
|
|
|
555
579
|
)
|
|
556
580
|
self._store[tuple(slice(b, e) for b, e in zip(begin, end))].write(data).result()
|
|
557
581
|
|
|
582
|
+
def _scalar(self, value: np.ndarray) -> np.ndarray:
|
|
583
|
+
"""A 0-d ``value`` in the array's dtype, or ``ValueError`` when converting changes it:
|
|
584
|
+
an integer or bool array takes only whole numbers in its range, a float array any
|
|
585
|
+
number that does not overflow it (rounded to its precision)."""
|
|
586
|
+
dtype = self.dtype
|
|
587
|
+
with np.errstate(invalid="ignore", over="ignore"), warnings.catch_warnings():
|
|
588
|
+
warnings.simplefilter("ignore")
|
|
589
|
+
out = value.astype(dtype)
|
|
590
|
+
if dtype.kind in "biu":
|
|
591
|
+
kept = bool(out == value)
|
|
592
|
+
else:
|
|
593
|
+
kept = bool(np.isfinite(out) or not np.isfinite(value))
|
|
594
|
+
if not kept:
|
|
595
|
+
raise ValueError(f"{self.path}: {value.item()!r} does not fit {dtype}")
|
|
596
|
+
return out
|
|
597
|
+
|
|
558
598
|
def write(self, box, data: np.ndarray, partial: bool = False) -> None:
|
|
559
599
|
"""Write ``data`` over a world box: the voxels Behaviour 2's centre rule selects, then
|
|
560
600
|
``write_index``. A box reaching outside the array raises; nothing is padded or
|
|
@@ -880,6 +920,57 @@ class WorldArray:
|
|
|
880
920
|
data[~inside] = padding
|
|
881
921
|
return _squeeze((data, inside) if mask else data, drop)
|
|
882
922
|
|
|
923
|
+
def read_onto(
|
|
924
|
+
self,
|
|
925
|
+
grid,
|
|
926
|
+
begin: Sequence[int] | None = None,
|
|
927
|
+
end: Sequence[int] | None = None,
|
|
928
|
+
padding: Any = None,
|
|
929
|
+
method: str | None = None,
|
|
930
|
+
mask: bool = False,
|
|
931
|
+
retries: Retry | None = None,
|
|
932
|
+
transform: Transform | None = None,
|
|
933
|
+
post_transform: Transform | None = None,
|
|
934
|
+
select: dict[str, int] | None = None,
|
|
935
|
+
) -> np.ndarray | tuple[np.ndarray, np.ndarray]:
|
|
936
|
+
"""Voxels ``[begin, end)`` of another grid (a ``Grid``), read from this array
|
|
937
|
+
(Behaviour 20): ``begin`` and ``end`` index the grid's spatial axes, all of it when
|
|
938
|
+
None, and the result has exactly that shape there; other axes are read as ``read``
|
|
939
|
+
reads them. At the array's own voxel size nothing is resampled, however the grids are
|
|
940
|
+
offset: each grid voxel takes the stored voxel whose centre lies in it. Otherwise the
|
|
941
|
+
array is resampled onto the grid by ``method``, anchored at the grid's corner. The
|
|
942
|
+
grid's units are converted to ``units``; its spatial axes must be named as this
|
|
943
|
+
array's are."""
|
|
944
|
+
sp, mine = grid.spatial, self._grid.spatial
|
|
945
|
+
names = tuple(grid.axes[i] for i in sp)
|
|
946
|
+
if names != tuple(self._grid.axes[i] for i in mine):
|
|
947
|
+
raise ValueError(
|
|
948
|
+
f"{self.path}: the grid's spatial axes are {names}; the array's "
|
|
949
|
+
f"{tuple(self._grid.axes[i] for i in mine)}"
|
|
950
|
+
)
|
|
951
|
+
vs, tr = [], []
|
|
952
|
+
for i, j in zip(sp, mine):
|
|
953
|
+
have, want = grid.units[i], self._grid.units[j]
|
|
954
|
+
f = 1.0 if have == want else U.factor(have, want)
|
|
955
|
+
vs.append(grid.voxel_size[i] * f)
|
|
956
|
+
tr.append(grid.translation[i] * f)
|
|
957
|
+
begin = (0,) * len(sp) if begin is None else tuple(int(b) for b in begin)
|
|
958
|
+
end = tuple(grid.shape[i] for i in sp) if end is None else tuple(int(e) for e in end)
|
|
959
|
+
if len(begin) != len(sp) or len(end) != len(sp):
|
|
960
|
+
raise ValueError(f"begin and end need one entry per spatial axis of the grid {names}")
|
|
961
|
+
return self.read(
|
|
962
|
+
Box.from_index(begin, end, vs, tr),
|
|
963
|
+
voxel_size=vs,
|
|
964
|
+
anchor=tuple(t - v / 2 for t, v in zip(tr, vs)),
|
|
965
|
+
padding=padding,
|
|
966
|
+
method=method,
|
|
967
|
+
mask=mask,
|
|
968
|
+
retries=retries,
|
|
969
|
+
transform=transform,
|
|
970
|
+
post_transform=post_transform,
|
|
971
|
+
select=select,
|
|
972
|
+
)
|
|
973
|
+
|
|
883
974
|
|
|
884
975
|
def _squeeze(out, drop: tuple[int, ...]):
|
|
885
976
|
if not drop:
|
|
@@ -1123,8 +1214,8 @@ def create(
|
|
|
1123
1214
|
0.5) following the group's format (Behaviours 11 and 12). ``chunks`` is required.
|
|
1124
1215
|
Nothing is ever deleted or overwritten: with ``exists="error"`` anything at ``path``
|
|
1125
1216
|
raises ``FileExistsError``; with ``"adopt"`` an identical array (shape, dtype, chunks,
|
|
1126
|
-
shards, placement
|
|
1127
|
-
naming every difference. Returns a writable ``WorldArray``."""
|
|
1217
|
+
shards, placement, and every key of ``attrs`` already holding the value given) is opened
|
|
1218
|
+
for writing and a different one raises ``ValueError`` naming every difference. Returns a writable ``WorldArray``."""
|
|
1128
1219
|
if grid is not None:
|
|
1129
1220
|
# a Grid planned the output (Behaviour 18): its facts are the array's, chunks and
|
|
1130
1221
|
# shards overridable; the placement arguments must not also be given
|
|
@@ -1165,6 +1256,7 @@ def create(
|
|
|
1165
1256
|
shards=shards,
|
|
1166
1257
|
voxel_size=vs,
|
|
1167
1258
|
translation=tr,
|
|
1259
|
+
attrs=attrs,
|
|
1168
1260
|
)
|
|
1169
1261
|
if diffs:
|
|
1170
1262
|
raise ValueError(
|
|
@@ -803,9 +803,12 @@ def differences(
|
|
|
803
803
|
shards: Sequence[int] | None,
|
|
804
804
|
voxel_size: Sequence[float],
|
|
805
805
|
translation: Sequence[float],
|
|
806
|
+
attrs: dict | None = None,
|
|
806
807
|
) -> list[str]:
|
|
807
808
|
"""How an existing array's ``ArrayMeta`` differs from what a caller wanted to create:
|
|
808
|
-
one line per field, empty when identical (placement within ``RTOL``).
|
|
809
|
+
one line per field, empty when identical (placement within ``RTOL``). Of the array's
|
|
810
|
+
attributes only the keys of ``attrs`` are compared, as JSON stores them; every other key
|
|
811
|
+
is someone else's."""
|
|
809
812
|
out = []
|
|
810
813
|
if tuple(meta.shape) != tuple(int(n) for n in shape):
|
|
811
814
|
out.append(f"shape: found {tuple(meta.shape)}, wanted {tuple(shape)}")
|
|
@@ -826,4 +829,10 @@ def differences(
|
|
|
826
829
|
out.append(
|
|
827
830
|
f"{what}: found {tuple(float(x) for x in f)}, wanted {tuple(float(x) for x in w)}"
|
|
828
831
|
)
|
|
832
|
+
found_attrs = meta.attrs or {}
|
|
833
|
+
for key, wanted in json.loads(json.dumps(attrs or {})).items():
|
|
834
|
+
if key not in found_attrs:
|
|
835
|
+
out.append(f"attrs[{key!r}]: missing, wanted {wanted!r}")
|
|
836
|
+
elif found_attrs[key] != wanted:
|
|
837
|
+
out.append(f"attrs[{key!r}]: found {found_attrs[key]!r}, wanted {wanted!r}")
|
|
829
838
|
return out
|
|
@@ -554,6 +554,12 @@ def test_relabel_rescales_everything_by_the_ratio(tmp_path):
|
|
|
554
554
|
resampled = wa.open(str(group / "s0"), voxel_size=(8, 8, 8), on_mismatch="resample")
|
|
555
555
|
assert resampled.source_voxel_size == (4.0, 4.0, 4.0)
|
|
556
556
|
assert resampled.voxel_size == (8.0, 8.0, 8.0)
|
|
557
|
+
for v in (
|
|
558
|
+
vol,
|
|
559
|
+
resampled,
|
|
560
|
+
): # where the level is, beside what it is (cellmap-flow's stored_placement)
|
|
561
|
+
assert v.source_translation == (10.0, 10.0, 10.0)
|
|
562
|
+
assert wa.open(str(group / "s0"), units="micrometer").source_translation == (0.01, 0.01, 0.01)
|
|
557
563
|
|
|
558
564
|
|
|
559
565
|
def test_a_resampled_box_is_the_target_voxels_shape_counts(tmp_path):
|
|
@@ -139,3 +139,45 @@ def test_partial_block_mean_and_edge_fill(tmp_path):
|
|
|
139
139
|
((0, 0, 0), (48, 16, 16)), voxel_size=(8, 8, 8), method="nearest", padding="edge"
|
|
140
140
|
)
|
|
141
141
|
assert list(near[:, 0, 0]) == pytest.approx([1, 3, 5, 6, 6, 6])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_read_onto_reads_another_grids_voxels(tmp_path):
|
|
145
|
+
"""cellmap-analyze's per-operation grid (Behaviour 20): voxels [begin, end) of a grid,
|
|
146
|
+
exactly that many, from any array; at the array's own voxel size nothing is resampled,
|
|
147
|
+
however far under a voxel the grids are offset."""
|
|
148
|
+
path, data = _ramp(tmp_path, "float32")
|
|
149
|
+
vol = wa.open(path, method="linear")
|
|
150
|
+
near = wa.Grid((8, 4, 4), (4, 4, 4), (3, 3, 3)) # corner 1 nm past the array's
|
|
151
|
+
np.testing.assert_array_equal(vol.read_onto(near), data) # stored values, not interpolated
|
|
152
|
+
far = wa.Grid(
|
|
153
|
+
(8, 4, 4), (4, 4, 4), (5, 2, 2)
|
|
154
|
+
) # 3 nm past on z: each grid voxel holds the next centre
|
|
155
|
+
out = vol.read_onto(far)
|
|
156
|
+
assert out.shape == (8, 4, 4)
|
|
157
|
+
np.testing.assert_array_equal(out[:7], data[1:])
|
|
158
|
+
assert np.all(out[7] == 0) # the last grid voxel holds no stored centre: the fill
|
|
159
|
+
finer = wa.Grid((16, 8, 8), (2, 2, 2), (-1, -1, -1), chunks=(4, 4, 4)) # corner -2
|
|
160
|
+
expect = vol.read(finer.box, voxel_size=(2, 2, 2), anchor=finer.corner)
|
|
161
|
+
np.testing.assert_array_equal(vol.read_onto(finer), expect)
|
|
162
|
+
halves = np.concatenate(
|
|
163
|
+
[vol.read_onto(finer, (0, 0, 0), (6, 8, 8)), vol.read_onto(finer, (6, 0, 0), (16, 8, 8))]
|
|
164
|
+
)
|
|
165
|
+
np.testing.assert_array_equal(halves, expect) # adjoining reads agree with one read
|
|
166
|
+
in_um = wa.Grid((16, 8, 8), (0.002,) * 3, (-0.001,) * 3, units="micrometer")
|
|
167
|
+
np.testing.assert_array_equal(vol.read_onto(in_um), expect)
|
|
168
|
+
# the grid's voxels -1 and 0 are centred at -3 and -1 nm, before the data: fill, masked
|
|
169
|
+
out, inside = vol.read_onto(finer, (-1, 1, 1), (3, 3, 3), mask=True)
|
|
170
|
+
assert out.shape == (4, 2, 2) and not inside[:2].any() and inside[2:].all()
|
|
171
|
+
with pytest.raises(ValueError, match="spatial axes"):
|
|
172
|
+
vol.read_onto(wa.Grid((4, 4), (4, 4), (2, 2), axes=("y", "x")))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_read_onto_its_own_grid_is_the_stored_array(tmp_path):
|
|
176
|
+
data = np.arange(7 * 6 * 5, dtype="uint16").reshape(7, 6, 5)
|
|
177
|
+
group = tmp_path / "odd.zarr"
|
|
178
|
+
write_zarr2_group(group, ome_multiscales("zyx", [("s0", (3.54, 4, 5.3), (1.0, -2.3, 7.7))]))
|
|
179
|
+
write_zarr2(group / "s0", data, chunks=(3, 3, 3))
|
|
180
|
+
vol = wa.open(str(group / "s0"))
|
|
181
|
+
np.testing.assert_array_equal(vol.read_onto(wa.Grid.from_meta(vol.grid)), data)
|
|
182
|
+
twice = wa.Grid.from_meta(vol.grid, voxel_size=(1.77, 2, 2.65))
|
|
183
|
+
assert vol.read_onto(twice).shape == twice.shape == (14, 12, 10)
|
|
@@ -272,6 +272,19 @@ def test_create_never_overwrites_and_adopts_only_an_identical_array(tmp_path):
|
|
|
272
272
|
wa.create(path, *args[:3], (3, 2, 2), chunks=(4, 4, 4), exists="adopt")
|
|
273
273
|
|
|
274
274
|
|
|
275
|
+
def test_adopt_checks_the_attributes_it_was_given(tmp_path):
|
|
276
|
+
"""cellmap-flow's _refuse_another_region: funlib's prepare_ds kept an old offset silently."""
|
|
277
|
+
path = str(tmp_path / "vol.zarr/s0")
|
|
278
|
+
args = ((8, 8, 8), "uint8", (4, 4, 4), (2, 2, 2))
|
|
279
|
+
wa.create(path, *args, chunks=(4, 4, 4), attrs={"offset": (0, 0, 0), "model": "a"})
|
|
280
|
+
again = wa.create(path, *args, chunks=(4, 4, 4), attrs={"offset": [0.0, 0, 0]}, exists="adopt")
|
|
281
|
+
assert again.meta.attrs["model"] == "a" # keys not given are not compared
|
|
282
|
+
with pytest.raises(ValueError) as e:
|
|
283
|
+
wa.create(path, *args, chunks=(4, 4, 4), attrs={"model": "b", "run": 2}, exists="adopt")
|
|
284
|
+
assert "attrs['model']: found 'a', wanted 'b'" in str(e.value)
|
|
285
|
+
assert "attrs['run']: missing, wanted 2" in str(e.value)
|
|
286
|
+
|
|
287
|
+
|
|
275
288
|
def test_create_keeps_everyone_elses_attributes(tmp_path):
|
|
276
289
|
group = tmp_path / "shared.zarr"
|
|
277
290
|
write_zarr3_group(
|
|
@@ -720,6 +733,44 @@ def test_ensure_group_entry_lists_an_array_the_package_did_not_create(tmp_path,
|
|
|
720
733
|
)
|
|
721
734
|
|
|
722
735
|
|
|
736
|
+
def test_a_pickled_relative_path_reopens_after_a_chdir(tmp_path, monkeypatch):
|
|
737
|
+
"""cellmap-analyze's dask workers change directory before they unpickle."""
|
|
738
|
+
monkeypatch.chdir(tmp_path)
|
|
739
|
+
wa.create("b.zarr/seg/s0", (8, 8, 8), "uint8", (4, 4, 4), (2, 2, 2), chunks=(4, 4, 4))
|
|
740
|
+
vol = wa.open("b.zarr/seg/s0", mode="r+")
|
|
741
|
+
vol.write_index((0, 0, 0), (4, 4, 4), np.full((4, 4, 4), 9, np.uint8))
|
|
742
|
+
blob = pickle.dumps(vol)
|
|
743
|
+
(tmp_path / "elsewhere").mkdir()
|
|
744
|
+
monkeypatch.chdir(tmp_path / "elsewhere")
|
|
745
|
+
again = pickle.loads(blob)
|
|
746
|
+
assert again.path == str(tmp_path / "b.zarr/seg/s0")
|
|
747
|
+
assert int(again.read_index((0, 0, 0), (1, 1, 1))[0, 0, 0]) == 9
|
|
748
|
+
assert vol.reopen().level_path == again.level_path
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def test_a_scalar_fills_the_box(tmp_path):
|
|
752
|
+
vol = wa.create(
|
|
753
|
+
str(tmp_path / "vol.zarr/s0"), (8, 8, 8), "uint8", (1, 1, 1), (0.5,) * 3, chunks=(4, 4, 4)
|
|
754
|
+
)
|
|
755
|
+
vol.write_index((0, 0, 0), (4, 8, 8), 7)
|
|
756
|
+
vol.write(((4, 0, 0), (8, 4, 4)), np.uint8(3))
|
|
757
|
+
vol.write_chunk((1, 1, 1), 5)
|
|
758
|
+
got = vol.read_index((0, 0, 0), (8, 8, 8))
|
|
759
|
+
assert np.all(got[:4] == 7) and np.all(got[4:, :4, :4] == 3) and np.all(got[4:, 4:, 4:] == 5)
|
|
760
|
+
for bad in (300, -1, 0.5, float("nan"), True + 255):
|
|
761
|
+
with pytest.raises(ValueError, match="does not fit uint8"):
|
|
762
|
+
vol.write_index((0, 0, 0), (4, 4, 4), bad)
|
|
763
|
+
vol.write_index((0, 0, 0), (4, 4, 4), 0) # the fill over a whole unit leaves it absent
|
|
764
|
+
assert (0, 0, 0) not in vol.list_chunks() and (0, 1, 0) in vol.list_chunks()
|
|
765
|
+
f = wa.create(
|
|
766
|
+
str(tmp_path / "f.zarr/s0"), (4,), "float32", (1,), (0.5,), axes=("x",), chunks=(4,)
|
|
767
|
+
)
|
|
768
|
+
f.write_index((0,), (4,), 0.1) # rounded to the array's precision
|
|
769
|
+
assert np.all(f.read_index((0,), (4,)) == np.float32(0.1))
|
|
770
|
+
with pytest.raises(ValueError, match="does not fit float32"):
|
|
771
|
+
f.write_index((0,), (4,), 1e300)
|
|
772
|
+
|
|
773
|
+
|
|
723
774
|
def test_a_pickled_handle_keeps_its_context(tmp_path):
|
|
724
775
|
path = str(tmp_path / "vol.zarr/s0")
|
|
725
776
|
wa.create(path, (8, 8, 8), "uint16", (4, 4, 4), (2, 2, 2), chunks=(4, 4, 4))
|
|
@@ -1247,7 +1247,7 @@ wheels = [
|
|
|
1247
1247
|
|
|
1248
1248
|
[[package]]
|
|
1249
1249
|
name = "worldarray"
|
|
1250
|
-
version = "0.
|
|
1250
|
+
version = "0.4.0"
|
|
1251
1251
|
source = { editable = "." }
|
|
1252
1252
|
dependencies = [
|
|
1253
1253
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|