worldarray 0.1.0__tar.gz → 0.1.1__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.1.1/CHANGELOG.md +35 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/PKG-INFO +3 -5
- {worldarray-0.1.0 → worldarray-0.1.1}/README.md +2 -4
- {worldarray-0.1.0 → worldarray-0.1.1}/docs/spec.md +39 -22
- {worldarray-0.1.0 → worldarray-0.1.1}/pyproject.toml +1 -1
- {worldarray-0.1.0 → worldarray-0.1.1}/src/worldarray/__init__.py +1 -1
- {worldarray-0.1.0 → worldarray-0.1.1}/src/worldarray/array.py +16 -9
- {worldarray-0.1.0 → worldarray-0.1.1}/src/worldarray/levels.py +12 -4
- {worldarray-0.1.0 → worldarray-0.1.1}/src/worldarray/placement.py +40 -15
- {worldarray-0.1.0 → worldarray-0.1.1}/src/worldarray/store.py +16 -2
- {worldarray-0.1.0 → worldarray-0.1.1}/tests/fixtures/metadata.json +8 -8
- {worldarray-0.1.0 → worldarray-0.1.1}/tests/test_access.py +16 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/tests/test_metadata.py +33 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/tests/test_read.py +49 -8
- {worldarray-0.1.0 → worldarray-0.1.1}/uv.lock +1 -1
- worldarray-0.1.0/CHANGELOG.md +0 -13
- {worldarray-0.1.0 → worldarray-0.1.1}/.github/workflows/ci.yml +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/.github/workflows/docs.yml +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/.github/workflows/publish.yml +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/.gitignore +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/LICENSE +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/docs/changelog.md +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/docs/index.md +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/mkdocs.yml +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/src/worldarray/context.py +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/src/worldarray/geometry.py +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/src/worldarray/resample.py +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/src/worldarray/retry.py +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/src/worldarray/units.py +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/tests/conftest.py +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/tests/test_geometry.py +0 -0
- {worldarray-0.1.0 → worldarray-0.1.1}/tests/test_resample.py +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
- funlib `voxel_size`/`offset` on the parent group place an array that carries none, for
|
|
6
|
+
zarr and N5 alike; funlib's keys on an N5 array are read in the array's C order, as
|
|
7
|
+
zarr-python's N5 store writes them (both from mesh-n-bone's review of its PR #48).
|
|
8
|
+
- A precomputed path that names a scale is that scale under `level=0` or `"first"` too;
|
|
9
|
+
other levels warn and are ignored, as on an array path.
|
|
10
|
+
- The GCE metadata guard is applied only off Google Cloud (decided from the DMI product
|
|
11
|
+
name and the `GCE_METADATA_*` variables), so metadata-server credentials keep working on
|
|
12
|
+
a Google VM.
|
|
13
|
+
- `Pyramid.placements()`, `voxel_sizes()` and `translations()` read the group document when
|
|
14
|
+
the pyramid was opened through one of its level paths too; they used to open every level
|
|
15
|
+
from there, and a missing level made them raise.
|
|
16
|
+
- **Placement change.** An N5 level with `downsamplingFactors` and no offset now sits with
|
|
17
|
+
its corner at the origin at every level, as Neuroglancer draws N5: translation
|
|
18
|
+
`f / 2 * base`. 0.1.0 followed BigDataViewer for `pixelResolution` pyramids, centring the
|
|
19
|
+
base's voxel 0 on the origin (`(f - 1) / 2 * base`, half a base voxel lower), while
|
|
20
|
+
placing `resolution` pyramids with the corner at the origin, so the two disagreed. Two
|
|
21
|
+
shared fixture cases move and are renamed: `n5_group_pixelresolution_and_downsampling_factors_corner_at_origin`
|
|
22
|
+
(translation 4, 4, 4) and `n5_bigstitcher_resolution_with_downsampling_factors_corner_at_origin`
|
|
23
|
+
(0.5, 0.157, 0.157).
|
|
24
|
+
|
|
25
|
+
## 0.1.0 (2026-10-21)
|
|
26
|
+
|
|
27
|
+
- First version, built to `docs/spec.md`: open zarr v2, zarr v3, N5 and Neuroglancer
|
|
28
|
+
precomputed arrays and pyramids through tensorstore; one metadata precedence per format
|
|
29
|
+
with the voxel-centre rule for translations; index-space and world-box reads with
|
|
30
|
+
padding; level selection; nearest and linear resampling; retries; shared and capped
|
|
31
|
+
tensorstore contexts.
|
|
32
|
+
Built and trialled in one day against the readers it replaces in cellmap-flow,
|
|
33
|
+
cellmap-analyze, mesh-n-bone and chunkmirage, and checked on their own data by the
|
|
34
|
+
CineMap, liconn-err, registration-station and salamander repos; see `docs/spec.md`,
|
|
35
|
+
"Trial status". Hot-path reads cost 0.1 to 0.2 ms over a raw tensorstore read.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: worldarray
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
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/
|
|
@@ -67,8 +67,7 @@ pip install worldarray
|
|
|
67
67
|
|
|
68
68
|
## Why
|
|
69
69
|
|
|
70
|
-
Volumetric data
|
|
71
|
-
stores, on local disks, S3, GCS and HTTP. Each format records *where the array sits*,
|
|
70
|
+
Volumetric data lives in zarr v2, zarr v3, N5 and Neuroglancer precomputed stores, on local disks, S3, GCS and HTTP. Each format records *where the array sits*,
|
|
72
71
|
its voxel size, origin, units and axes, in its own dialect, and the dialects disagree on
|
|
73
72
|
the one thing that matters most: whether a stored origin means the centre of the first
|
|
74
73
|
voxel or its corner. Get that wrong and every object is half a voxel out of place, which
|
|
@@ -79,8 +78,7 @@ rule for each, picking a pyramid level for a wanted resolution, and reading a re
|
|
|
79
78
|
given in physical units as a plain numpy block, padded where the region leaves the data,
|
|
80
79
|
resampled when no level matches, fast enough for a model server or a thousand cluster
|
|
81
80
|
workers, with retries for flaky object stores and thread caps for shared hosts. No one
|
|
82
|
-
package did all of that, so every tool
|
|
83
|
-
apart. worldarray is that reader, written once, with the rules stated and tested.
|
|
81
|
+
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.
|
|
84
82
|
|
|
85
83
|
Its dependencies are `tensorstore` and `numpy`. It is read-only in this version. The
|
|
86
84
|
[specification](https://janeliascicomp.github.io/worldarray/spec/) is the contract it is
|
|
@@ -8,8 +8,7 @@ pip install worldarray
|
|
|
8
8
|
|
|
9
9
|
## Why
|
|
10
10
|
|
|
11
|
-
Volumetric data
|
|
12
|
-
stores, on local disks, S3, GCS and HTTP. Each format records *where the array sits*,
|
|
11
|
+
Volumetric data lives in zarr v2, zarr v3, N5 and Neuroglancer precomputed stores, on local disks, S3, GCS and HTTP. Each format records *where the array sits*,
|
|
13
12
|
its voxel size, origin, units and axes, in its own dialect, and the dialects disagree on
|
|
14
13
|
the one thing that matters most: whether a stored origin means the centre of the first
|
|
15
14
|
voxel or its corner. Get that wrong and every object is half a voxel out of place, which
|
|
@@ -20,8 +19,7 @@ rule for each, picking a pyramid level for a wanted resolution, and reading a re
|
|
|
20
19
|
given in physical units as a plain numpy block, padded where the region leaves the data,
|
|
21
20
|
resampled when no level matches, fast enough for a model server or a thousand cluster
|
|
22
21
|
workers, with retries for flaky object stores and thread caps for shared hosts. No one
|
|
23
|
-
package did all of that, so every tool
|
|
24
|
-
apart. worldarray is that reader, written once, with the rules stated and tested.
|
|
22
|
+
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.
|
|
25
23
|
|
|
26
24
|
Its dependencies are `tensorstore` and `numpy`. It is read-only in this version. The
|
|
27
25
|
[specification](https://janeliascicomp.github.io/worldarray/spec/) is the contract it is
|
|
@@ -144,7 +144,8 @@ pyr.metas() ; pyr.level_meta("s1") # ArrayMeta per level from the documents al
|
|
|
144
144
|
# array's inner chunk), fill and placement, with no tensorstore opened
|
|
145
145
|
pyr.placements() # (name, spatial voxel size, spatial translation) per level from the group's own document
|
|
146
146
|
pyr.voxel_sizes() ; pyr.translations() # where it lists the levels (OME-NGFF, COSEM N5, precomputed), so
|
|
147
|
-
# choosing or placing a level opens nothing
|
|
147
|
+
# choosing or placing a level opens nothing, whether the pyramid was opened by its
|
|
148
|
+
# group path or by one of its level paths; else by opening each level
|
|
148
149
|
|
|
149
150
|
wa.metadata(path, level=None, units=None, open=True) -> ArrayMeta # open=False: from the documents alone
|
|
150
151
|
wa.detect(path, missing_ok=False) -> Node | None # format, kind (array|group), path, docs, scale_key; .attrs (raw user
|
|
@@ -215,10 +216,15 @@ this document and that one differ, fix one of them.
|
|
|
215
216
|
CF coordinates. Index `i` on an axis sits at `translation + i * voxel_size`; voxel `i`
|
|
216
217
|
covers `[translation + (i - 1/2) * voxel_size, translation + (i + 1/2) * voxel_size)`.
|
|
217
218
|
Formats that store voxel 0's **corner** are moved half a voxel along the spatial axes
|
|
218
|
-
on reading. N5
|
|
219
|
-
|
|
220
|
-
`f*i .. f*i+f-1
|
|
221
|
-
|
|
219
|
+
on reading. An N5 level with `downsamplingFactors` and no offset sits as Neuroglancer
|
|
220
|
+
draws it, a pure scale of the base grid with every level's corner at the origin: level-L
|
|
221
|
+
voxel `i` covers full-resolution voxels `f*i .. f*i+f-1`, so its centre is `f*i + f/2`
|
|
222
|
+
full-resolution voxels from the origin and its translation `f / 2 * base`. BigDataViewer
|
|
223
|
+
centres voxel 0 on the origin instead, half a base voxel lower at every level; the shift
|
|
224
|
+
between levels, `(f - 1) / 2` base voxels, is the same in both and is the imglib2 rule
|
|
225
|
+
liconn-err and registration-station use. (Decided 2026-10-22, when mesh-n-bone found
|
|
226
|
+
that `pixelResolution` and `resolution` pyramids disagreed; 0.1.0 followed BigDataViewer
|
|
227
|
+
for `pixelResolution`.) The shift
|
|
222
228
|
applies only along axes named `z`, `y` or `x`, or the last three when axes are unnamed;
|
|
223
229
|
never along channel or time.
|
|
224
230
|
|
|
@@ -227,7 +233,7 @@ this document and that one differ, fix one of them.
|
|
|
227
233
|
| OME-NGFF `translation` (zarr v2/v3) | centre of voxel 0 | as is |
|
|
228
234
|
| COSEM `transform.translate` (N5 or zarr) | centre (fibsem-tools wrote it as the first sample's position; Neuroglancer ignores `transform` and `offset` on N5 entirely) | as is |
|
|
229
235
|
| funlib `offset` with `resolution`/`voxel_size` (N5 or zarr; `offset` missing = 0) | corner | `offset + voxel_size / 2` |
|
|
230
|
-
| N5 `pixelResolution` x `downsamplingFactors f`, no offset |
|
|
236
|
+
| N5 `pixelResolution` or `resolution` x `downsamplingFactors f`, no offset | corner at the origin at every level, as Neuroglancer draws N5 | `f / 2 * pixelResolution` |
|
|
231
237
|
| precomputed `voxel_offset` x `resolution` | corner | `+ resolution / 2` |
|
|
232
238
|
| CF coordinate array (xarray zarr) | sample position = centre | as is (chunkmirage layers this) |
|
|
233
239
|
| nothing at all | Neuroglancer draws a plain array with its corner at the origin | voxels of 1, translation 0.5 |
|
|
@@ -284,16 +290,19 @@ this document and that one differ, fix one of them.
|
|
|
284
290
|
`sequence`; ignore the multiscale-level transforms (they lead to other systems, which
|
|
285
291
|
chunkmirage's `scene://` applies itself) and any other transform type. Else the array's
|
|
286
292
|
own `resolution` / `voxel_size`, `offset`, `units`, `axis_names` (funlib) or `transform`
|
|
287
|
-
(COSEM), C order; else
|
|
293
|
+
(COSEM), C order; else the same funlib keys on the parent group when the array carries
|
|
294
|
+
none (mesh-n-bone's inputs); else tensorstore's `dimension_units` attribute (zarr v3). Axes from
|
|
288
295
|
OME `axes`, `dimension_names` (zarr v3), `_ARRAY_DIMENSIONS` (xarray) or `axis_names`.
|
|
289
296
|
- N5: the array's `transform` (COSEM; C order unless `ordering: "F"`); else the parent's
|
|
290
|
-
`multiscales[].datasets[].transform` for this path; else `
|
|
297
|
+
`multiscales[].datasets[].transform` for this path; else funlib's `voxel_size` and
|
|
298
|
+
`offset` (array, else group), which zarr-python's N5 store leaves in the array's C
|
|
299
|
+
order, read by the zarr funlib rule above; else `pixelResolution` (both the
|
|
291
300
|
`{dimensions, unit}` form and a bare list) or `resolution`, from the array else the
|
|
292
301
|
group, times the level's `downsamplingFactors`; plus `offset`; axes from the `axes`
|
|
293
|
-
attribute. These lists are x-first. Without an `offset
|
|
294
|
-
|
|
295
|
-
`downsamplingFactors
|
|
296
|
-
|
|
302
|
+
attribute. These lists are x-first. Without an `offset` the corner is at the origin at
|
|
303
|
+
every level whichever key gave the base (BigStitcher writes `resolution` and
|
|
304
|
+
`downsamplingFactors`; funlib writes `resolution` alone): translation `f / 2 * base`,
|
|
305
|
+
half a voxel of the level (Convention 1).
|
|
297
306
|
- **Short lists.** A funlib, COSEM or N5 per-axis list shorter than the array (a 3-entry
|
|
298
307
|
`voxel_size` on a `(c, z, y, x)` prediction, which funlib's `prepare_ds(num_channels=)`
|
|
299
308
|
writes) covers the trailing axes; the leading axes get voxel size 1, translation 0,
|
|
@@ -326,8 +335,11 @@ this document and that one differ, fix one of them.
|
|
|
326
335
|
that answers anything but a document (a server root's 301) ends the search for a
|
|
327
336
|
multiscales entry quietly. Off Google Cloud, `GCE_METADATA_ROOT` is pointed at a name that
|
|
328
337
|
fails at once before the first `gs://` open, because tensorstore's probe of the GCE
|
|
329
|
-
metadata server otherwise stalls every open for seconds
|
|
330
|
-
|
|
338
|
+
metadata server otherwise stalls every open for seconds; "off Google Cloud" is decided
|
|
339
|
+
with no network call, from the machine's DMI product name and the `GCE_METADATA_HOST` and
|
|
340
|
+
`GCE_METADATA_ROOT` variables, and on a Google machine the variable is left alone so the
|
|
341
|
+
credentials the real metadata server issues keep working (`store.GUARD_GCE_METADATA`
|
|
342
|
+
turns the guard off). A `precomputed://` prefix is accepted and
|
|
331
343
|
dropped. The `.n5` or `.zarr` suffix is never used to decide the format. A zarr v2
|
|
332
344
|
compressor carrying members tensorstore rejects (the `checksum` newer numcodecs writes
|
|
333
345
|
for zstd) is opened without them. Access: a path with no scheme is a local file; `s3://`
|
|
@@ -380,13 +392,14 @@ this document and that one differ, fix one of them.
|
|
|
380
392
|
3. **Level selection** (`select_level`, `Pyramid.select`, `open(voxel_size=)`) follows
|
|
381
393
|
cellmap-flow's four modes plus one: `floor` (the level at that voxel size, else the
|
|
382
394
|
**coarsest** level that is no coarser than the target on any axis, else the first),
|
|
383
|
-
`finer` (as `floor`, but an error instead of the fallback to the first level,
|
|
384
|
-
|
|
385
|
-
|
|
395
|
+
`finer` (as `floor`, but an error instead of the fallback to the first level, for
|
|
396
|
+
callers that would rather fail than read coarser data than asked), `exact` (that level or
|
|
397
|
+
`ValueError` listing the levels), `nearest` (closest on a log scale summed over
|
|
386
398
|
axes, finer on a tie), `resample` (the level to resample from). Voxel sizes compare as
|
|
387
399
|
floats with `rtol 1e-6`, so 10.48 is not 10. A group path without `level` or
|
|
388
400
|
`voxel_size` raises with the level list; a level name that does not exist raises with
|
|
389
|
-
the list; an array path
|
|
401
|
+
the list; an array path, or a precomputed path naming a scale, is that array or scale
|
|
402
|
+
under `level=0` or `"first"` silently and warns and ignores any other `level`. When no level has the
|
|
390
403
|
requested `voxel_size`, `on_mismatch` decides: `"error"` raises with the list;
|
|
391
404
|
`"resample"` reads the level `level_mode` picks and resamples (Behaviour 4); `"relabel"`
|
|
392
405
|
reads that level voxel for voxel and reports `voxel_size` as the requested size with
|
|
@@ -487,7 +500,7 @@ chunkmirage derives from raw attributes carries a `layered` note beside the pack
|
|
|
487
500
|
|
|
488
501
|
- chunkmirage's cases: N5 transposed to C order with `resolution` and `offset`; N5 COSEM
|
|
489
502
|
`transform` C order; N5 group `pixelResolution` and `downsamplingFactors`
|
|
490
|
-
(
|
|
503
|
+
(the corner at the origin at every level, as Neuroglancer); N5 group `multiscales` transform matched by path; OME levels matched by
|
|
491
504
|
path; OME top-level transforms applied after each dataset; an OME-unlisted array borrows
|
|
492
505
|
no other level's scale; zarr legacy array attrs as a corner; a zarr3 OME 0.5 group with a
|
|
493
506
|
channel axis; precomputed `voxel_offset` as a centre translation; xarray dimensions and
|
|
@@ -510,7 +523,11 @@ chunkmirage derives from raw attributes carries a `layered` note beside the pack
|
|
|
510
523
|
fallback; detection ignores the directory suffix and finds a precomputed scale key.
|
|
511
524
|
|
|
512
525
|
Reads: every statement in Behaviour 1, 2, 3 and 9; a document-less group probed through
|
|
513
|
-
`s0`, integer and `"first"` levels, `detect(missing_ok=True)`, `index_box`;
|
|
526
|
+
`s0`, integer and `"first"` levels, `detect(missing_ok=True)`, `index_box`; funlib keys on
|
|
527
|
+
the parent group of an attribute-less zarr and N5 array, and funlib `voxel_size`/`offset` on
|
|
528
|
+
an N5 array in C order; a precomputed path naming a scale kept under `level="first"`; the
|
|
529
|
+
GCE guard left alone on a Google machine; a pyramid opened through one of its level paths
|
|
530
|
+
placing every level from the group document while one level's array is missing; `read_index`
|
|
514
531
|
with `select`; `base_voxel_size` rescaling a BigStitcher-style pyramid; `AWS_ENDPOINT_URL`,
|
|
515
532
|
`AWS_ENDPOINT_URL_S3` and the region reaching the S3 specs and the GCS redirect keying the
|
|
516
533
|
kvstore cache; an array path kept under `voxel_size`; relabel keeping the corner; a
|
|
@@ -531,8 +548,8 @@ the data; a resample target set at open reports the target voxel size with the s
|
|
|
531
548
|
Placement: the same cube stored four ways, as OME-NGFF with a `translation`, as N5 with
|
|
532
549
|
funlib `resolution`/`offset`, as N5 with a COSEM `transform`, and as precomputed with a
|
|
533
550
|
`voxel_offset`, reads back with the same `translation` and the same bytes for the same
|
|
534
|
-
world box, within `0.1 * voxel_size`; and an N5
|
|
535
|
-
equivalent OME level 1 does.
|
|
551
|
+
world box, within `0.1 * voxel_size`; and an N5 level 1 with `downsamplingFactors` sits where the
|
|
552
|
+
equivalent OME level 1 does, both pyramids' s0 corner at the origin.
|
|
536
553
|
|
|
537
554
|
Network (skipped without it; not yet written): one public OpenOrganelle volume with a
|
|
538
555
|
non-integer z spacing (4.56 or 5.24 nm), read anonymously from s3: the package reports the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "worldarray"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.1"
|
|
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" }
|
|
@@ -26,7 +26,7 @@ from . import resample as _resample
|
|
|
26
26
|
from . import units as U
|
|
27
27
|
from .context import shared_context
|
|
28
28
|
from .geometry import Box, index_range, is_aligned
|
|
29
|
-
from .levels import LevelRef, Mode, discover, same_voxel_size, select_level
|
|
29
|
+
from .levels import LevelRef, Mode, discover, discover_group, same_voxel_size, select_level
|
|
30
30
|
from .placement import ArrayMeta, light_levels, read_meta, read_meta_light
|
|
31
31
|
from .retry import Retry, read_with_retries
|
|
32
32
|
from .store import Node, detect, is_remote, open_array, precomputed_scale_index
|
|
@@ -145,11 +145,17 @@ class WorldArray:
|
|
|
145
145
|
# --- construction helpers -------------------------------------------------
|
|
146
146
|
|
|
147
147
|
def _choose_level(self, node: Node, level, voxel_size, level_mode, on_mismatch) -> LevelRef:
|
|
148
|
-
if
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
if node.format == "precomputed" and node.scale_key is not None:
|
|
149
|
+
# a path naming a scale is that scale, as an array path is that array
|
|
150
|
+
if level not in (None, 0, "first"):
|
|
151
|
+
warnings.warn(
|
|
152
|
+
f"{self.path} names a precomputed scale; level={level!r} is ignored",
|
|
153
|
+
stacklevel=3,
|
|
154
|
+
)
|
|
151
155
|
index = precomputed_scale_index(node.docs["info"], node.scale_key)
|
|
152
156
|
return LevelRef(node.scale_key, node.path, index)
|
|
157
|
+
if level == "first":
|
|
158
|
+
level = 0
|
|
153
159
|
if node.kind == "array":
|
|
154
160
|
if level is not None and level != 0: # "first" (already 0) and 0 mean "this array"
|
|
155
161
|
warnings.warn(f"{self.path} is an array; level={level!r} is ignored", stacklevel=3)
|
|
@@ -743,7 +749,7 @@ class Pyramid:
|
|
|
743
749
|
self._select = select or {}
|
|
744
750
|
self._kwargs = kwargs
|
|
745
751
|
self._node = _node if _node is not None else detect(path)
|
|
746
|
-
self._refs =
|
|
752
|
+
self._group, self._refs = discover_group(self._node) # the node listing the levels
|
|
747
753
|
if not self._refs:
|
|
748
754
|
raise ValueError(
|
|
749
755
|
f"{path}: no levels found (no multiscales datasets, no s0, no precomputed scales)"
|
|
@@ -795,9 +801,8 @@ class Pyramid:
|
|
|
795
801
|
COSEM N5, precomputed), else by opening each level."""
|
|
796
802
|
if self._light is None:
|
|
797
803
|
space, _ = _unit_targets(self.units)
|
|
798
|
-
group
|
|
799
|
-
|
|
800
|
-
group = detect(self._refs[0].path.rsplit("/", 1)[0], missing_ok=True)
|
|
804
|
+
# the group the levels were discovered from, whichever path opened the pyramid
|
|
805
|
+
group = self._group
|
|
801
806
|
light = light_levels(group, self.names, space) if group is not None else None
|
|
802
807
|
if light is None:
|
|
803
808
|
light = [
|
|
@@ -867,7 +872,9 @@ def metadata(
|
|
|
867
872
|
if level == "first":
|
|
868
873
|
level = 0
|
|
869
874
|
if node.format == "precomputed" and (level is not None or node.scale_key is not None):
|
|
870
|
-
wanted =
|
|
875
|
+
wanted = (
|
|
876
|
+
node.scale_key if node.scale_key is not None else level
|
|
877
|
+
) # a scale path is that scale
|
|
871
878
|
meta = read_meta_light(node, scale_index=precomputed_scale_index(node.docs["info"], wanted))
|
|
872
879
|
elif node.kind == "group":
|
|
873
880
|
refs = discover(node)
|
|
@@ -48,8 +48,16 @@ def _probe_levels(path: str) -> list[str]:
|
|
|
48
48
|
def discover(node: Node) -> list[LevelRef]:
|
|
49
49
|
"""The levels of a group node, or of the group an array node belongs to (just the array
|
|
50
50
|
itself when its parent is not a multiscale group)."""
|
|
51
|
+
return discover_group(node)[1]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def discover_group(node: Node) -> tuple[Node | None, list[LevelRef]]:
|
|
55
|
+
"""``discover`` plus the node whose document lists those levels: the group itself, the
|
|
56
|
+
ancestor an array path belongs to, the precomputed ``info`` node; None for an array
|
|
57
|
+
standing alone, so a ``Pyramid`` opened through a level path still reads placements
|
|
58
|
+
from the group document instead of opening every level."""
|
|
51
59
|
if node.format == "precomputed":
|
|
52
|
-
return [
|
|
60
|
+
return node, [
|
|
53
61
|
LevelRef(str(s.get("key", i)), node.path, i)
|
|
54
62
|
for i, s in enumerate(node.docs["info"]["scales"])
|
|
55
63
|
]
|
|
@@ -67,14 +75,14 @@ def discover(node: Node) -> list[LevelRef]:
|
|
|
67
75
|
and group.kind == "group"
|
|
68
76
|
and rel in _group_multiscale_paths(group)
|
|
69
77
|
):
|
|
70
|
-
return
|
|
78
|
+
return discover_group(group)
|
|
71
79
|
if parent(ancestor) is None:
|
|
72
80
|
break
|
|
73
81
|
rel = f"{basename(ancestor)}/{rel}"
|
|
74
82
|
ancestor = parent(ancestor)
|
|
75
|
-
return [LevelRef(basename(node.path), node.path)]
|
|
83
|
+
return None, [LevelRef(basename(node.path), node.path)]
|
|
76
84
|
names = _group_multiscale_paths(node) or _probe_levels(node.path)
|
|
77
|
-
return [LevelRef(name, join(node.path, name)) for name in names]
|
|
85
|
+
return node, [LevelRef(name, join(node.path, name)) for name in names]
|
|
78
86
|
|
|
79
87
|
|
|
80
88
|
def _spatial_voxel_size(level) -> np.ndarray:
|
|
@@ -4,8 +4,8 @@ The rule everything else rests on: a **translation is the centre of voxel 0** (O
|
|
|
4
4
|
COSEM ``transform``, CF coordinates, Neuroglancer's own zarr reader). Formats that store
|
|
5
5
|
voxel 0's corner are moved half a voxel along the spatial axes on reading: precomputed's
|
|
6
6
|
``voxel_offset`` and funlib's ``offset`` (0 when only a ``resolution`` is given). N5 levels
|
|
7
|
-
with ``
|
|
8
|
-
|
|
7
|
+
with ``downsamplingFactors`` and no offset keep their corner at the origin at every level,
|
|
8
|
+
as Neuroglancer draws N5: translation ``f / 2 * base`` (spec, Conventions 1 and 4).
|
|
9
9
|
|
|
10
10
|
Per-axis lists shorter than the array (a 3-entry ``voxel_size`` on a ``(c, z, y, x)``
|
|
11
11
|
prediction) cover the trailing axes; the leading axes get 1.0, 0.0 or ``""``. OME lists
|
|
@@ -415,6 +415,11 @@ def _zarr(
|
|
|
415
415
|
found = _ome(hit[0], hit[1], ndim, node.path)
|
|
416
416
|
if found.voxel_size is None:
|
|
417
417
|
found = _array_attrs(attrs, ndim, node.path)
|
|
418
|
+
if found.voxel_size is None and _has_funlib_keys(group_attrs):
|
|
419
|
+
# funlib's keys on the parent group, the array carrying none (mesh-n-bone's inputs)
|
|
420
|
+
axes = found.axes
|
|
421
|
+
found = _array_attrs(_funlib_subset(group_attrs), ndim, node.path)
|
|
422
|
+
found.axes = found.axes or axes
|
|
418
423
|
else:
|
|
419
424
|
_check_against_attrs(attrs, found, ndim, node.path, conflicts)
|
|
420
425
|
if found.axes is None:
|
|
@@ -501,6 +506,21 @@ def _unpack_sequence(transforms: list[dict]) -> list[dict]:
|
|
|
501
506
|
return out
|
|
502
507
|
|
|
503
508
|
|
|
509
|
+
_FUNLIB_KEYS = ("voxel_size", "resolution", "offset", "units", "axis_names")
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _has_funlib_keys(attrs: dict) -> bool:
|
|
513
|
+
return any(
|
|
514
|
+
isinstance(attrs.get(k), (list, tuple)) for k in ("voxel_size", "resolution", "offset")
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _funlib_subset(attrs: dict) -> dict:
|
|
519
|
+
"""Only funlib's keys of a group's attributes, so a group-level ``transform`` or ``scale``
|
|
520
|
+
(which mean other things on a group) is not taken for the array's placement."""
|
|
521
|
+
return {k: attrs[k] for k in _FUNLIB_KEYS if k in attrs}
|
|
522
|
+
|
|
523
|
+
|
|
504
524
|
def _array_attrs(attrs: dict, ndim: int, path: str) -> _Resolved:
|
|
505
525
|
"""funlib (``resolution``/``voxel_size``, ``offset``, ``units``, ``axis_names``) or COSEM
|
|
506
526
|
(``transform``) attributes on the array itself, C order. An ``offset`` with no voxel
|
|
@@ -581,12 +601,12 @@ def _check_against_attrs(
|
|
|
581
601
|
|
|
582
602
|
def _n5(attrs: dict, group_attrs: dict, node: Node, ndim: int) -> _Resolved:
|
|
583
603
|
"""COSEM ``transform`` on the array; else the group's ``multiscales[].datasets[].transform``
|
|
584
|
-
for this path; else ``
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
604
|
+
for this path; else funlib's ``voxel_size``/``offset`` (array, else group), C order; else
|
|
605
|
+
``pixelResolution``/``resolution`` (array, else group) times the level's
|
|
606
|
+
``downsamplingFactors``, plus ``offset``. Lists other than ``transform`` and funlib's are
|
|
607
|
+
x-first and reversed to C order here. Without an ``offset`` the corner is at the origin
|
|
608
|
+
at every level, whichever key gave the base: translation ``f / 2 * base``, half a voxel
|
|
609
|
+
of the level, as Neuroglancer draws N5 (Convention 1)."""
|
|
590
610
|
name = basename(node.path)
|
|
591
611
|
transform = attrs.get("transform")
|
|
592
612
|
if isinstance(transform, dict) and "scale" in transform:
|
|
@@ -596,6 +616,13 @@ def _n5(attrs: dict, group_attrs: dict, node: Node, ndim: int) -> _Resolved:
|
|
|
596
616
|
return _cosem_transform(hit[1]["transform"], ndim, name)
|
|
597
617
|
|
|
598
618
|
axes = _axes(list(attrs.get("axes", group_attrs.get("axes", [])) or [])[::-1], ndim)
|
|
619
|
+
for source in (attrs, group_attrs):
|
|
620
|
+
if isinstance(source.get("voxel_size"), (list, tuple)):
|
|
621
|
+
# funlib's own key names, which zarr-python's N5 store leaves in the array's C
|
|
622
|
+
# order (unlike resolution and pixelResolution, x-first); offset is a corner
|
|
623
|
+
found = _array_attrs(_funlib_subset(source), ndim, name)
|
|
624
|
+
found.axes = found.axes or axes
|
|
625
|
+
return found
|
|
599
626
|
unit: str | None = None
|
|
600
627
|
base, key = None, None
|
|
601
628
|
for source in (attrs, group_attrs):
|
|
@@ -635,14 +662,12 @@ def _n5(attrs: dict, group_attrs: dict, node: Node, ndim: int) -> _Resolved:
|
|
|
635
662
|
|
|
636
663
|
if "offset" in attrs:
|
|
637
664
|
corner = _floats(list(attrs["offset"])[::-1], ndim, f"{name}: offset", pad=0.0)
|
|
638
|
-
translation = corner_to_centre(corner, voxel_size, axes)
|
|
639
|
-
elif key == "resolution" and "downsamplingFactors" not in attrs:
|
|
640
|
-
# funlib/daisy N5: a resolution with no offset is a corner at 0 (funlib never writes
|
|
641
|
-
# downsamplingFactors; BigStitcher writes "resolution" AND downsamplingFactors, and means BDV)
|
|
642
|
-
translation = corner_to_centre((0.0,) * ndim, voxel_size, axes)
|
|
643
665
|
else:
|
|
644
|
-
#
|
|
645
|
-
|
|
666
|
+
# no stored origin: the corner at the origin at every level, as Neuroglancer draws N5
|
|
667
|
+
# (a pure scale of the base grid by downsamplingFactors). BigDataViewer centres voxel
|
|
668
|
+
# 0 on the origin instead, half a base voxel lower; the shift between levels is the same
|
|
669
|
+
corner = (0.0,) * ndim
|
|
670
|
+
translation = corner_to_centre(corner, voxel_size, axes)
|
|
646
671
|
return _Resolved(axes, units, tuple(float(v) for v in voxel_size), tuple(translation), "n5")
|
|
647
672
|
|
|
648
673
|
|
|
@@ -88,6 +88,19 @@ def join(base: str, *parts: str) -> str:
|
|
|
88
88
|
# Access
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
@functools.lru_cache(maxsize=1)
|
|
92
|
+
def _on_google_cloud() -> bool:
|
|
93
|
+
"""A Google VM or GKE node, decided with no network call: the environment names a
|
|
94
|
+
metadata host, or the machine's DMI product name says Google."""
|
|
95
|
+
if os.environ.get("GCE_METADATA_HOST") or os.environ.get("GCE_METADATA_ROOT"):
|
|
96
|
+
return True
|
|
97
|
+
try:
|
|
98
|
+
with open("/sys/class/dmi/id/product_name") as f:
|
|
99
|
+
return f.read().strip().startswith("Google")
|
|
100
|
+
except OSError:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
|
|
91
104
|
def kvstore_specs(root: str) -> list[dict]:
|
|
92
105
|
"""The tensorstore kvstore specs that read the directory ``root``, in the order to try
|
|
93
106
|
them. ``s3://``: anonymously, then AWS's default credentials (S3 refuses even a public
|
|
@@ -109,9 +122,10 @@ def kvstore_specs(root: str) -> list[dict]:
|
|
|
109
122
|
extra["aws_region"] = region
|
|
110
123
|
return [{**spec, **extra, "aws_credentials": {"type": t}} for t in ("anonymous", "default")]
|
|
111
124
|
if spec["driver"] == "gcs":
|
|
112
|
-
if GUARD_GCE_METADATA:
|
|
125
|
+
if GUARD_GCE_METADATA and not _on_google_cloud():
|
|
113
126
|
# off Google Cloud, tensorstore's probe of the GCE metadata server for credentials
|
|
114
|
-
# stalls every gs:// open for seconds; point it at a name that fails at once
|
|
127
|
+
# stalls every gs:// open for seconds; point it at a name that fails at once. On a
|
|
128
|
+
# Google machine the server is real and the credentials it serves must keep working
|
|
115
129
|
os.environ.setdefault("GCE_METADATA_ROOT", "metadata.google.internal.invalid")
|
|
116
130
|
public = (os.environ.get("TENSORSTORE_GCS_HTTP_URL") or GCS_PUBLIC_URL).rstrip("/")
|
|
117
131
|
return [
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
}
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
|
-
"name": "
|
|
107
|
+
"name": "n5_group_pixelresolution_and_downsampling_factors_corner_at_origin",
|
|
108
108
|
"format": "n5",
|
|
109
109
|
"shape": [
|
|
110
110
|
4,
|
|
@@ -136,9 +136,9 @@
|
|
|
136
136
|
8
|
|
137
137
|
],
|
|
138
138
|
"translation": [
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
4,
|
|
140
|
+
4,
|
|
141
|
+
4
|
|
142
142
|
],
|
|
143
143
|
"axes": [
|
|
144
144
|
"z",
|
|
@@ -1289,7 +1289,7 @@
|
|
|
1289
1289
|
}
|
|
1290
1290
|
},
|
|
1291
1291
|
{
|
|
1292
|
-
"name": "
|
|
1292
|
+
"name": "n5_bigstitcher_resolution_with_downsampling_factors_corner_at_origin",
|
|
1293
1293
|
"format": "n5",
|
|
1294
1294
|
"shape": [
|
|
1295
1295
|
4,
|
|
@@ -1319,9 +1319,9 @@
|
|
|
1319
1319
|
0.314
|
|
1320
1320
|
],
|
|
1321
1321
|
"translation": [
|
|
1322
|
-
0,
|
|
1323
|
-
0.
|
|
1324
|
-
0.
|
|
1322
|
+
0.5,
|
|
1323
|
+
0.157,
|
|
1324
|
+
0.157
|
|
1325
1325
|
],
|
|
1326
1326
|
"axes": [
|
|
1327
1327
|
"z",
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
6
|
+
|
|
5
7
|
import numpy as np
|
|
6
8
|
import pytest
|
|
7
9
|
|
|
@@ -192,3 +194,17 @@ def test_capped_context_can_leave_the_network_alone():
|
|
|
192
194
|
spec = wa.capped_context_spec(concurrency=2, network=False)
|
|
193
195
|
assert "s3_request_concurrency" not in spec and spec["file_io_concurrency"] == {"limit": 2}
|
|
194
196
|
assert "s3_request_concurrency" in wa.capped_context_spec()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_gce_guard_is_left_alone_on_a_google_machine(monkeypatch):
|
|
200
|
+
"""Off Google Cloud the metadata probe is short-circuited; on a Google machine the
|
|
201
|
+
variable is left alone so the real metadata server's credentials keep working."""
|
|
202
|
+
monkeypatch.setenv("GCE_METADATA_ROOT", "remembered-for-restoration")
|
|
203
|
+
monkeypatch.delenv("GCE_METADATA_ROOT")
|
|
204
|
+
monkeypatch.delenv("GCE_METADATA_HOST", raising=False)
|
|
205
|
+
monkeypatch.setattr(store, "_on_google_cloud", lambda: True)
|
|
206
|
+
store.kvstore_specs("gs://bucket/vol")
|
|
207
|
+
assert "GCE_METADATA_ROOT" not in os.environ
|
|
208
|
+
monkeypatch.setattr(store, "_on_google_cloud", lambda: False)
|
|
209
|
+
store.kvstore_specs("gs://bucket/vol")
|
|
210
|
+
assert os.environ["GCE_METADATA_ROOT"] == "metadata.google.internal.invalid"
|
|
@@ -150,3 +150,36 @@ def test_lenient_units_take_unknown_units_as_the_target(tmp_path, caplog):
|
|
|
150
150
|
)
|
|
151
151
|
assert "nanometer" in wa.units.TABLE["length"] and "second" in wa.units.TABLE["time"]
|
|
152
152
|
assert wa.open(str(group / "s0"), units=None).meta.fill_value in (None, 0)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_parent_group_funlib_attrs_place_an_attribute_less_array(tmp_path):
|
|
156
|
+
"""funlib's keys on the parent group, the array carrying none, zarr and N5 alike
|
|
157
|
+
(mesh-n-bone's inputs): read as on the array, offset a corner."""
|
|
158
|
+
from conftest import write_n5, write_n5_group
|
|
159
|
+
|
|
160
|
+
for fmt in ("zarr2", "n5"):
|
|
161
|
+
root = tmp_path / f"vol.{fmt}"
|
|
162
|
+
if fmt == "zarr2":
|
|
163
|
+
write_zarr2_group(root, {"voxel_size": [1, 2, 3], "offset": [4, 4, 4]})
|
|
164
|
+
write_zarr2(root / "s0", pattern((4, 4, 4)))
|
|
165
|
+
else:
|
|
166
|
+
write_n5_group(root, {"voxel_size": [1, 2, 3], "offset": [4, 4, 4]})
|
|
167
|
+
write_n5(root / "s0", pattern((4, 4, 4)))
|
|
168
|
+
meta = wa.metadata(str(root / "s0"))
|
|
169
|
+
assert meta.voxel_size == (1.0, 2.0, 3.0), fmt
|
|
170
|
+
assert meta.translation == pytest.approx((4.5, 5.0, 5.5)), fmt
|
|
171
|
+
assert meta.placed_by == "funlib", fmt
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_n5_array_funlib_keys_are_c_order(tmp_path):
|
|
175
|
+
"""``voxel_size``/``offset`` under funlib's names on an N5 array are not reversed, unlike
|
|
176
|
+
``resolution`` and ``pixelResolution``, because zarr-python's N5 store leaves user
|
|
177
|
+
attributes alone."""
|
|
178
|
+
from conftest import write_n5
|
|
179
|
+
|
|
180
|
+
root = tmp_path / "vol.n5"
|
|
181
|
+
write_n5(root / "s0", pattern((4, 4, 4)), attrs={"voxel_size": [1, 2, 3], "offset": [4, 4, 4]})
|
|
182
|
+
meta = wa.metadata(str(root / "s0"))
|
|
183
|
+
assert meta.voxel_size == (1.0, 2.0, 3.0)
|
|
184
|
+
assert meta.translation == pytest.approx((4.5, 5.0, 5.5))
|
|
185
|
+
assert meta.placed_by == "funlib"
|
|
@@ -218,13 +218,14 @@ def test_placement_agrees_across_formats(tmp_path):
|
|
|
218
218
|
np.testing.assert_array_equal(vol.read(box), data[2:5, 2:5, 2:5], err_msg=str(path))
|
|
219
219
|
|
|
220
220
|
|
|
221
|
-
def
|
|
221
|
+
def test_n5_downsampled_level_matches_ome_level(tmp_path):
|
|
222
222
|
"""An N5 pyramid with pixelResolution and downsamplingFactors and no offset sits where the
|
|
223
|
-
equivalent OME pyramid
|
|
223
|
+
equivalent OME pyramid with s0's corner at the origin puts its levels, as Neuroglancer
|
|
224
|
+
draws both: s0 centred at half a voxel, s1 at a whole base voxel (f / 2 * base)."""
|
|
224
225
|
base, s1 = pattern((8, 8, 8)), pattern((4, 4, 4))
|
|
225
226
|
ome = tmp_path / "ome.zarr"
|
|
226
227
|
write_zarr2_group(
|
|
227
|
-
ome, ome_multiscales("zyx", [("s0", (4, 4, 4), (
|
|
228
|
+
ome, ome_multiscales("zyx", [("s0", (4, 4, 4), (2, 2, 2)), ("s1", (8, 8, 8), (4, 4, 4))])
|
|
228
229
|
)
|
|
229
230
|
write_zarr2(ome / "s0", base)
|
|
230
231
|
write_zarr2(ome / "s1", s1)
|
|
@@ -232,10 +233,12 @@ def test_bigdataviewer_level_matches_ome_level(tmp_path):
|
|
|
232
233
|
write_n5_group(n5, {"pixelResolution": {"dimensions": [4, 4, 4], "unit": "nm"}})
|
|
233
234
|
write_n5(n5 / "s0", base, attrs={"downsamplingFactors": [1, 1, 1]})
|
|
234
235
|
write_n5(n5 / "s1", s1, attrs={"downsamplingFactors": [2, 2, 2]})
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
for level in ("s0", "s1"):
|
|
237
|
+
a, b = wa.open(str(ome / level)), wa.open(str(n5 / level))
|
|
238
|
+
assert a.translation == pytest.approx(b.translation), level
|
|
239
|
+
assert b.corner == pytest.approx((0.0, 0.0, 0.0)), level
|
|
240
|
+
box = ((6.0, 6.0, 6.0), (22.0, 22.0, 22.0))
|
|
241
|
+
np.testing.assert_array_equal(a.read(box), b.read(box))
|
|
239
242
|
|
|
240
243
|
|
|
241
244
|
def test_chunks_covering(cube):
|
|
@@ -413,7 +416,7 @@ def test_base_voxel_size_on_a_unitless_n5_pyramid_warns_about_nothing(tmp_path,
|
|
|
413
416
|
)
|
|
414
417
|
assert not [r for r in caplog.records if "no unit" in r.message]
|
|
415
418
|
assert s1.voxel_size == pytest.approx((1.0, 0.314, 0.314)) and s1.translation == pytest.approx(
|
|
416
|
-
(0.
|
|
419
|
+
(0.5, 0.157, 0.157) # the corner at the origin at every level
|
|
417
420
|
)
|
|
418
421
|
|
|
419
422
|
|
|
@@ -699,3 +702,41 @@ def test_callables_see_the_array_without_pinned_axes_and_empty_groups_say_so(tmp
|
|
|
699
702
|
write_zarr2_group(empty)
|
|
700
703
|
with pytest.raises(ValueError, match="no levels found"):
|
|
701
704
|
wa.metadata(str(empty), level="first", open=False)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def test_precomputed_scale_path_is_that_scale_under_level_first(tmp_path):
|
|
708
|
+
"""A path naming a scale is that scale, as an array path is that array; ``level=0`` and
|
|
709
|
+
``"first"`` are silently it and any other level warns (mesh-n-bone's review)."""
|
|
710
|
+
from conftest import pattern
|
|
711
|
+
|
|
712
|
+
pre = tmp_path / "pc"
|
|
713
|
+
data = pattern((8, 8, 8))
|
|
714
|
+
write_precomputed(
|
|
715
|
+
pre,
|
|
716
|
+
[
|
|
717
|
+
{"data": data, "resolution": [4, 4, 4], "key": "4nm"},
|
|
718
|
+
{"data": data[::2, ::2, ::2], "resolution": [8, 8, 8], "key": "8nm"},
|
|
719
|
+
],
|
|
720
|
+
)
|
|
721
|
+
for level in (None, 0, "first"):
|
|
722
|
+
vol = wa.open(str(pre / "8nm"), level=level)
|
|
723
|
+
assert (vol.level_name, vol.voxel_size) == ("8nm", (8.0, 8.0, 8.0)), level
|
|
724
|
+
light = wa.metadata(str(pre / "8nm"), level="first", open=False)
|
|
725
|
+
assert light.voxel_size == (8.0, 8.0, 8.0)
|
|
726
|
+
with pytest.warns(UserWarning, match="names a precomputed scale"):
|
|
727
|
+
assert wa.open(str(pre / "8nm"), level="4nm").level_name == "8nm"
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def test_pyramid_opened_through_a_level_path_places_from_the_group_document(pyramid):
|
|
731
|
+
"""``placements`` reads the group document whichever path opened the pyramid, so a
|
|
732
|
+
level whose array is gone is still placed and nothing is opened (mesh-n-bone's review)."""
|
|
733
|
+
import shutil
|
|
734
|
+
|
|
735
|
+
group, _ = pyramid
|
|
736
|
+
expected = wa.open_pyramid(group).placements()
|
|
737
|
+
shutil.rmtree(f"{group}/s2")
|
|
738
|
+
pyr = wa.open_pyramid(f"{group}/s0")
|
|
739
|
+
assert pyr.names == ["s0", "s1", "s2"]
|
|
740
|
+
assert pyr.placements() == expected
|
|
741
|
+
assert pyr.voxel_sizes()[-1] == ("s2", (16.0, 16.0, 16.0))
|
|
742
|
+
assert pyr.translations()[-1] == ("s2", (8.0, 8.0, 8.0))
|
|
@@ -1247,7 +1247,7 @@ wheels = [
|
|
|
1247
1247
|
|
|
1248
1248
|
[[package]]
|
|
1249
1249
|
name = "worldarray"
|
|
1250
|
-
version = "0.1.
|
|
1250
|
+
version = "0.1.1"
|
|
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'" },
|
worldarray-0.1.0/CHANGELOG.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.1.0 (2026-10-21)
|
|
4
|
-
|
|
5
|
-
- First version, built to `docs/spec.md`: open zarr v2, zarr v3, N5 and Neuroglancer
|
|
6
|
-
precomputed arrays and pyramids through tensorstore; one metadata precedence per format
|
|
7
|
-
with the voxel-centre rule for translations; index-space and world-box reads with
|
|
8
|
-
padding; level selection; nearest and linear resampling; retries; shared and capped
|
|
9
|
-
tensorstore contexts.
|
|
10
|
-
Built and trialled in one day against the readers it replaces in cellmap-flow,
|
|
11
|
-
cellmap-analyze, mesh-n-bone and chunkmirage, and checked on their own data by the
|
|
12
|
-
CineMap, liconn-err, registration-station and salamander repos; see `docs/spec.md`,
|
|
13
|
-
"Trial status". Hot-path reads cost 0.1 to 0.2 ms over a raw tensorstore read.
|
|
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
|