worldarray 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
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/__init__.py +12 -4
- worldarray/array.py +46 -4
- worldarray/grid.py +492 -0
- worldarray/store.py +5 -0
- worldarray/units.py +21 -0
- {worldarray-0.2.0.dist-info → worldarray-0.3.0.dist-info}/METADATA +14 -1
- worldarray-0.3.0.dist-info/RECORD +16 -0
- worldarray-0.2.0.dist-info/RECORD +0 -15
- {worldarray-0.2.0.dist-info → worldarray-0.3.0.dist-info}/WHEEL +0 -0
- {worldarray-0.2.0.dist-info → worldarray-0.3.0.dist-info}/licenses/LICENSE +0 -0
worldarray/__init__.py
CHANGED
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Open zarr v2, zarr v3, N5 and Neuroglancer precomputed arrays and pyramids by path, know
|
|
4
4
|
where each sits (voxel size, the centre of voxel 0, units, axes), and read voxel boxes or
|
|
5
|
-
world boxes as numpy arrays, padded where the box leaves the data
|
|
6
|
-
|
|
5
|
+
world boxes as numpy arrays, padded where the box leaves the data; create placed OME-NGFF
|
|
6
|
+
levels and write them from any number of jobs; plan an output grid and cut it into blocks
|
|
7
|
+
before anything exists. ``docs/spec.md`` is the contract; the README is its first five
|
|
8
|
+
sections.
|
|
7
9
|
"""
|
|
8
10
|
|
|
9
11
|
from __future__ import annotations
|
|
10
12
|
|
|
11
|
-
from . import store, units, write
|
|
13
|
+
from . import grid, store, units, write
|
|
12
14
|
from .array import Pyramid, WorldArray, create, metadata, open, open_pyramid, open_tensorstore
|
|
13
15
|
from .context import capped_context, capped_context_spec, shared_context
|
|
14
16
|
from .geometry import Box, index_range
|
|
17
|
+
from .grid import Block, Grid, blocks, level_factors
|
|
15
18
|
from .levels import select_level
|
|
16
19
|
from .placement import ArrayMeta, corner_to_centre, default_axes
|
|
17
20
|
from .retry import Retry
|
|
@@ -25,19 +28,24 @@ from .write import (
|
|
|
25
28
|
parse_chunk_key,
|
|
26
29
|
)
|
|
27
30
|
|
|
28
|
-
__version__ = "0.
|
|
31
|
+
__version__ = "0.3.0"
|
|
29
32
|
|
|
30
33
|
__all__ = [
|
|
31
34
|
"ArrayMeta",
|
|
35
|
+
"Block",
|
|
32
36
|
"Box",
|
|
37
|
+
"Grid",
|
|
33
38
|
"Node",
|
|
39
|
+
"blocks",
|
|
34
40
|
"chunk_key",
|
|
35
41
|
"corner_to_centre",
|
|
36
42
|
"create",
|
|
37
43
|
"default_axes",
|
|
38
44
|
"detect",
|
|
39
45
|
"ensure_group_entry",
|
|
46
|
+
"grid",
|
|
40
47
|
"index_range",
|
|
48
|
+
"level_factors",
|
|
41
49
|
"level_placement",
|
|
42
50
|
"level_shape",
|
|
43
51
|
"list_chunk_keys",
|
worldarray/array.py
CHANGED
|
@@ -551,6 +551,34 @@ class WorldArray:
|
|
|
551
551
|
)
|
|
552
552
|
self.write_index(begin, end, data, partial=partial)
|
|
553
553
|
|
|
554
|
+
def blocks(self, block_shape=None, context=0, box=None, clip: bool = True):
|
|
555
|
+
"""The array cut into blocks (Behaviour 17): write boxes tiling the stored grid (or a
|
|
556
|
+
voxel ``box``) with ``block_shape`` (default ``write_shape``, so the default blocks are
|
|
557
|
+
aligned writes), read boxes grown by ``context`` and clipped to the array when
|
|
558
|
+
``clip``. Plain records, nothing scheduled."""
|
|
559
|
+
from .grid import blocks as _blocks
|
|
560
|
+
|
|
561
|
+
return _blocks(
|
|
562
|
+
self._grid.shape,
|
|
563
|
+
self.write_shape if block_shape is None else block_shape,
|
|
564
|
+
context=context,
|
|
565
|
+
box=box,
|
|
566
|
+
clip=clip,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
def write_chunk(self, index: Sequence[int], data: np.ndarray) -> None:
|
|
570
|
+
"""Write the whole write unit at ``index`` on the storage grid (the shard of a sharded
|
|
571
|
+
array); ``data`` has that unit's shape, clipped at the array's end."""
|
|
572
|
+
unit, shape = self.write_shape, self._grid.shape
|
|
573
|
+
index = tuple(int(i) for i in index)
|
|
574
|
+
if len(index) != len(shape):
|
|
575
|
+
raise ValueError(f"{self.path}: index needs one entry per axis of {shape}")
|
|
576
|
+
begin = tuple(i * u for i, u in zip(index, unit))
|
|
577
|
+
if any(i < 0 or b >= n for i, b, n in zip(index, begin, shape)):
|
|
578
|
+
raise ValueError(f"{self.path}: no write unit at {index}")
|
|
579
|
+
end = tuple(min(b + u, n) for b, u, n in zip(begin, unit, shape))
|
|
580
|
+
self.write_index(begin, end, data)
|
|
581
|
+
|
|
554
582
|
def read_index(
|
|
555
583
|
self,
|
|
556
584
|
begin: Sequence[int],
|
|
@@ -1051,11 +1079,12 @@ class Pyramid:
|
|
|
1051
1079
|
|
|
1052
1080
|
def create(
|
|
1053
1081
|
path: str,
|
|
1054
|
-
shape: Sequence[int],
|
|
1055
|
-
dtype,
|
|
1056
|
-
voxel_size,
|
|
1057
|
-
translation,
|
|
1082
|
+
shape: Sequence[int] | None = None,
|
|
1083
|
+
dtype=None,
|
|
1084
|
+
voxel_size=None,
|
|
1085
|
+
translation=None,
|
|
1058
1086
|
*,
|
|
1087
|
+
grid=None,
|
|
1059
1088
|
axes: Sequence[str] | None = None,
|
|
1060
1089
|
units: Units = "nanometer",
|
|
1061
1090
|
chunks: Sequence[int] | None = None,
|
|
@@ -1079,6 +1108,19 @@ def create(
|
|
|
1079
1108
|
raises ``FileExistsError``; with ``"adopt"`` an identical array (shape, dtype, chunks,
|
|
1080
1109
|
shards, placement) is opened for writing and a different one raises ``ValueError``
|
|
1081
1110
|
naming every difference. Returns a writable ``WorldArray``."""
|
|
1111
|
+
if grid is not None:
|
|
1112
|
+
# a Grid planned the output (Behaviour 18): its facts are the array's, chunks and
|
|
1113
|
+
# shards overridable; the placement arguments must not also be given
|
|
1114
|
+
if any(v is not None for v in (shape, voxel_size, translation, axes)):
|
|
1115
|
+
raise ValueError("pass grid= or shape, voxel_size, translation and axes, not both")
|
|
1116
|
+
shape, voxel_size, translation = grid.shape, grid.voxel_size, grid.translation
|
|
1117
|
+
axes, units = grid.axes, grid.units
|
|
1118
|
+
chunks = grid.chunks if chunks is None else chunks
|
|
1119
|
+
shards = grid.shards if shards is None else shards
|
|
1120
|
+
if shape is None or dtype is None or voxel_size is None or translation is None:
|
|
1121
|
+
raise ValueError(
|
|
1122
|
+
"create needs shape, dtype, voxel_size and translation, or grid= and dtype"
|
|
1123
|
+
)
|
|
1082
1124
|
if chunks is None:
|
|
1083
1125
|
raise ValueError("chunks is required: the package never picks a chunk shape")
|
|
1084
1126
|
ndim = len(shape)
|
worldarray/grid.py
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""Grids and blocks: an output planned, and cut into jobs, before anything is stored.
|
|
2
|
+
|
|
3
|
+
``blocks(shape, block_shape, context, box, clip)`` tiles ``[0, shape)``, or a voxel
|
|
4
|
+
``box`` with the block grid anchored at its begin, into write boxes in C order with no
|
|
5
|
+
overlap and no gap, the last block on an axis clipped. Each block's read box is its write
|
|
6
|
+
box grown by ``context``. ``Grid`` is a ``WorldArray`` without a store: the same ``box``,
|
|
7
|
+
``corner``, ``index_box``, ``chunks_covering``, ``write_shape`` and ``blocks`` from the
|
|
8
|
+
facts alone. ``level_factors`` is the pyramid schedule mesh-n-bone and cellmap-analyze
|
|
9
|
+
share. Nothing here schedules or runs anything (spec, Behaviours 17 to 19).
|
|
10
|
+
|
|
11
|
+
Choices the spec leaves open, made here:
|
|
12
|
+
|
|
13
|
+
- ``box`` may reach past the array on any side (a ROI rounded outward to a factor
|
|
14
|
+
multiple). The write boxes tile ``box`` exactly, outside the array too; ``clip=True``
|
|
15
|
+
clips the **read** boxes to the array, ``[0, shape)``, never to ``box`` and never a write
|
|
16
|
+
box. A write box wholly outside the array then has an empty read box at the array's
|
|
17
|
+
nearest face. ``clip=False`` leaves read boxes unclipped, negative begins included, for a
|
|
18
|
+
padded ``read_index``.
|
|
19
|
+
- ``context`` is one int, one per axis, or a ``(before, after)`` pair per axis; the per-axis
|
|
20
|
+
entries may mix ints and pairs. Negative context raises.
|
|
21
|
+
- ``Blocks`` is a plain frozen dataclass of the five arguments, normalised: ``box`` to a
|
|
22
|
+
pair of int tuples (``[0, shape)`` when none is given), ``context`` to one
|
|
23
|
+
``(before, after)`` pair per axis. Blocks are computed from their id on demand, so
|
|
24
|
+
``blocks[id]`` is O(1) and the record pickles to a worker without the blocks. Ids run from
|
|
25
|
+
0 to ``len - 1``; any other id is an ``IndexError``. ``index_of(begin)`` takes a begin on
|
|
26
|
+
the box-anchored grid.
|
|
27
|
+
- ``Block.at_start`` and ``Block.at_end`` say, per axis, whether the block is the first or
|
|
28
|
+
last on the block grid (the box's edge, or the array's). ``Block.inner`` is the write box
|
|
29
|
+
inside the read box, ``slice(begin - read_begin, end - read_begin)`` per axis, which stays
|
|
30
|
+
a correct offset into a padded read when ``clip=False`` lets ``read_begin`` go below 0.
|
|
31
|
+
- ``Grid`` keeps ``voxel_size`` and ``translation`` with one entry per axis, as
|
|
32
|
+
``ArrayMeta`` does; given over the spatial axes only, the other axes get 1.0 and 0.0 (the
|
|
33
|
+
short-list rule of Convention 4). ``units`` is kept per axis too: a string is the spatial
|
|
34
|
+
axes' unit and every other axis gets ``""``; a dict by kind gives ``"space"`` to the
|
|
35
|
+
spatial axes and ``"time"`` to an axis named ``t``, ``""`` elsewhere; a sequence is per
|
|
36
|
+
axis. ``shards`` need ``chunks`` and must be a whole number of them on every axis.
|
|
37
|
+
- ``Grid.chunks_covering(begin, end, write=False)`` takes ``write`` as ``WorldArray``'s
|
|
38
|
+
does in 0.2: ``write=True`` runs on ``write_shape``. A grid without that shape raises.
|
|
39
|
+
- ``Grid.meta(dtype)`` has ``format`` None, ``path`` ``""``, ``parent`` None, empty
|
|
40
|
+
``attrs`` and ``group_attrs``, ``chunk_shape`` the chunks (the shape when none),
|
|
41
|
+
``axes_named`` True (a created array names its axes), ``placed_by`` ``"ome"`` and
|
|
42
|
+
``fill_value`` None: a grid has no store, no documents and no fill.
|
|
43
|
+
- ``Grid.from_meta(meta)`` keeps the array's chunks (and its ``shard_shape`` when the meta
|
|
44
|
+
carries one); the target grid at ``voxel_size`` keeps neither, since its chunks are the
|
|
45
|
+
new array's to choose. ``voxel_size`` is in the meta's units.
|
|
46
|
+
- ``level_factors`` compares voxel sizes with ``rtol 1e-6``, as Behaviour 3 does. The stop
|
|
47
|
+
rule is checked before each level, so a ``shape`` already within ``stop_shape`` gives no
|
|
48
|
+
levels and the last level returned is the first that fits. ``ratio`` below 1 raises,
|
|
49
|
+
since no axis would ever qualify.
|
|
50
|
+
|
|
51
|
+
The ``WorldArray`` side of 0.3 (``vol.blocks``, ``vol.write_chunk`` and
|
|
52
|
+
``wa.create(path, grid=g)``) is wired in with the write module of 0.2; nothing here opens a
|
|
53
|
+
store.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
import itertools
|
|
59
|
+
import math
|
|
60
|
+
import operator
|
|
61
|
+
from collections.abc import Iterable, Iterator, Sequence
|
|
62
|
+
from dataclasses import dataclass
|
|
63
|
+
|
|
64
|
+
from .geometry import Box, index_range
|
|
65
|
+
from .placement import ArrayMeta, default_axes, spatial_axes
|
|
66
|
+
from .resample import target_shape
|
|
67
|
+
|
|
68
|
+
RTOL = 1e-6
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _whole(values, what: str) -> tuple[int, ...]:
|
|
72
|
+
values = tuple(values)
|
|
73
|
+
out = tuple(int(v) for v in values)
|
|
74
|
+
if any(o != v for o, v in zip(out, values)):
|
|
75
|
+
raise ValueError(f"{what} must be whole numbers, got {values}")
|
|
76
|
+
return out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _sized(values, what: str, ndim: int, minimum: int) -> tuple[int, ...]:
|
|
80
|
+
out = _whole(values, what)
|
|
81
|
+
if len(out) != ndim:
|
|
82
|
+
raise ValueError(f"{what} has {len(out)} entries; expected {ndim}")
|
|
83
|
+
if any(v < minimum for v in out):
|
|
84
|
+
raise ValueError(f"{what} entries must be at least {minimum}, got {out}")
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _context(context, ndim: int) -> tuple[tuple[int, int], ...]:
|
|
89
|
+
"""One ``(before, after)`` pair per axis from an int, one int per axis, or a pair per axis."""
|
|
90
|
+
if not isinstance(context, Iterable):
|
|
91
|
+
entries = [context] * ndim
|
|
92
|
+
else:
|
|
93
|
+
entries = list(context)
|
|
94
|
+
if len(entries) != ndim:
|
|
95
|
+
raise ValueError(f"context has {len(entries)} entries; the blocks have {ndim} axes")
|
|
96
|
+
pairs = []
|
|
97
|
+
for e in entries:
|
|
98
|
+
pair = _whole((e, e) if not isinstance(e, Iterable) else e, "context")
|
|
99
|
+
if len(pair) != 2:
|
|
100
|
+
raise ValueError(f"a context entry is an int or a (before, after) pair, got {e!r}")
|
|
101
|
+
if min(pair) < 0:
|
|
102
|
+
raise ValueError(f"context cannot be negative, got {e!r}")
|
|
103
|
+
pairs.append(pair)
|
|
104
|
+
return tuple(pairs)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# Blocks
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class Block:
|
|
113
|
+
"""One block: a plain, picklable record. ``begin``/``end`` is the write box,
|
|
114
|
+
``read_begin``/``read_end`` the write box grown by the context (clipped to the array
|
|
115
|
+
when the blocks clip)."""
|
|
116
|
+
|
|
117
|
+
index: tuple[int, ...] # position on the block grid
|
|
118
|
+
id: int # linear C-order id
|
|
119
|
+
begin: tuple[int, ...]
|
|
120
|
+
end: tuple[int, ...]
|
|
121
|
+
read_begin: tuple[int, ...]
|
|
122
|
+
read_end: tuple[int, ...]
|
|
123
|
+
at_start: tuple[bool, ...] # per axis: the first block on the grid
|
|
124
|
+
at_end: tuple[bool, ...] # per axis: the last block on the grid
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def inner(self) -> tuple[slice, ...]:
|
|
128
|
+
"""The write box inside the read box: ``data[block.inner]`` of a read of
|
|
129
|
+
``[read_begin, read_end)`` is the block's own voxels."""
|
|
130
|
+
return tuple(
|
|
131
|
+
slice(b - rb, e - rb) for b, e, rb in zip(self.begin, self.end, self.read_begin)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(frozen=True)
|
|
136
|
+
class Blocks:
|
|
137
|
+
"""The blocks tiling ``[0, shape)``, or ``box``, in C order. A frozen record of its
|
|
138
|
+
arguments that computes each block on demand: ``len``, iteration, ``blocks[id]`` (O(1))
|
|
139
|
+
and ``index_of(begin)``."""
|
|
140
|
+
|
|
141
|
+
shape: tuple[int, ...]
|
|
142
|
+
block_shape: tuple[int, ...]
|
|
143
|
+
context: tuple[tuple[int, int], ...] = 0 # normalised to (before, after) per axis
|
|
144
|
+
box: tuple[tuple[int, ...], tuple[int, ...]] | None = None # normalised; [0, shape) if None
|
|
145
|
+
clip: bool = True
|
|
146
|
+
|
|
147
|
+
def __post_init__(self) -> None:
|
|
148
|
+
shape = _whole(self.shape, "shape")
|
|
149
|
+
if any(s < 0 for s in shape):
|
|
150
|
+
raise ValueError(f"shape cannot be negative, got {shape}")
|
|
151
|
+
n = len(shape)
|
|
152
|
+
block_shape = _sized(self.block_shape, "block_shape", n, 1)
|
|
153
|
+
if self.box is None:
|
|
154
|
+
box = ((0,) * n, shape)
|
|
155
|
+
else:
|
|
156
|
+
b = Box.coerce(self.box)
|
|
157
|
+
if b.ndim != n:
|
|
158
|
+
raise ValueError(f"box has {b.ndim} axes; the blocks have {n}")
|
|
159
|
+
box = (_whole(b.begin, "box"), _whole(b.end, "box"))
|
|
160
|
+
object.__setattr__(self, "shape", shape)
|
|
161
|
+
object.__setattr__(self, "block_shape", block_shape)
|
|
162
|
+
object.__setattr__(self, "context", _context(self.context, n))
|
|
163
|
+
object.__setattr__(self, "box", box)
|
|
164
|
+
object.__setattr__(self, "clip", bool(self.clip))
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def _counts(self) -> tuple[int, ...]:
|
|
168
|
+
"""Blocks per axis."""
|
|
169
|
+
return tuple(-(-max(e - b, 0) // s) for b, e, s in zip(*self.box, self.block_shape))
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def count(self) -> int:
|
|
173
|
+
return math.prod(self._counts)
|
|
174
|
+
|
|
175
|
+
def __len__(self) -> int:
|
|
176
|
+
return self.count
|
|
177
|
+
|
|
178
|
+
def __iter__(self) -> Iterator[Block]:
|
|
179
|
+
counts = self._counts
|
|
180
|
+
for i, index in enumerate(itertools.product(*(range(c) for c in counts))):
|
|
181
|
+
yield self._block(index, i, counts)
|
|
182
|
+
|
|
183
|
+
def __getitem__(self, block_id: int) -> Block:
|
|
184
|
+
i = operator.index(block_id)
|
|
185
|
+
counts = self._counts
|
|
186
|
+
total = math.prod(counts)
|
|
187
|
+
if not 0 <= i < total:
|
|
188
|
+
raise IndexError(f"block id {i} is outside [0, {total})")
|
|
189
|
+
index, rest = [], i
|
|
190
|
+
for c in reversed(counts):
|
|
191
|
+
rest, k = divmod(rest, c)
|
|
192
|
+
index.append(k)
|
|
193
|
+
return self._block(tuple(reversed(index)), i, counts)
|
|
194
|
+
|
|
195
|
+
def index_of(self, begin: Sequence[int]) -> tuple[int, ...]:
|
|
196
|
+
"""The block index of the block whose write box starts at ``begin``."""
|
|
197
|
+
begin = tuple(begin)
|
|
198
|
+
if len(begin) != len(self.shape):
|
|
199
|
+
raise ValueError(f"begin has {len(begin)} entries; the blocks have {len(self.shape)}")
|
|
200
|
+
index = []
|
|
201
|
+
for b, b0, s, c in zip(begin, self.box[0], self.block_shape, self._counts):
|
|
202
|
+
k, r = divmod(b - b0, s)
|
|
203
|
+
if r != 0 or not 0 <= k < c:
|
|
204
|
+
raise ValueError(
|
|
205
|
+
f"{begin} is not a block's first voxel: the blocks start at {self.box[0]} "
|
|
206
|
+
f"in steps of {self.block_shape}, {self._counts} per axis"
|
|
207
|
+
)
|
|
208
|
+
index.append(int(k))
|
|
209
|
+
return tuple(index)
|
|
210
|
+
|
|
211
|
+
def _block(self, index: tuple[int, ...], block_id: int, counts: tuple[int, ...]) -> Block:
|
|
212
|
+
origin, stop = self.box
|
|
213
|
+
begin = tuple(o + k * s for o, k, s in zip(origin, index, self.block_shape))
|
|
214
|
+
end = tuple(min(b + s, e) for b, s, e in zip(begin, self.block_shape, stop))
|
|
215
|
+
read_begin = tuple(b - c[0] for b, c in zip(begin, self.context))
|
|
216
|
+
read_end = tuple(e + c[1] for e, c in zip(end, self.context))
|
|
217
|
+
if self.clip:
|
|
218
|
+
read_begin = tuple(min(max(r, 0), n) for r, n in zip(read_begin, self.shape))
|
|
219
|
+
read_end = tuple(
|
|
220
|
+
max(min(r, n), lo) for r, n, lo in zip(read_end, self.shape, read_begin)
|
|
221
|
+
)
|
|
222
|
+
return Block(
|
|
223
|
+
index=tuple(index),
|
|
224
|
+
id=block_id,
|
|
225
|
+
begin=begin,
|
|
226
|
+
end=end,
|
|
227
|
+
read_begin=read_begin,
|
|
228
|
+
read_end=read_end,
|
|
229
|
+
at_start=tuple(k == 0 for k in index),
|
|
230
|
+
at_end=tuple(k == c - 1 for k, c in zip(index, counts)),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def blocks(shape, block_shape, context=0, box=None, clip: bool = True) -> Blocks:
|
|
235
|
+
"""The blocks tiling ``[0, shape)``, or the voxel ``box`` (anchored at its begin), by
|
|
236
|
+
``block_shape``, each read box grown by ``context`` and clipped to the array when
|
|
237
|
+
``clip`` (Behaviour 17)."""
|
|
238
|
+
return Blocks(shape, block_shape, context, box, clip)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# Grids
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _per_axis(values, what: str, ndim: int, spatial: tuple[int, ...], other: float):
|
|
246
|
+
values = tuple(float(v) for v in values)
|
|
247
|
+
if len(values) == ndim:
|
|
248
|
+
return values
|
|
249
|
+
if len(values) == len(spatial):
|
|
250
|
+
out = [other] * ndim
|
|
251
|
+
for k, axis in enumerate(spatial):
|
|
252
|
+
out[axis] = values[k]
|
|
253
|
+
return tuple(out)
|
|
254
|
+
raise ValueError(
|
|
255
|
+
f"{what} has {len(values)} entries; expected {len(spatial)} (spatial) or {ndim}"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _axis_units(units, axes: tuple[str, ...], spatial: tuple[int, ...]) -> tuple[str, ...]:
|
|
260
|
+
sp = set(spatial)
|
|
261
|
+
if isinstance(units, str):
|
|
262
|
+
return tuple(units if i in sp else "" for i in range(len(axes)))
|
|
263
|
+
if isinstance(units, dict):
|
|
264
|
+
unknown = set(units) - {"space", "time"}
|
|
265
|
+
if unknown:
|
|
266
|
+
raise ValueError(f"units by kind take 'space' and 'time', got {sorted(unknown)}")
|
|
267
|
+
return tuple(
|
|
268
|
+
units.get("space", "") if i in sp else units.get("time", "") if a == "t" else ""
|
|
269
|
+
for i, a in enumerate(axes)
|
|
270
|
+
)
|
|
271
|
+
units = tuple(str(u) for u in units)
|
|
272
|
+
if len(units) != len(axes):
|
|
273
|
+
raise ValueError(f"units has {len(units)} entries; expected {len(axes)}")
|
|
274
|
+
return units
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@dataclass(frozen=True)
|
|
278
|
+
class Grid:
|
|
279
|
+
"""A store-less grid: ``shape`` voxels of ``voxel_size`` with voxel 0 centred at
|
|
280
|
+
``translation`` (in ``units``), optionally chunked and sharded. Answers what a
|
|
281
|
+
``WorldArray`` of the same facts answers (Behaviour 18)."""
|
|
282
|
+
|
|
283
|
+
shape: tuple[int, ...]
|
|
284
|
+
voxel_size: tuple[float, ...] # one entry per axis once constructed
|
|
285
|
+
translation: tuple[float, ...] # the CENTRE of voxel 0, one entry per axis
|
|
286
|
+
axes: tuple[str, ...] | None = None # default_axes(ndim) when None
|
|
287
|
+
units: str | dict | tuple[str, ...] = "nanometer" # one string per axis once constructed
|
|
288
|
+
chunks: tuple[int, ...] | None = None
|
|
289
|
+
shards: tuple[int, ...] | None = None
|
|
290
|
+
|
|
291
|
+
def __post_init__(self) -> None:
|
|
292
|
+
shape = _whole(self.shape, "shape")
|
|
293
|
+
if any(s < 0 for s in shape):
|
|
294
|
+
raise ValueError(f"shape cannot be negative, got {shape}")
|
|
295
|
+
n = len(shape)
|
|
296
|
+
axes = default_axes(n) if self.axes is None else tuple(str(a) for a in self.axes)
|
|
297
|
+
if len(axes) != n:
|
|
298
|
+
raise ValueError(f"axes has {len(axes)} entries; the shape has {n}")
|
|
299
|
+
spatial = spatial_axes(axes)
|
|
300
|
+
voxel_size = _per_axis(self.voxel_size, "voxel_size", n, spatial, 1.0)
|
|
301
|
+
if any(v <= 0 for v in voxel_size):
|
|
302
|
+
raise ValueError(f"voxel_size must be positive, got {voxel_size}")
|
|
303
|
+
translation = _per_axis(self.translation, "translation", n, spatial, 0.0)
|
|
304
|
+
chunks = None if self.chunks is None else _sized(self.chunks, "chunks", n, 1)
|
|
305
|
+
shards = None if self.shards is None else _sized(self.shards, "shards", n, 1)
|
|
306
|
+
if shards is not None:
|
|
307
|
+
if chunks is None:
|
|
308
|
+
raise ValueError("shards need chunks: a shard is a whole number of chunks")
|
|
309
|
+
if any(s % c for s, c in zip(shards, chunks)):
|
|
310
|
+
raise ValueError(f"shards {shards} are not a whole number of chunks {chunks}")
|
|
311
|
+
object.__setattr__(self, "shape", shape)
|
|
312
|
+
object.__setattr__(self, "axes", axes)
|
|
313
|
+
object.__setattr__(self, "voxel_size", voxel_size)
|
|
314
|
+
object.__setattr__(self, "translation", translation)
|
|
315
|
+
object.__setattr__(self, "units", _axis_units(self.units, axes, spatial))
|
|
316
|
+
object.__setattr__(self, "chunks", chunks)
|
|
317
|
+
object.__setattr__(self, "shards", shards)
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
def from_meta(cls, meta: ArrayMeta, voxel_size: Sequence[float] | None = None) -> Grid:
|
|
321
|
+
"""The grid of an existing array; with ``voxel_size`` (per spatial axis, in the meta's
|
|
322
|
+
units), Behaviour 4's target grid over it: anchored at its corner, ``ceil(n * s / t)``
|
|
323
|
+
voxels per spatial axis, voxel 0 centred at ``corner + t / 2``, so a
|
|
324
|
+
``read(voxel_size=t)`` of the source fills it exactly."""
|
|
325
|
+
if voxel_size is None:
|
|
326
|
+
return cls(
|
|
327
|
+
meta.shape,
|
|
328
|
+
meta.voxel_size,
|
|
329
|
+
meta.translation,
|
|
330
|
+
meta.axes,
|
|
331
|
+
meta.units,
|
|
332
|
+
meta.chunk_shape,
|
|
333
|
+
getattr(meta, "shard_shape", None),
|
|
334
|
+
)
|
|
335
|
+
target = tuple(float(v) for v in voxel_size)
|
|
336
|
+
spatial = meta.spatial
|
|
337
|
+
if len(target) != len(spatial):
|
|
338
|
+
raise ValueError(
|
|
339
|
+
f"voxel_size has {len(target)} entries; the array has {len(spatial)} spatial axes"
|
|
340
|
+
)
|
|
341
|
+
shape, vs, tr = list(meta.shape), list(meta.voxel_size), list(meta.translation)
|
|
342
|
+
corner = meta.corner
|
|
343
|
+
for k, axis in enumerate(spatial):
|
|
344
|
+
shape[axis] = target_shape(meta.shape[axis], meta.voxel_size[axis], target[k])
|
|
345
|
+
vs[axis] = target[k]
|
|
346
|
+
tr[axis] = corner[axis] + target[k] / 2
|
|
347
|
+
return cls(tuple(shape), tuple(vs), tuple(tr), meta.axes, meta.units)
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def ndim(self) -> int:
|
|
351
|
+
return len(self.shape)
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def spatial(self) -> tuple[int, ...]:
|
|
355
|
+
"""Indices of the axes named z, y or x when any are; else the last three."""
|
|
356
|
+
return spatial_axes(self.axes)
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def corner(self) -> tuple[float, ...]:
|
|
360
|
+
"""``translation - voxel_size / 2`` on the spatial axes."""
|
|
361
|
+
return tuple(self.translation[i] - self.voxel_size[i] / 2 for i in self.spatial)
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def box(self) -> Box:
|
|
365
|
+
"""The grid's extent on the spatial axes, ``[corner, corner + shape * voxel_size)``."""
|
|
366
|
+
sp = self.spatial
|
|
367
|
+
return Box.from_index(
|
|
368
|
+
(0,) * len(sp),
|
|
369
|
+
tuple(self.shape[i] for i in sp),
|
|
370
|
+
tuple(self.voxel_size[i] for i in sp),
|
|
371
|
+
tuple(self.translation[i] for i in sp),
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def index_box(self, box) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
|
375
|
+
"""The voxel index range a world box selects (Behaviour 2's centre rule), over the
|
|
376
|
+
spatial axes or every axis; the other axes come back whole."""
|
|
377
|
+
box = Box.coerce(box)
|
|
378
|
+
spatial, ndim = self.spatial, self.ndim
|
|
379
|
+
if box.ndim == len(spatial):
|
|
380
|
+
axes = list(spatial)
|
|
381
|
+
elif box.ndim == ndim:
|
|
382
|
+
axes = list(range(ndim))
|
|
383
|
+
else:
|
|
384
|
+
raise ValueError(
|
|
385
|
+
f"box has {box.ndim} axes; the grid has {ndim} ({len(spatial)} spatial)"
|
|
386
|
+
)
|
|
387
|
+
lo, hi = index_range(
|
|
388
|
+
box.begin,
|
|
389
|
+
box.end,
|
|
390
|
+
[self.voxel_size[a] for a in axes],
|
|
391
|
+
[self.translation[a] for a in axes],
|
|
392
|
+
)
|
|
393
|
+
begin, end = [0] * ndim, list(self.shape)
|
|
394
|
+
for a, b, e in zip(axes, lo, hi):
|
|
395
|
+
begin[a], end[a] = b, e
|
|
396
|
+
return tuple(begin), tuple(end)
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def write_shape(self) -> tuple[int, ...] | None:
|
|
400
|
+
"""The unit one writer may own: the shards when given, else the chunks; None when
|
|
401
|
+
the grid has neither."""
|
|
402
|
+
return self.shards if self.shards is not None else self.chunks
|
|
403
|
+
|
|
404
|
+
def chunks_covering(
|
|
405
|
+
self, begin: Sequence[int], end: Sequence[int], write: bool = False
|
|
406
|
+
) -> Iterator[tuple[int, ...]]:
|
|
407
|
+
"""The chunk indices a voxel box touches, clipped to the grid; ``write=True`` on the
|
|
408
|
+
write-unit grid."""
|
|
409
|
+
cs = self.write_shape if write else self.chunks
|
|
410
|
+
if cs is None:
|
|
411
|
+
raise ValueError(f"this grid has no {'write_shape' if write else 'chunks'}")
|
|
412
|
+
first = [max(0, int(b)) // c for b, c in zip(begin, cs)]
|
|
413
|
+
last = [(min(int(e), n) - 1) // c for e, n, c in zip(end, self.shape, cs)]
|
|
414
|
+
if any(hi < lo for lo, hi in zip(first, last)):
|
|
415
|
+
return iter(())
|
|
416
|
+
return (
|
|
417
|
+
tuple(int(i + lo) for i, lo in zip(idx, first))
|
|
418
|
+
for idx in itertools.product(*(range(hi - lo + 1) for lo, hi in zip(first, last)))
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def blocks(self, block_shape=None, context=0, box=None, clip: bool = True) -> Blocks:
|
|
422
|
+
"""``blocks`` over this grid's shape; ``block_shape`` defaults to ``write_shape``, so
|
|
423
|
+
default blocks are aligned writes."""
|
|
424
|
+
if block_shape is None:
|
|
425
|
+
block_shape = self.write_shape
|
|
426
|
+
if block_shape is None:
|
|
427
|
+
raise ValueError("this grid has no chunks or shards; pass block_shape")
|
|
428
|
+
return Blocks(self.shape, block_shape, context, box, clip)
|
|
429
|
+
|
|
430
|
+
def meta(self, dtype) -> ArrayMeta:
|
|
431
|
+
"""The ``ArrayMeta`` an array created from this grid reports, placed by OME."""
|
|
432
|
+
return ArrayMeta(
|
|
433
|
+
shape=self.shape,
|
|
434
|
+
dtype=dtype,
|
|
435
|
+
chunk_shape=self.chunks if self.chunks is not None else self.shape,
|
|
436
|
+
axes=self.axes,
|
|
437
|
+
axes_named=True,
|
|
438
|
+
units=self.units,
|
|
439
|
+
voxel_size=self.voxel_size,
|
|
440
|
+
translation=self.translation,
|
|
441
|
+
format=None,
|
|
442
|
+
path="",
|
|
443
|
+
placed_by="ome",
|
|
444
|
+
shard_shape=self.shards,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# ---------------------------------------------------------------------------
|
|
449
|
+
# Schedules
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def level_factors(
|
|
453
|
+
voxel_size: Sequence[float],
|
|
454
|
+
ratio: float = 1.5,
|
|
455
|
+
max_levels: int | None = None,
|
|
456
|
+
stop_shape: Sequence[int] | None = None,
|
|
457
|
+
shape: Sequence[int] | None = None,
|
|
458
|
+
) -> list[tuple[int, ...]]:
|
|
459
|
+
"""Per level, the per-axis downsampling factors from the level before: 2 on every axis
|
|
460
|
+
whose current voxel size is within ``ratio`` of the smallest current one, 1 elsewhere,
|
|
461
|
+
compounding level over level, until ``max_levels`` levels or until a level's shape
|
|
462
|
+
(``shape`` ceil-divided by the cumulative factors) fits within ``stop_shape`` on every
|
|
463
|
+
axis. Voxel size (30, 8, 8) gives (1, 2, 2), (1, 2, 2), then (2, 2, 2) on: z waits until
|
|
464
|
+
y and x reach 32. Isotropic data gives all 2s (mesh-n-bone's schedule, Behaviour 19)."""
|
|
465
|
+
current = [float(v) for v in voxel_size]
|
|
466
|
+
n = len(current)
|
|
467
|
+
if n == 0 or any(v <= 0 for v in current):
|
|
468
|
+
raise ValueError(f"voxel_size must be positive, got {tuple(voxel_size)}")
|
|
469
|
+
if ratio < 1:
|
|
470
|
+
raise ValueError(f"ratio must be at least 1, got {ratio}")
|
|
471
|
+
if max_levels is None and stop_shape is None:
|
|
472
|
+
raise ValueError("pass max_levels, or stop_shape with shape")
|
|
473
|
+
if max_levels is not None and max_levels < 0:
|
|
474
|
+
raise ValueError(f"max_levels cannot be negative, got {max_levels}")
|
|
475
|
+
if stop_shape is not None:
|
|
476
|
+
if shape is None:
|
|
477
|
+
raise ValueError("stop_shape needs shape")
|
|
478
|
+
stop_shape = _sized(stop_shape, "stop_shape", n, 1)
|
|
479
|
+
shape = _sized(shape, "shape", n, 0)
|
|
480
|
+
cumulative = [1] * n
|
|
481
|
+
out: list[tuple[int, ...]] = []
|
|
482
|
+
while max_levels is None or len(out) < max_levels:
|
|
483
|
+
if stop_shape is not None and all(
|
|
484
|
+
-(-s // c) <= t for s, c, t in zip(shape, cumulative, stop_shape)
|
|
485
|
+
):
|
|
486
|
+
break
|
|
487
|
+
limit = ratio * min(current) * (1 + RTOL)
|
|
488
|
+
step = tuple(2 if v <= limit else 1 for v in current)
|
|
489
|
+
out.append(step)
|
|
490
|
+
current = [v * f for v, f in zip(current, step)]
|
|
491
|
+
cumulative = [c * f for c, f in zip(cumulative, step)]
|
|
492
|
+
return out
|
worldarray/store.py
CHANGED
|
@@ -77,10 +77,15 @@ GUARD_GCE_METADATA = True # see kvstore_specs
|
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
def basename(path: str) -> str:
|
|
80
|
+
"""The last component of a path or URL (``s3://b/a/s0`` gives ``s0``); a trailing slash
|
|
81
|
+
is dropped first."""
|
|
80
82
|
return normalize_path(path).rsplit("/", 1)[-1]
|
|
81
83
|
|
|
82
84
|
|
|
83
85
|
def join(base: str, *parts: str) -> str:
|
|
86
|
+
"""``base`` and ``parts`` joined by single slashes, keeping a URL's scheme
|
|
87
|
+
(``join("s3://b/a/", "s0")`` gives ``s3://b/a/s0``). Public, with ``parent`` and
|
|
88
|
+
``basename``; like them it normalises ``base`` first (a local path made absolute)."""
|
|
84
89
|
return "/".join([normalize_path(base).rstrip("/"), *(p.strip("/") for p in parts)])
|
|
85
90
|
|
|
86
91
|
|
worldarray/units.py
CHANGED
|
@@ -77,6 +77,27 @@ def factor(
|
|
|
77
77
|
return a[1] / b[1]
|
|
78
78
|
|
|
79
79
|
|
|
80
|
+
# The OME short form of each unit, by its full name; every spelling of a unit reaches its
|
|
81
|
+
# entry through the table above, so "nanometre", "nm" and "Nanometers" agree.
|
|
82
|
+
_SHORT = {
|
|
83
|
+
"picometer": "pm", "angstrom": "Å", "nanometer": "nm", "micrometer": "µm",
|
|
84
|
+
"millimeter": "mm", "centimeter": "cm", "meter": "m",
|
|
85
|
+
"nanosecond": "ns", "microsecond": "µs", "millisecond": "ms", "second": "s",
|
|
86
|
+
"minute": "min", "hour": "h", "day": "d",
|
|
87
|
+
} # fmt: skip
|
|
88
|
+
_SHORT_BY_SIZE = {_lookup(name): short for name, short in _SHORT.items()}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def abbreviation(unit: str) -> str:
|
|
92
|
+
"""The OME short form of a unit: ``"nm"`` for any spelling of nanometre the table knows,
|
|
93
|
+
``"µm"`` (U+00B5) for any spelling of micrometre, ``"Å"``, ``"s"``, ``"min"`` and so
|
|
94
|
+
on. A unit the table does not know (``""``, ``"pixel"``) is a ``ValueError``."""
|
|
95
|
+
found = _lookup(unit) if unit else None
|
|
96
|
+
if found is None or found not in _SHORT_BY_SIZE:
|
|
97
|
+
raise ValueError(f"unknown unit {unit!r}")
|
|
98
|
+
return _SHORT_BY_SIZE[found]
|
|
99
|
+
|
|
100
|
+
|
|
80
101
|
# OME-NGFF's name for each unit the table knows, by (kind, size): every spelling of a unit
|
|
81
102
|
# reaches its entry through the table above, so "nanometre", "nm" and "Nanometers" agree.
|
|
82
103
|
# The American spelling is OME's.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: worldarray
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Read zarr, N5 and Neuroglancer precomputed volumes in world coordinates through tensorstore: correct voxel placement, pyramid levels, padded region reads, resampling. numpy + tensorstore only.
|
|
5
5
|
Project-URL: Homepage, https://janeliascicomp.github.io/worldarray/
|
|
6
6
|
Project-URL: Documentation, https://janeliascicomp.github.io/worldarray/
|
|
@@ -156,6 +156,11 @@ s1 = wa.open_pyramid("out.zarr", mode="r+").add_level("s1", factors=2) # place
|
|
|
156
156
|
and only two answers to "it exists": raise, or adopt an identical array after checking;
|
|
157
157
|
whole-unit writes from many processes with no coordination; `add_level` as metadata plus
|
|
158
158
|
an empty array; chunk keys for every format. Nothing is ever deleted or overwritten.
|
|
159
|
+
- **Plans outputs and blocks** (0.3): a store-less `Grid` with the same geometry as an
|
|
160
|
+
array, so an output is planned, cut into blocks and only then created; `blocks` tiling an
|
|
161
|
+
array or a box into write boxes with context-grown read boxes, in C order, as plain
|
|
162
|
+
picklable records a cluster job takes by id; the anisotropy-aware pyramid schedule.
|
|
163
|
+
Nothing is scheduled or run.
|
|
159
164
|
|
|
160
165
|
## The placement rule
|
|
161
166
|
|
|
@@ -202,6 +207,14 @@ wa.ome_multiscales(axes, units, levels, version="0.5", type=None) ; wa.level_pla
|
|
|
202
207
|
wa.level_shape(shape, factors, origin=0) ; wa.chunk_key(index, format, ...) ; wa.parse_chunk_key(key, format, ...)
|
|
203
208
|
wa.ensure_group_entry(group_path, name, voxel_size, translation, axes, units="nanometer") # list an array you made yourself
|
|
204
209
|
wa.units.ome_name("nm") -> "nanometer" ; wa.write.codecs_of(path) -> (codecs, shards)
|
|
210
|
+
|
|
211
|
+
# grids and blocks (0.3)
|
|
212
|
+
g = wa.Grid(shape, voxel_size, translation, axes=None, units="nanometer", chunks=None, shards=None) # or Grid.from_meta(meta, voxel_size=)
|
|
213
|
+
wa.create(path, grid=g, dtype="uint64")
|
|
214
|
+
for b in wa.blocks(shape, block_shape, context=0, box=None, clip=True): # also vol.blocks() and g.blocks()
|
|
215
|
+
b.begin, b.end, b.read_begin, b.read_end, b.inner, b.at_end, b.id # plain records; blocks[id] is O(1)
|
|
216
|
+
wa.level_factors(voxel_size, ratio=1.5, max_levels=None, stop_shape=None, shape=None)
|
|
217
|
+
wa.units.abbreviation("nanometer") ; wa.store.join(path, "s0") ; wa.store.parent(path) ; wa.store.basename(path)
|
|
205
218
|
```
|
|
206
219
|
|
|
207
220
|
`WorldArray`: `ts`, `meta`, `grid`, `shape`, `dtype`, `axes`, `voxel_size`, `translation`,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
worldarray/__init__.py,sha256=_g8gaopTTDUXz_vfEqaWzSEXSf3hflzNW6zjCLpiX-A,1828
|
|
2
|
+
worldarray/array.py,sha256=1lmdq-Mi5Nu4wWJ3d6Ek5ZeA0-QmJJ6JgHdsQG7ud3k,56051
|
|
3
|
+
worldarray/context.py,sha256=MZQoHLyir1xNYCHtQPGTIQXaFzFr3oAcyfddrE_UWIA,3284
|
|
4
|
+
worldarray/geometry.py,sha256=ByQu8WrcUydKU2eDsugKwvx1LpQbETmhutfy5mv9inI,7449
|
|
5
|
+
worldarray/grid.py,sha256=ATa6rcWygQ9Pigvqftz1tV_0U6enA6xF4WA_Y2ubD6Y,22313
|
|
6
|
+
worldarray/levels.py,sha256=ICtax6A5Qi7DDlZI2S5UPg-S8Ze-AZoMxXyRykcs36A,6414
|
|
7
|
+
worldarray/placement.py,sha256=l7kPkS6GatjoD_Eb7pJqkkxsHLKLUr5gA9JrxeMsnxo,34140
|
|
8
|
+
worldarray/resample.py,sha256=nAls-m0vchrsua0q_1yx1K9-djlAnWtkgokCfXqbpIQ,10411
|
|
9
|
+
worldarray/retry.py,sha256=Uw4YP-tPwtg0tgpBzY0ux4F5vehdTZTp0385QH_vvrk,2766
|
|
10
|
+
worldarray/store.py,sha256=432iIX35ak_cHhibHcZd80ehH0MgN35rtxOtYU35LYg,19154
|
|
11
|
+
worldarray/units.py,sha256=02i7kpkPjB3HJ2-7ZAvEXLjv6j0mmfiYZjpUNwFDvz4,5238
|
|
12
|
+
worldarray/write.py,sha256=IjV2jtwSBBAcWKHfms9KPUALX5GDs7v-2FRpTMO2Gfo,33442
|
|
13
|
+
worldarray-0.3.0.dist-info/METADATA,sha256=lhY78UeT83ZZLyshx6u-DwSWFo-1aWqhXmaXYaKARXk,15280
|
|
14
|
+
worldarray-0.3.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
15
|
+
worldarray-0.3.0.dist-info/licenses/LICENSE,sha256=NsviVkqc3M7YUSeSNmisDSg3e3L8S4ofeIjthWm8pvk,1518
|
|
16
|
+
worldarray-0.3.0.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
worldarray/__init__.py,sha256=NEoYvsC31iUBYD0cKQrcEgBYq_DHaRtL09yXYNdfR08,1560
|
|
2
|
-
worldarray/array.py,sha256=Wh6-oW8UN-yVBurGCZjmkrvjTrzTiD0aBTfn7UzsaJE,53722
|
|
3
|
-
worldarray/context.py,sha256=MZQoHLyir1xNYCHtQPGTIQXaFzFr3oAcyfddrE_UWIA,3284
|
|
4
|
-
worldarray/geometry.py,sha256=ByQu8WrcUydKU2eDsugKwvx1LpQbETmhutfy5mv9inI,7449
|
|
5
|
-
worldarray/levels.py,sha256=ICtax6A5Qi7DDlZI2S5UPg-S8Ze-AZoMxXyRykcs36A,6414
|
|
6
|
-
worldarray/placement.py,sha256=l7kPkS6GatjoD_Eb7pJqkkxsHLKLUr5gA9JrxeMsnxo,34140
|
|
7
|
-
worldarray/resample.py,sha256=nAls-m0vchrsua0q_1yx1K9-djlAnWtkgokCfXqbpIQ,10411
|
|
8
|
-
worldarray/retry.py,sha256=Uw4YP-tPwtg0tgpBzY0ux4F5vehdTZTp0385QH_vvrk,2766
|
|
9
|
-
worldarray/store.py,sha256=fYLKiy-w8Aa0GxJC8lFCAXx3Etq_2R7i_hyHQxefpyM,18783
|
|
10
|
-
worldarray/units.py,sha256=Ma4O6QIvG9RolCVu8YEFT2IQSANmuUWOBb0NOR3Dr6k,4213
|
|
11
|
-
worldarray/write.py,sha256=IjV2jtwSBBAcWKHfms9KPUALX5GDs7v-2FRpTMO2Gfo,33442
|
|
12
|
-
worldarray-0.2.0.dist-info/METADATA,sha256=yAZBBAe_9mfdnK1TbDGKng0PwI7CKkR0CiukEk3dTeU,14274
|
|
13
|
-
worldarray-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
14
|
-
worldarray-0.2.0.dist-info/licenses/LICENSE,sha256=NsviVkqc3M7YUSeSNmisDSg3e3L8S4ofeIjthWm8pvk,1518
|
|
15
|
-
worldarray-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|