worldarray 0.3.1__tar.gz → 0.5.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.5.0}/CHANGELOG.md +67 -2
- {worldarray-0.3.1 → worldarray-0.5.0}/PKG-INFO +15 -9
- {worldarray-0.3.1 → worldarray-0.5.0}/README.md +14 -8
- {worldarray-0.3.1 → worldarray-0.5.0}/docs/spec.md +156 -27
- {worldarray-0.3.1 → worldarray-0.5.0}/pyproject.toml +1 -1
- {worldarray-0.3.1 → worldarray-0.5.0}/src/worldarray/__init__.py +3 -2
- {worldarray-0.3.1 → worldarray-0.5.0}/src/worldarray/array.py +126 -18
- worldarray-0.5.0/src/worldarray/context.py +205 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/src/worldarray/retry.py +44 -13
- {worldarray-0.3.1 → worldarray-0.5.0}/src/worldarray/store.py +3 -1
- {worldarray-0.3.1 → worldarray-0.5.0}/src/worldarray/write.py +12 -2
- {worldarray-0.3.1 → worldarray-0.5.0}/tests/test_access.py +94 -1
- worldarray-0.5.0/tests/test_lost_wakeup.py +76 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/tests/test_read.py +6 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/tests/test_resample.py +42 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/tests/test_write.py +94 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/uv.lock +1 -1
- worldarray-0.3.1/src/worldarray/context.py +0 -84
- {worldarray-0.3.1 → worldarray-0.5.0}/.github/workflows/ci.yml +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/.github/workflows/docs.yml +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/.github/workflows/publish.yml +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/.gitignore +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/LICENSE +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/docs/changelog.md +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/docs/index.md +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/mkdocs.yml +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/src/worldarray/geometry.py +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/src/worldarray/grid.py +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/src/worldarray/levels.py +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/src/worldarray/placement.py +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/src/worldarray/resample.py +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/src/worldarray/units.py +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/tests/conftest.py +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/tests/fixtures/metadata.json +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/tests/fixtures/write.json +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/tests/test_geometry.py +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/tests/test_grid.py +0 -0
- {worldarray-0.3.1 → worldarray-0.5.0}/tests/test_metadata.py +0 -0
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 (2026-10-23)
|
|
4
|
+
|
|
5
|
+
Two bugs that hung or crashed cellmap-analyze 0.6 on /nrs (salamander's and c-elegans'
|
|
6
|
+
runs, 2026-10-22), and tensorstore's threads sized to the job. No placement change and no
|
|
7
|
+
unit table change.
|
|
8
|
+
|
|
9
|
+
- A read whose first attempt timed out raised `IndexError` instead of retrying: the
|
|
10
|
+
warning took the first line of the error's message, and tensorstore's `TimeoutError()`
|
|
11
|
+
has none. The warning now names the error's type when the message is empty, and the
|
|
12
|
+
error after the last attempt names the operation, the path and the attempts.
|
|
13
|
+
- Every context the package makes or opens a store on is watched: once a second a daemon
|
|
14
|
+
thread gives its decoding and file I/O pools one no-op task each (an in-memory copy and
|
|
15
|
+
a local read of a missing key; no store is touched). tensorstore up to at least 0.1.85
|
|
16
|
+
can strand a queued task: a pool worker idle for 20 ms exits as the task arrives, and
|
|
17
|
+
the task never runs (fixed upstream in google/tensorstore ed51718085, in no release
|
|
18
|
+
yet). With `data_copy` and `file_io` capped at 1, as cellmap-analyze's workers were,
|
|
19
|
+
dozens of workers per run waited forever in `write_index` with no tensorstore thread
|
|
20
|
+
alive, and reads timed out into the `IndexError` above. One more task on the same pool
|
|
21
|
+
wakes it, which salamander's and c-elegans' sessions proved on live stuck workers, so a
|
|
22
|
+
stranded task now runs within about a second, at any concurrency, whoever waits on it:
|
|
23
|
+
worldarray, `vol.ts` directly or `await`. Reproduced on purpose on tensorstore 0.1.75,
|
|
24
|
+
0.1.78 and 0.1.85 (opt-in `tests/test_lost_wakeup.py`): cellmap-analyze's block loop
|
|
25
|
+
gave 29 `IndexError`s and 55 writes stuck past 20 s on 0.4.0, none on 0.5.0, slowest
|
|
26
|
+
0.98 s. Spec open question 14 says when this goes.
|
|
27
|
+
- `write_index`, `write` and `write_chunk` are timed out and retried as reads are
|
|
28
|
+
(Behaviour 6), by a new `retries=` argument or the handle's `retries`. A write used to
|
|
29
|
+
wait on its future with no timeout at all.
|
|
30
|
+
- Without `context`, the shared context's decoding and file I/O pools are limited to the
|
|
31
|
+
new `wa.thread_budget()`: `OMP_NUM_THREADS` when set (dask sets 1 in each worker), else
|
|
32
|
+
the job's slots (`LSB_DJOB_NUMPROC`, `SLURM_CPUS_PER_TASK`, `NSLOTS`), else the CPUs the
|
|
33
|
+
process may run on. A server that wants more threads than its job's slots asks for more
|
|
34
|
+
slots or passes its own context.
|
|
35
|
+
tensorstore alone sizes both to every CPU on the node, whatever `bsub -n` asked for, so
|
|
36
|
+
a dask worker's tensorstore now takes the same one thread as its numpy (pinned to one
|
|
37
|
+
CPU, tensorstore's default context started 43 to 45 pool threads, the capped one 4 to 5).
|
|
38
|
+
Against cellmap-analyze 0.5.0's own block I/O on NFS (128^3 uint64 label blocks, each
|
|
39
|
+
one chunk), its 0.6.1 on this release reads a block in 26 ms instead of 40 and writes it
|
|
40
|
+
in 45 ms instead of 62 from one process at one thread per pool, and about 1.15 times
|
|
41
|
+
faster from 8. Speed gains grow with blocks of several chunks and more threads.
|
|
42
|
+
|
|
43
|
+
## 0.4.0 (2026-10-22)
|
|
44
|
+
|
|
45
|
+
cellmap-analyze's three asks, so it can call worldarray directly with no wrapper, and two
|
|
46
|
+
of cellmap-flow's. No placement change and no unit table change.
|
|
47
|
+
|
|
48
|
+
- `vol.read_onto(grid, begin=None, end=None, ...)` reads voxels `[begin, end)` of a
|
|
49
|
+
`Grid`'s spatial axes from the array, exactly that shape (Behaviour 20). It is the
|
|
50
|
+
`read` of the grid's index box at the grid's voxel size anchored at its corner, so at
|
|
51
|
+
the array's own voxel size nothing is resampled, whatever the sub-voxel offset, and
|
|
52
|
+
otherwise the array is resampled onto the grid. The grid's units are converted.
|
|
53
|
+
- A pickled handle, and `reopen`, use the path resolved against the working directory at
|
|
54
|
+
open, so a worker that changes directory before unpickling opens the same store; it
|
|
55
|
+
used to raise `FileNotFoundError` for a relative path.
|
|
56
|
+
- `write_index`, `write` and `write_chunk` take a scalar, which fills the box. It is
|
|
57
|
+
converted to the dtype only when its value survives (`0` into `uint8`; `300`, `-1`, `0.5`
|
|
58
|
+
and NaN raise), and the fill value over whole units leaves them absent.
|
|
59
|
+
- `vol.source_translation`: the stored level's translation in `units`, beside
|
|
60
|
+
`source_voxel_size`, under resample and relabel too (cellmap-flow's `stored_placement`).
|
|
61
|
+
- `create(exists="adopt")` also compares the keys of `attrs` with the existing array's
|
|
62
|
+
attributes and raises naming each that differs or is missing; keys not given are not
|
|
63
|
+
compared. An adopt given different `attrs` used to keep the old ones silently, as funlib's
|
|
64
|
+
`prepare_ds` kept an old offset (cellmap-flow's `_refuse_another_region`).
|
|
65
|
+
|
|
3
66
|
## 0.3.1 (2026-10-22)
|
|
4
67
|
|
|
5
68
|
- `source_voxel_size` reports the stored level's voxel size under `on_mismatch="relabel"`,
|
|
@@ -10,8 +73,10 @@
|
|
|
10
73
|
`Grid.from_meta(meta, voxel_size=t).box` already was; it was the stored data's extent, so
|
|
11
74
|
`read(box)` could return fewer voxels than `shape` (cellmap-flow's 7 voxels of 8 nm at
|
|
12
75
|
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`
|
|
76
|
+
- **Unit table change.** The unit table knows kilometres (`kilometer`, `kilometre`, `km`,
|
|
77
|
+
1e3 m), so they convert, abbreviate to `km` and are written as OME's `kilometer`
|
|
78
|
+
(chunkmirage's ask). This should have been a minor release (spec section 8): cellmap-flow
|
|
79
|
+
exports the table and its CI failed until it moved its pin to `>=0.3.1`.
|
|
15
80
|
- README: the placement rule gains the corner formula, a worked example, why the centre
|
|
16
81
|
is the model, and how the read rule follows from it.
|
|
17
82
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: worldarray
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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)
|
|
@@ -144,9 +145,12 @@ s1 = wa.open_pyramid("out.zarr", mode="r+").add_level("s1", factors=2) # place
|
|
|
144
145
|
`transform` applied before padding (or `post_transform` after resampling), and resampling by nearest neighbour or linear
|
|
145
146
|
interpolation anchored where you say. Adjoining block reads agree with one read across
|
|
146
147
|
both.
|
|
147
|
-
- **Behaves on a cluster**: one shared tensorstore context per process
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
- **Behaves on a cluster**: one shared tensorstore context per process, its threads sized
|
|
149
|
+
from `OMP_NUM_THREADS` or the LSF job's slots instead of the node's cores, or a capped one
|
|
150
|
+
per worker; reads and writes retried with backoff and size-aware timeouts on transient
|
|
151
|
+
store errors, a cancelled future on timeout, and every context's thread pools nudged once
|
|
152
|
+
a second against a tensorstore bug that can leave a queued task with no thread, so a
|
|
153
|
+
stuck operation recovers or fails loudly instead of hanging the job.
|
|
150
154
|
- **Carries the geometry**: `Box` with `intersect`, `union`, `contains`, `grow`, `shift`,
|
|
151
155
|
`snap` to a grid, and the voxel indices a world box selects.
|
|
152
156
|
- **Writes placed arrays** (0.2): the OME-NGFF 0.4 or 0.5 document from voxel size, centre
|
|
@@ -159,8 +163,8 @@ s1 = wa.open_pyramid("out.zarr", mode="r+").add_level("s1", factors=2) # place
|
|
|
159
163
|
- **Plans outputs and blocks** (0.3): a store-less `Grid` with the same geometry as an
|
|
160
164
|
array, so an output is planned, cut into blocks and only then created; `blocks` tiling an
|
|
161
165
|
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.
|
|
166
|
+
picklable records a cluster job takes by id; the anisotropy-aware pyramid schedule; every
|
|
167
|
+
input read onto that grid by its voxel indices (0.4). Nothing is scheduled or run.
|
|
164
168
|
|
|
165
169
|
## The placement rule
|
|
166
170
|
|
|
@@ -205,6 +209,7 @@ wa.metadata(path, level=None, units=None, open=True) -> ArrayMeta # open=False
|
|
|
205
209
|
wa.detect(path, missing_ok=False) -> Node # format, array or group, raw attrs
|
|
206
210
|
wa.open_tensorstore(path, level=None, context=None)
|
|
207
211
|
wa.shared_context(cache_bytes=0) ; wa.capped_context(cache_bytes=0, concurrency=1, network=True) ; wa.Retry(...)
|
|
212
|
+
wa.thread_budget() # the shared context's thread limit: OMP_NUM_THREADS, the job's slots, or CPUs
|
|
208
213
|
wa.list_chunk_keys(path, separator=".") # chunk indices from keys alone, no metadata needed
|
|
209
214
|
wa.Box ; wa.units.TABLE
|
|
210
215
|
|
|
@@ -227,12 +232,13 @@ wa.units.abbreviation("nanometer") ; wa.store.join(path, "s0") ; wa.store.parent
|
|
|
227
232
|
```
|
|
228
233
|
|
|
229
234
|
`WorldArray`: `ts`, `meta`, `grid`, `shape`, `dtype`, `axes`, `voxel_size`, `translation`,
|
|
230
|
-
`corner`, `box`, `levels`, `level_name`, `level_path`, `source_voxel_size`, `resampled`,
|
|
235
|
+
`corner`, `box`, `levels`, `level_name`, `level_path`, `source_voxel_size`, `source_translation`, `resampled`,
|
|
231
236
|
`writable`, `write_shape`;
|
|
232
237
|
`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)`,
|
|
238
|
+
`read_index(begin, end, padding=, mask=, select=, transform=)`, `read_onto(grid, begin=, end=, ...)`, `index_box(box)`,
|
|
234
239
|
`chunks_covering(begin, end, write=)`, `list_chunks()`, `chunk_key(index)`, `parse_chunk_key(key)`,
|
|
235
|
-
`write_index(begin, end, data, partial=)
|
|
240
|
+
`write_index(begin, end, data, partial=, retries=)` and `write(box, data, partial=, retries=)` (`data` an array or a
|
|
241
|
+
scalar), `reopen(context=)`.
|
|
236
242
|
|
|
237
243
|
A box is a `(begin, end)` pair of floats in your units, half-open per axis, over the
|
|
238
244
|
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)
|
|
@@ -85,9 +86,12 @@ s1 = wa.open_pyramid("out.zarr", mode="r+").add_level("s1", factors=2) # place
|
|
|
85
86
|
`transform` applied before padding (or `post_transform` after resampling), and resampling by nearest neighbour or linear
|
|
86
87
|
interpolation anchored where you say. Adjoining block reads agree with one read across
|
|
87
88
|
both.
|
|
88
|
-
- **Behaves on a cluster**: one shared tensorstore context per process
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
- **Behaves on a cluster**: one shared tensorstore context per process, its threads sized
|
|
90
|
+
from `OMP_NUM_THREADS` or the LSF job's slots instead of the node's cores, or a capped one
|
|
91
|
+
per worker; reads and writes retried with backoff and size-aware timeouts on transient
|
|
92
|
+
store errors, a cancelled future on timeout, and every context's thread pools nudged once
|
|
93
|
+
a second against a tensorstore bug that can leave a queued task with no thread, so a
|
|
94
|
+
stuck operation recovers or fails loudly instead of hanging the job.
|
|
91
95
|
- **Carries the geometry**: `Box` with `intersect`, `union`, `contains`, `grow`, `shift`,
|
|
92
96
|
`snap` to a grid, and the voxel indices a world box selects.
|
|
93
97
|
- **Writes placed arrays** (0.2): the OME-NGFF 0.4 or 0.5 document from voxel size, centre
|
|
@@ -100,8 +104,8 @@ s1 = wa.open_pyramid("out.zarr", mode="r+").add_level("s1", factors=2) # place
|
|
|
100
104
|
- **Plans outputs and blocks** (0.3): a store-less `Grid` with the same geometry as an
|
|
101
105
|
array, so an output is planned, cut into blocks and only then created; `blocks` tiling an
|
|
102
106
|
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.
|
|
107
|
+
picklable records a cluster job takes by id; the anisotropy-aware pyramid schedule; every
|
|
108
|
+
input read onto that grid by its voxel indices (0.4). Nothing is scheduled or run.
|
|
105
109
|
|
|
106
110
|
## The placement rule
|
|
107
111
|
|
|
@@ -146,6 +150,7 @@ wa.metadata(path, level=None, units=None, open=True) -> ArrayMeta # open=False
|
|
|
146
150
|
wa.detect(path, missing_ok=False) -> Node # format, array or group, raw attrs
|
|
147
151
|
wa.open_tensorstore(path, level=None, context=None)
|
|
148
152
|
wa.shared_context(cache_bytes=0) ; wa.capped_context(cache_bytes=0, concurrency=1, network=True) ; wa.Retry(...)
|
|
153
|
+
wa.thread_budget() # the shared context's thread limit: OMP_NUM_THREADS, the job's slots, or CPUs
|
|
149
154
|
wa.list_chunk_keys(path, separator=".") # chunk indices from keys alone, no metadata needed
|
|
150
155
|
wa.Box ; wa.units.TABLE
|
|
151
156
|
|
|
@@ -168,12 +173,13 @@ wa.units.abbreviation("nanometer") ; wa.store.join(path, "s0") ; wa.store.parent
|
|
|
168
173
|
```
|
|
169
174
|
|
|
170
175
|
`WorldArray`: `ts`, `meta`, `grid`, `shape`, `dtype`, `axes`, `voxel_size`, `translation`,
|
|
171
|
-
`corner`, `box`, `levels`, `level_name`, `level_path`, `source_voxel_size`, `resampled`,
|
|
176
|
+
`corner`, `box`, `levels`, `level_name`, `level_path`, `source_voxel_size`, `source_translation`, `resampled`,
|
|
172
177
|
`writable`, `write_shape`;
|
|
173
178
|
`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)`,
|
|
179
|
+
`read_index(begin, end, padding=, mask=, select=, transform=)`, `read_onto(grid, begin=, end=, ...)`, `index_box(box)`,
|
|
175
180
|
`chunks_covering(begin, end, write=)`, `list_chunks()`, `chunk_key(index)`, `parse_chunk_key(key)`,
|
|
176
|
-
`write_index(begin, end, data, partial=)
|
|
181
|
+
`write_index(begin, end, data, partial=, retries=)` and `write(box, data, partial=, retries=)` (`data` an array or a
|
|
182
|
+
scalar), `reopen(context=)`.
|
|
177
183
|
|
|
178
184
|
A box is a `(begin, end)` pair of floats in your units, half-open per axis, over the
|
|
179
185
|
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)
|
|
@@ -229,7 +230,8 @@ in integer Rois builds them from `corner`, `voxel_size` and `shape` in its own s
|
|
|
229
230
|
where it also chooses the integer scale factor that makes the corner whole.
|
|
230
231
|
|
|
231
232
|
`shared_context(cache_bytes)` is one `tensorstore.Context` per budget for the process, so
|
|
232
|
-
the levels of a pyramid and repeat opens share one cache pool; the default budget is 0.
|
|
233
|
+
the levels of a pyramid and repeat opens share one cache pool; the default budget is 0. Its
|
|
234
|
+
decoding and file I/O pools are sized by `thread_budget()` (0.5, Behaviour 7).
|
|
233
235
|
`capped_context` is mesh-n-bone's worker-process context (every tensorstore concurrency
|
|
234
236
|
resource limited, cache off); it returns a **new** context per call, as those workers
|
|
235
237
|
inline the spec into every open today, and `capped_context_spec` exports the dict for
|
|
@@ -285,7 +287,8 @@ vol = wa.create(
|
|
|
285
287
|
attrs=None, # extra array attributes, merged into the array's own
|
|
286
288
|
group_attrs=None, # extra group attributes, merged; no one else's key is dropped or rewritten (Behaviour 12)
|
|
287
289
|
exists="error", # "error": anything already at `path` raises | "adopt": an identical array (shape, dtype, chunks,
|
|
288
|
-
# shards, placement within rtol 1e-6
|
|
290
|
+
# shards, placement within rtol 1e-6, and from 0.4 every key of `attrs` holding the value given)
|
|
291
|
+
# is opened for writing; a different one raises naming each
|
|
289
292
|
# difference. There is no third value. An adopt writes no group document when the entry
|
|
290
293
|
# already says the same (Behaviour 12)
|
|
291
294
|
store_fill=False, # True stores write units equal to fill_value instead of leaving them absent
|
|
@@ -337,6 +340,32 @@ wa.store.join(path, *parts) ; wa.store.parent(path) ; wa.store.basename(path)
|
|
|
337
340
|
wa.units.abbreviation(unit) -> str # the OME short form, "nm" for any spelling of nanometre the table knows
|
|
338
341
|
```
|
|
339
342
|
|
|
343
|
+
### 0.4: reading onto a grid
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
vol.read_onto(grid, begin=None, end=None, padding=None, method=None, mask=False, retries=None, transform=None,
|
|
347
|
+
post_transform=None, select=None)
|
|
348
|
+
# voxels [begin, end) of a Grid's spatial axes (all of it when None), read from this array:
|
|
349
|
+
# exactly that shape, by the centre rule at the array's voxel size, else resampled onto the
|
|
350
|
+
# grid anchored at its corner (Behaviour 20)
|
|
351
|
+
vol.write_index(begin, end, 0) ; vol.write(box, 0) ; vol.write_chunk(index, 0)
|
|
352
|
+
# a scalar fills the box, converted to the dtype only when its value survives (Behaviour 13)
|
|
353
|
+
vol.source_translation # the stored level's translation in `units`, beside source_voxel_size, under resample and relabel
|
|
354
|
+
wa.create(..., attrs={...}, exists="adopt") # an adopt also compares the keys of `attrs` (Behaviour 11)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### 0.5: writes that cannot hang
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
vol.write_index(begin, end, data, partial=False, retries=None)
|
|
361
|
+
vol.write(box, data, partial=False, retries=None) ; vol.write_chunk(index, data, retries=None)
|
|
362
|
+
# timed out and retried as reads are: `retries`, else the handle's (Behaviour 6)
|
|
363
|
+
wa.thread_budget() -> int # threads for tensorstore's decoding and file I/O in this process: OMP_NUM_THREADS, else
|
|
364
|
+
# LSB_DJOB_NUMPROC, else the CPUs it may run on (Behaviour 7)
|
|
365
|
+
# every context the package makes or opens a store on is kicked once a second, whoever
|
|
366
|
+
# waits on it (Behaviour 6)
|
|
367
|
+
```
|
|
368
|
+
|
|
340
369
|
## 3. Conventions
|
|
341
370
|
|
|
342
371
|
These are the rules the package exists to hold in one place. They match chunkmirage's
|
|
@@ -604,25 +633,64 @@ this document and that one differ, fix one of them.
|
|
|
604
633
|
`on_mismatch="relabel"` and picks `method` with its own `is_label_dtype` rule;
|
|
605
634
|
cellmap-analyze passes `method` from its `interpolation_order`.
|
|
606
635
|
|
|
607
|
-
6. **Retries** apply to data reads
|
|
608
|
-
already pin: attempt `k` (from 1) waits `timeout * k` seconds on `.result()`, where
|
|
636
|
+
6. **Retries** apply to data reads and data writes, with mesh-n-bone's formula, which its
|
|
637
|
+
tests already pin: attempt `k` (from 1) waits `timeout * k` seconds on `.result()`, where
|
|
609
638
|
`timeout` is the given value or, when `None`, `clamp(MiB / 16, 5, 120)` for a remote
|
|
610
639
|
store (`gs://`, `s3://`, `http(s)://`) and `clamp(MiB / 256, 5, 30)` for a local one
|
|
611
640
|
(`file`, `memory`), `MiB` being the size of the in-bounds part of the read after
|
|
612
|
-
clipping; between attempts the sleep is `base_delay * 1.3 ** (k - 1)` plus `U(0, 1)`
|
|
641
|
+
clipping, or of the box written; between attempts the sleep is `base_delay * 1.3 ** (k - 1)` plus `U(0, 1)`
|
|
613
642
|
seconds; at most `max_attempts` (default 10). Retried: `TimeoutError`, and a tensorstore
|
|
614
643
|
error whose message starts with `UNAVAILABLE`, `DEADLINE_EXCEEDED` or
|
|
615
644
|
`RESOURCE_EXHAUSTED` or carries an HTTP 5xx or 429. Any other error is raised at once. A
|
|
616
|
-
timed-out tensorstore future is cancelled before the next attempt starts
|
|
617
|
-
|
|
645
|
+
timed-out tensorstore future is cancelled before the next attempt starts; a write may
|
|
646
|
+
still land after its cancel, with the same bytes the next attempt writes. Each retry is
|
|
647
|
+
logged as a warning naming the error's first line, or its type when the message is
|
|
648
|
+
empty (tensorstore's timeout is `TimeoutError()`); when every attempt times out the
|
|
649
|
+
`TimeoutError` names the operation, the path and the attempts. Metadata documents are not
|
|
650
|
+
retried this way (Convention 5). The policy is a per-handle attribute and a per-read and
|
|
651
|
+
per-write argument, never only constructor state.
|
|
652
|
+
|
|
653
|
+
The watchdog (0.5): tensorstore up to at least 0.1.85 can strand a queued task. A pool
|
|
654
|
+
worker idle for 20 ms exits, and a task queued as it exits gets no thread while the pool
|
|
655
|
+
counts itself full (fixed upstream in google/tensorstore ed51718085, in no release yet).
|
|
656
|
+
The future then waits forever with no tensorstore thread alive, on a read, a write or an
|
|
657
|
+
open. A pool limited to one thread meets it most; cellmap-analyze 0.6 hung on /nrs with
|
|
658
|
+
`data_copy` and `file_io` at 1, and one more task on the same context woke the pool
|
|
659
|
+
within seconds (salamander's and c-elegans' live tests, 2026-10-22, with un-kicked
|
|
660
|
+
workers staying stuck). cellmap-analyze's read retries of June 2025 ("it sometimes
|
|
661
|
+
hangs") worked for the same reason: each attempt is a new task. So every context the
|
|
662
|
+
package makes (`shared_context`, `capped_context`, a rebuilt one) or opens a store on is
|
|
663
|
+
watched, weakly: one daemon thread, started on first use, gives each live watched
|
|
664
|
+
context's decoding and file I/O pools one no-op task every second (an in-memory array
|
|
665
|
+
copy and a local read of a key that does not exist; no store is touched, about 0.3 ms
|
|
666
|
+
of CPU). A stranded task then runs within about a second whoever waits on it: the
|
|
667
|
+
package, `vol.ts` directly, or `await`, at any concurrency. The retry stays the outer
|
|
668
|
+
net. Open question 14 says when the watchdog goes.
|
|
618
669
|
|
|
619
670
|
7. **Contexts.** Without `context`, every open in the process shares `shared_context(0)`:
|
|
620
|
-
tensorstore's cache is off and the levels of one pyramid share one context.
|
|
671
|
+
tensorstore's cache is off and the levels of one pyramid share one context. Its
|
|
672
|
+
`data_copy_concurrency` and `file_io_concurrency` limits are `thread_budget()`: the
|
|
673
|
+
first of `OMP_NUM_THREADS` (dask sets 1 in every worker it spawns; a cluster job script
|
|
674
|
+
sets its slots) and `LSB_DJOB_NUMPROC` (an LSF job's slots) that holds a positive
|
|
675
|
+
integer, else the CPUs `sched_getaffinity` allows: the cap every other threaded library
|
|
676
|
+
in the job already takes, as Janelia's cluster guidance asks. tensorstore alone sizes
|
|
677
|
+
both pools to every CPU of the node, whatever the job's slots, so each of twelve dask
|
|
678
|
+
workers in a twelve-slot job on a 96-core node could start 96 threads (0.5). A dask
|
|
679
|
+
worker therefore runs tensorstore on one thread of each pool, which Behaviour 6's
|
|
680
|
+
watchdog makes safe. SLURM's `SLURM_CPUS_PER_TASK` and SGE's `NSLOTS` stand in for
|
|
681
|
+
`LSB_DJOB_NUMPROC` elsewhere. The cap is the default because the process that passes
|
|
682
|
+
no context is the one nobody tuned: a quick dask script over `wa.open(path)` is how a
|
|
683
|
+
job came to start a thousand threads on twelve slots. One thread per pool slows one
|
|
684
|
+
task (a 256^3 block write took 2.1 s against 1.3 s uncapped) but not a job of many
|
|
685
|
+
workers, whose throughput its cores bound either way (four writers: 1.8 s against
|
|
686
|
+
1.9 s). A process that wants more threads than its job has slots, a server under
|
|
687
|
+
`bsub -n 1` say, asks for more slots or passes its own context (decided 2026-10-23,
|
|
688
|
+
over chunkmirage's objection, once the watchdog had removed the hang risk).
|
|
621
689
|
`capped_context` sets `data_copy_concurrency`, `file_io_concurrency`,
|
|
622
690
|
`gcs_request_concurrency`, `s3_request_concurrency` and `http_request_concurrency` to
|
|
623
691
|
`concurrency` and the cache to `cache_bytes`, and returns a new context each call, so two
|
|
624
|
-
handles never share one capped pool unless the caller passes the same object twice
|
|
625
|
-
are plain tensorstore contexts; a caller may pass its own, at construction or through
|
|
692
|
+
handles never share one capped pool unless the caller passes the same object twice; a
|
|
693
|
+
`concurrency` of 1 is safe under Behaviour 6's watchdog. Both are plain tensorstore contexts; a caller may pass its own, at construction or through
|
|
626
694
|
`reopen(context=)` when a long-lived worker changes its concurrency (cellmap-analyze
|
|
627
695
|
reopens tensorstore on every read today for that reason).
|
|
628
696
|
|
|
@@ -682,9 +750,11 @@ Behaviours 10 to 16 are 0.2; 17 to 19 are 0.3.
|
|
|
682
750
|
compression; cellmap-flow's blosc stays blosc by passing it. There is no overwrite: with
|
|
683
751
|
`exists="error"` any document already at `path` raises `FileExistsError`; with
|
|
684
752
|
`exists="adopt"` an array whose shape, dtype, chunks, shards and placement match
|
|
685
|
-
(placement within `rtol 1e-6`)
|
|
686
|
-
|
|
687
|
-
and
|
|
753
|
+
(placement within `rtol 1e-6`), and whose attributes hold every key of `attrs` with the
|
|
754
|
+
value given as JSON stores it (keys not given are someone else's and not compared), is
|
|
755
|
+
opened for writing and its dataset entry ensured, and one that differs raises
|
|
756
|
+
`ValueError` listing every differing field and key with the value found and the value
|
|
757
|
+
wanted (cellmap-flow's `_refuse_another_region`, registration-station's
|
|
688
758
|
missing check, which let a shorter last band's chunk grid win a creation race). A
|
|
689
759
|
creation that loses a race, the store reporting the array present after the probe said
|
|
690
760
|
absent, is re-read with backoff, 5 attempts at `0.5 s * attempt`, and then treated as an
|
|
@@ -697,7 +767,9 @@ Behaviours 10 to 16 are 0.2; 17 to 19 are 0.3.
|
|
|
697
767
|
being the one it replaces. `create` returns a writable `WorldArray`;
|
|
698
768
|
`wa.open(path, mode="r+")` opens an existing one with every rule of Conventions 3 to 5
|
|
699
769
|
unchanged, and a handle pickles by its arguments and its context's spec, so each worker
|
|
700
|
-
process opens its own cheaply
|
|
770
|
+
process opens its own cheaply (a local path goes resolved against the working directory
|
|
771
|
+
at open, so a worker that changes directory before unpickling, as cellmap-analyze's dask
|
|
772
|
+
workers do, opens the same store; `reopen` uses it too), on the shared context when the handle had one, else on
|
|
701
773
|
a context rebuilt once per process from the same spec, so a capped context stays
|
|
702
774
|
capped (mesh-n-bone's per-process cache and thread caps).
|
|
703
775
|
|
|
@@ -722,7 +794,12 @@ Behaviours 10 to 16 are 0.2; 17 to 19 are 0.3.
|
|
|
722
794
|
|
|
723
795
|
13. **Aligned writes need no coordination.** `write_index(begin, end, data)` requires
|
|
724
796
|
`data.shape == end - begin`, `data.dtype == vol.dtype` (no silent cast; `ValueError`),
|
|
725
|
-
|
|
797
|
+
or `data` a scalar (a number or a 0-d array) that fills the box: it is converted to the
|
|
798
|
+
dtype only when its value survives, so an integer or bool array takes whole numbers in
|
|
799
|
+
its range and a float array any number it does not overflow, rounded to its precision
|
|
800
|
+
(`0` into `uint8` is written; `300`, `-1`, `0.5` and NaN raise), and a scalar equal to
|
|
801
|
+
`fill_value` over whole units leaves them absent as an all-fill array does (cellmap-analyze
|
|
802
|
+
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
803
|
`write_shape` on its axis or the array's end on that axis, so the clipped last unit is
|
|
727
804
|
allowed; otherwise `ValueError` names the axis, the bound and the unit size. A unit
|
|
728
805
|
written whole is one put, so writers in different processes writing different units
|
|
@@ -730,7 +807,8 @@ Behaviours 10 to 16 are 0.2; 17 to 19 are 0.3.
|
|
|
730
807
|
construction. `partial=True` lifts the check and does tensorstore's read-modify-write
|
|
731
808
|
of the touched units, documented as unsafe while anything else writes the same unit
|
|
732
809
|
(cellmap-flow's paint slabs, which it serialises itself). A write commits before the
|
|
733
|
-
call returns
|
|
810
|
+
call returns, timed out and retried by Behaviour 6 (0.5; before, it was not, and a
|
|
811
|
+
stranded write waited forever). `write(box,
|
|
734
812
|
data)` maps a world box by Behaviour 2 and then calls `write_index`; a box reaching
|
|
735
813
|
outside the array raises (nothing is padded on write) and `voxel_size` is not an
|
|
736
814
|
argument (nothing is resampled on write).
|
|
@@ -809,6 +887,22 @@ Behaviours 10 to 16 are 0.2; 17 to 19 are 0.3.
|
|
|
809
887
|
returns the OME short form for any spelling the table knows and raises for one it does
|
|
810
888
|
not.
|
|
811
889
|
|
|
890
|
+
20. **Reading onto a grid.** `vol.read_onto(grid, begin, end)` returns voxels `[begin, end)`
|
|
891
|
+
of a `Grid`'s spatial axes, all of it when `begin` and `end` are None, read from this
|
|
892
|
+
array, with exactly `end - begin` voxels on those axes; the array's other axes are read
|
|
893
|
+
as `read` reads them (whole, or pinned by `select`), the open-time `padding`, `method`,
|
|
894
|
+
`retries` and `transform` apply, and `mask` marks the grid voxels that held data. It is
|
|
895
|
+
`read(Box.from_index(begin, end, grid voxel size, grid translation), voxel_size=grid
|
|
896
|
+
voxel size, anchor=grid corner)`, so Behaviours 2 and 4 decide every voxel: at the
|
|
897
|
+
array's own voxel size (within level selection's rtol 1e-6) nothing is resampled,
|
|
898
|
+
whatever the offset between the grids, and each grid voxel takes the stored voxel whose
|
|
899
|
+
centre lies in it, never an interpolation, with `method="linear"` too; at any other
|
|
900
|
+
voxel size the array is resampled onto the grid, anchored at its corner. Adjoining
|
|
901
|
+
`[begin, end)` reads therefore agree with one read across both. The grid's spatial axes
|
|
902
|
+
must be named as the array's are (`ValueError` otherwise), and its units are converted
|
|
903
|
+
to the array's `units`. This is cellmap-analyze's per-operation grid: blocks come from
|
|
904
|
+
one `Grid`, every input is read onto it, and outputs are created on it.
|
|
905
|
+
|
|
812
906
|
## 5. Tests (behavioural; synthetic stores in `tmp_path`; one optional network test)
|
|
813
907
|
|
|
814
908
|
Conformance, one tiny store each, checking `voxel_size`, `translation`, `axes`,
|
|
@@ -898,8 +992,21 @@ factor; missing ancestor documents down from the `.zarr` root and none above it,
|
|
|
898
992
|
ancestor's format followed and a contradiction refused, an array in the way refused; an
|
|
899
993
|
adopt writing no document and a changed placement writing one; a lost entry written again
|
|
900
994
|
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
|
|
995
|
+
pickled handle keeping a capped context's spec and sharing the shared one, and a relative
|
|
996
|
+
path reopening after a `chdir`; `codecs_of` by path; an all-fill write removing its own key;
|
|
997
|
+
a scalar filling a box through `write_index`, `write` and `write_chunk`, refused when it
|
|
998
|
+
does not fit the dtype. 0.5: a read and a write whose first attempts never finish (a future
|
|
999
|
+
nothing resolves) cancelled and retried, the warning naming `TimeoutError`, and the error
|
|
1000
|
+
after the last attempt; a capped context and a caller's context passed to an open both
|
|
1001
|
+
kicked by the watchdog, a dead context no longer watched, and a kick landing on a capped
|
|
1002
|
+
context's own pools; `thread_budget` from each variable, a bad value skipped, and the
|
|
1003
|
+
shared context's limits. Opt-in (`WORLDARRAY_STRESS=1`, about a minute,
|
|
1004
|
+
`tests/test_lost_wakeup.py`): worker processes aim tasks at the moment a one-thread pool's
|
|
1005
|
+
worker exits idle, so a plain context strands some for good (13 of about 7,000 on
|
|
1006
|
+
tensorstore 0.1.78; 40 to 44 of 12,000 to 15,000 with 16 workers on 0.1.75 and 0.1.85)
|
|
1007
|
+
and a watched one strands none, its slowest task under a second. Run against cellmap-analyze's
|
|
1008
|
+
block loop (a chunk read, then a whole-chunk `write_index`, on its 0.6.1 context), 0.4.0
|
|
1009
|
+
gave 29 `IndexError`s and 55 writes stuck past 20 s, and 0.5.0 none, slowest 0.98 s.
|
|
903
1010
|
|
|
904
1011
|
Grids (0.3): `blocks` partitioning with and without a box, `context` as an int, per axis
|
|
905
1012
|
and as before/after pairs, clipping on and off, `len`, `blocks[id]` and `index_of`;
|
|
@@ -908,6 +1015,14 @@ and as before/after pairs, clipping on and off, `len`, `blocks[id]` and `index_o
|
|
|
908
1015
|
reproducing mesh-n-bone's anisotropic table (8, 8, 30 nm: z stays at 1 until y and x
|
|
909
1016
|
reach 32) and all 2s for isotropic data; `abbreviation` for every spelling in the table.
|
|
910
1017
|
|
|
1018
|
+
Reading onto a grid (0.4): `read_onto` at the array's voxel size returning stored values
|
|
1019
|
+
under a sub-voxel offset with `method="linear"`, and the next voxel past a half-voxel one;
|
|
1020
|
+
onto a finer grid equal to the `read` it is defined as, in micrometres too, adjoining
|
|
1021
|
+
reads agreeing, the mask marking grid voxels before the data; its own grid at voxel sizes
|
|
1022
|
+
3.54, 4 and 5.3 returning the stored array exactly; a grid with other axis names refused.
|
|
1023
|
+
`source_translation` under relabel and resample and in micrometres; an adopt keeping keys
|
|
1024
|
+
it was not given and refusing a different or missing one, naming both values.
|
|
1025
|
+
|
|
911
1026
|
Target: about 600 lines of test cases for 0.1 and about 300 more for 0.2 and 0.3, the
|
|
912
1027
|
store builders in `conftest.py` aside; no wording pins; no `/nrs`; no cluster.
|
|
913
1028
|
|
|
@@ -1075,6 +1190,16 @@ over funlib geometry, by this spec's own assignment.
|
|
|
1075
1190
|
boxes on one grid. `Grid.blocks` could carry world boxes too (`b.box`, `b.read_box` as
|
|
1076
1191
|
`Box`, a context in world units). Two lines on cellmap-flow's side today. Not planned
|
|
1077
1192
|
(2026-10-22): daisy does cellmap-flow's tiling and stays; reopen if cellmap-flow drops it.
|
|
1193
|
+
14. **When the watchdog goes.** Behaviour 6's watchdog works around tensorstore's lost
|
|
1194
|
+
wakeup, fixed upstream in google/tensorstore ed51718085 (2026-09-30, "Fix lost wakeups
|
|
1195
|
+
in TaskGroup when workers block or exit") and in no release up to 0.1.85. When a release
|
|
1196
|
+
contains it (`gh api repos/google/tensorstore/compare/ed51718085...vX.Y.Z` reports
|
|
1197
|
+
"ahead" or "identical"), confirm it with `WORLDARRAY_STRESS=1 pytest
|
|
1198
|
+
tests/test_lost_wakeup.py -s` (a plain context should strand nothing), require that
|
|
1199
|
+
release, remove `watch`, `kick` and the watchdog thread from `context.py`, and keep the
|
|
1200
|
+
write retries. That release
|
|
1201
|
+
needs Python 3.11 or later, as tensorstore has since 0.1.81, so it also ends support for
|
|
1202
|
+
3.10.
|
|
1078
1203
|
|
|
1079
1204
|
## 8. Constraints for whoever builds on this
|
|
1080
1205
|
|
|
@@ -1084,10 +1209,14 @@ tests, minimal reusable code. The README is sections 1 to 5 of this file; a beha
|
|
|
1084
1209
|
is not stated here is not implemented without adding the statement first.
|
|
1085
1210
|
|
|
1086
1211
|
Versioning. Every consumer pins a floating range and its CI installs the newest version in
|
|
1087
|
-
it
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1212
|
+
it within the hour of a release, so the version number says what a release can break. A
|
|
1213
|
+
**patch** release brings the code back to what this spec says, or reads more stores or
|
|
1214
|
+
reads them faster; it places nothing differently and adds nothing public. A **minor**
|
|
1215
|
+
release is anything else: it may move voxels or add API, and an entry added to, renamed in
|
|
1216
|
+
or removed from the unit table is API (`wa.units.TABLE` is public, and cellmap-flow exports
|
|
1217
|
+
it to its browser app behind a freshness test). Both were once shipped as patches and both
|
|
1218
|
+
turned cellmap-flow's CI red on every open PR within the hour: 0.1.1 moved offset-less N5
|
|
1219
|
+
pyramids by half a base voxel, and 0.3.1 added kilometres. A changelog entry starts with
|
|
1220
|
+
**Placement change** when voxels move and **Unit table change** when the table does, and the
|
|
1221
|
+
release message to each consumer repeats those lines; nothing before 1.0 promises more.
|
|
1222
|
+
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.5.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" }
|
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
from . import grid, store, units, write
|
|
14
14
|
from .array import Pyramid, WorldArray, create, metadata, open, open_pyramid, open_tensorstore
|
|
15
|
-
from .context import capped_context, capped_context_spec, shared_context
|
|
15
|
+
from .context import capped_context, capped_context_spec, shared_context, thread_budget
|
|
16
16
|
from .geometry import Box, index_range
|
|
17
17
|
from .grid import Block, Grid, blocks, level_factors
|
|
18
18
|
from .levels import select_level
|
|
@@ -28,7 +28,7 @@ from .write import (
|
|
|
28
28
|
parse_chunk_key,
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
-
__version__ = "0.
|
|
31
|
+
__version__ = "0.5.0"
|
|
32
32
|
|
|
33
33
|
__all__ = [
|
|
34
34
|
"ArrayMeta",
|
|
@@ -62,6 +62,7 @@ __all__ = [
|
|
|
62
62
|
"open_tensorstore",
|
|
63
63
|
"select_level",
|
|
64
64
|
"shared_context",
|
|
65
|
+
"thread_budget",
|
|
65
66
|
"store",
|
|
66
67
|
"units",
|
|
67
68
|
"write",
|