funlib.persistence 0.6.1__tar.gz → 0.8.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.
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/PKG-INFO +7 -8
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/README.md +1 -1
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/pyproject.toml +6 -12
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/__init__.py +1 -1
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/arrays/array.py +163 -28
- funlib_persistence-0.8.0/src/funlib/persistence/arrays/datasets.py +576 -0
- funlib_persistence-0.8.0/src/funlib/persistence/arrays/lazy_ops.py +6 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/arrays/metadata.py +31 -29
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/arrays/ome_datasets.py +6 -6
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/graphs/graph_database.py +69 -3
- funlib_persistence-0.8.0/src/funlib/persistence/graphs/pgsql_graph_database.py +390 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/graphs/sql_graph_database.py +288 -118
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/graphs/sqlite_graph_database.py +96 -17
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib.persistence.egg-info/PKG-INFO +7 -8
- funlib_persistence-0.8.0/src/funlib.persistence.egg-info/SOURCES.txt +31 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib.persistence.egg-info/requires.txt +4 -5
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/tests/test_array.py +97 -1
- funlib_persistence-0.8.0/tests/test_datasets.py +995 -0
- funlib_persistence-0.8.0/tests/test_graph.py +855 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/tests/test_metadata.py +4 -4
- funlib_persistence-0.8.0/tests/test_pgsql_graph_database.py +91 -0
- funlib_persistence-0.6.1/funlib/persistence/arrays/datasets.py +0 -383
- funlib_persistence-0.6.1/funlib/persistence/arrays/lazy_ops.py +0 -5
- funlib_persistence-0.6.1/funlib/persistence/graphs/pgsql_graph_database.py +0 -206
- funlib_persistence-0.6.1/funlib.persistence.egg-info/SOURCES.txt +0 -30
- funlib_persistence-0.6.1/tests/test_datasets.py +0 -292
- funlib_persistence-0.6.1/tests/test_graph.py +0 -476
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/LICENSE +0 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/setup.cfg +0 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/arrays/__init__.py +0 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/arrays/freezable.py +0 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/arrays/utils.py +0 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/graphs/__init__.py +0 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/py.typed +0 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/types.py +0 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib.persistence.egg-info/dependency_links.txt +0 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib.persistence.egg-info/top_level.txt +0 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/tests/test_config.py +0 -0
- {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/tests/test_slices.py +0 -0
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: funlib.persistence
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Interfaces for data (arrays and graphs) and storage formats (databases and file formats)
|
|
5
5
|
Author-email: William Patton <pattonw@hhmi.org>, Jan Funke <funkej@hhmi.org>
|
|
6
6
|
License: MIT
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: zarr<3
|
|
12
|
-
Requires-Dist:
|
|
13
|
-
Requires-Dist: iohub>=0.2.0b0
|
|
11
|
+
Requires-Dist: zarr<4,>=3
|
|
12
|
+
Requires-Dist: iohub>=0.3.0a5
|
|
14
13
|
Requires-Dist: funlib.geometry>=0.3.0
|
|
15
14
|
Requires-Dist: networkx>=3.0.0
|
|
16
15
|
Requires-Dist: pymongo>=4.0.0
|
|
@@ -18,13 +17,13 @@ Requires-Dist: numpy>=2.0.0
|
|
|
18
17
|
Requires-Dist: pydantic>=2.0.0
|
|
19
18
|
Requires-Dist: dask>=2024.0.0
|
|
20
19
|
Requires-Dist: toml>=0.10.0
|
|
21
|
-
Requires-Dist: psycopg2-binary>=2.9.
|
|
20
|
+
Requires-Dist: psycopg2-binary>=2.9.11
|
|
22
21
|
Provides-Extra: dev
|
|
23
22
|
Requires-Dist: coverage>=7.7.1; extra == "dev"
|
|
24
|
-
Requires-Dist: mypy>=1.15.0; extra == "dev"
|
|
25
23
|
Requires-Dist: pytest>=8.3.5; extra == "dev"
|
|
26
24
|
Requires-Dist: pytest-mock>=3.14.0; extra == "dev"
|
|
27
25
|
Requires-Dist: ruff>=0.11.2; extra == "dev"
|
|
26
|
+
Requires-Dist: ty>=0.0.16; extra == "dev"
|
|
28
27
|
Requires-Dist: types-networkx; extra == "dev"
|
|
29
28
|
Requires-Dist: types-psycopg2; extra == "dev"
|
|
30
29
|
Requires-Dist: types-toml; extra == "dev"
|
|
@@ -32,7 +31,7 @@ Dynamic: license-file
|
|
|
32
31
|
|
|
33
32
|
[](https://github.com/funkelab/funlib.persistence/actions/workflows/tests.yaml)
|
|
34
33
|
[](https://github.com/funkelab/funlib.persistence/actions/workflows/ruff.yaml)
|
|
35
|
-
[](https://github.com/funkelab/funlib.persistence/actions/workflows/ty.yaml)
|
|
36
35
|
[](https://pypi.org/project/funlib.persistence/)
|
|
37
36
|
|
|
38
37
|
# funlib.persistence
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[](https://github.com/funkelab/funlib.persistence/actions/workflows/tests.yaml)
|
|
2
2
|
[](https://github.com/funkelab/funlib.persistence/actions/workflows/ruff.yaml)
|
|
3
|
-
[](https://github.com/funkelab/funlib.persistence/actions/workflows/ty.yaml)
|
|
4
4
|
[](https://pypi.org/project/funlib.persistence/)
|
|
5
5
|
|
|
6
6
|
# funlib.persistence
|
|
@@ -13,17 +13,13 @@ authors = [
|
|
|
13
13
|
]
|
|
14
14
|
dynamic = ['version']
|
|
15
15
|
|
|
16
|
-
requires-python = ">=3.
|
|
16
|
+
requires-python = ">=3.11"
|
|
17
17
|
classifiers = ["Programming Language :: Python :: 3"]
|
|
18
18
|
keywords = []
|
|
19
19
|
|
|
20
20
|
dependencies = [
|
|
21
|
-
"zarr>=
|
|
22
|
-
|
|
23
|
-
# We can pin zarr to >2.18.7 but then we have to drop python 3.10
|
|
24
|
-
# pin numcodecs to avoid breaking change
|
|
25
|
-
"numcodecs>0.13,<0.16.0",
|
|
26
|
-
"iohub>=0.2.0b0",
|
|
21
|
+
"zarr>=3,<4",
|
|
22
|
+
"iohub>=0.3.0a5",
|
|
27
23
|
"funlib.geometry>=0.3.0",
|
|
28
24
|
"networkx>=3.0.0",
|
|
29
25
|
"pymongo>=4.0.0",
|
|
@@ -31,7 +27,7 @@ dependencies = [
|
|
|
31
27
|
"pydantic>=2.0.0",
|
|
32
28
|
"dask>=2024.0.0",
|
|
33
29
|
"toml>=0.10.0",
|
|
34
|
-
"psycopg2-binary>=2.9.
|
|
30
|
+
"psycopg2-binary>=2.9.11",
|
|
35
31
|
]
|
|
36
32
|
|
|
37
33
|
[tool.setuptools.dynamic]
|
|
@@ -40,10 +36,10 @@ version = { attr = "funlib.persistence.__version__" }
|
|
|
40
36
|
[project.optional-dependencies]
|
|
41
37
|
dev = [
|
|
42
38
|
"coverage>=7.7.1",
|
|
43
|
-
"mypy>=1.15.0",
|
|
44
39
|
"pytest>=8.3.5",
|
|
45
40
|
"pytest-mock>=3.14.0",
|
|
46
41
|
"ruff>=0.11.2",
|
|
42
|
+
"ty>=0.0.16",
|
|
47
43
|
"types-networkx",
|
|
48
44
|
"types-psycopg2",
|
|
49
45
|
"types-toml",
|
|
@@ -56,10 +52,8 @@ lint.select = ["F", "W", "I001"]
|
|
|
56
52
|
[tool.setuptools.package-data]
|
|
57
53
|
"funlib.persistence" = ["py.typed"]
|
|
58
54
|
|
|
59
|
-
[tool.mypy]
|
|
60
|
-
explicit_package_bases = true
|
|
61
|
-
|
|
62
55
|
# # module specific overrides
|
|
63
56
|
[[tool.mypy.overrides]]
|
|
64
57
|
module = ["zarr.*", "iohub.*"]
|
|
65
58
|
ignore_missing_imports = true
|
|
59
|
+
|
{funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/arrays/array.py
RENAMED
|
@@ -6,9 +6,8 @@ from typing import Any, Optional, Union
|
|
|
6
6
|
import dask.array as da
|
|
7
7
|
import numpy as np
|
|
8
8
|
from dask.array.optimization import fuse_slice
|
|
9
|
-
from zarr import Array as ZarrArray
|
|
10
|
-
|
|
11
9
|
from funlib.geometry import Coordinate, Roi
|
|
10
|
+
from zarr import Array as ZarrArray
|
|
12
11
|
|
|
13
12
|
from .freezable import Freezable
|
|
14
13
|
from .lazy_ops import LazyOp
|
|
@@ -75,6 +74,18 @@ class Array(Freezable):
|
|
|
75
74
|
offset, voxel_size, axis_names, units, and types. Metadata
|
|
76
75
|
can either be passed in or read from array attributes.
|
|
77
76
|
|
|
77
|
+
scheduler (``Optional[str]``):
|
|
78
|
+
|
|
79
|
+
The dask scheduler used to compute reads (`__getitem__` and
|
|
80
|
+
`to_ndarray`); writes do not go through dask. `None` (the default)
|
|
81
|
+
leaves the choice to dask, which uses its threaded scheduler for
|
|
82
|
+
arrays. Pass `"single-threaded"` when the caller already
|
|
83
|
+
parallelises array access (one process or thread per block, as
|
|
84
|
+
daisy does), where dask's own thread pool only adds contention
|
|
85
|
+
and can deadlock. See
|
|
86
|
+
https://docs.dask.org/en/stable/scheduler-overview.html for the
|
|
87
|
+
available schedulers.
|
|
88
|
+
|
|
78
89
|
"""
|
|
79
90
|
|
|
80
91
|
data: da.Array
|
|
@@ -90,7 +101,9 @@ class Array(Freezable):
|
|
|
90
101
|
chunks: Optional[Union[int, Sequence[int], str]] = "auto",
|
|
91
102
|
lazy_op: Optional[LazyOp] = None,
|
|
92
103
|
strict_metadata: bool = False,
|
|
104
|
+
scheduler: Optional[str] = None,
|
|
93
105
|
):
|
|
106
|
+
self.scheduler = scheduler
|
|
94
107
|
if not isinstance(data, da.Array):
|
|
95
108
|
self.data = da.from_array(data, chunks=chunks)
|
|
96
109
|
else:
|
|
@@ -111,11 +124,13 @@ class Array(Freezable):
|
|
|
111
124
|
# only used if not reading from zarr and there is no built in `.attrs`
|
|
112
125
|
self._attrs: dict[str, Any] = {}
|
|
113
126
|
|
|
127
|
+
# whether the view's chunks still sit on the stored chunk grid (see
|
|
128
|
+
# `_keeps_chunk_grid`); `chunk_shape` is reported only while they do
|
|
129
|
+
self._on_chunk_grid = True
|
|
130
|
+
self.lazy_ops: list[LazyOp] = []
|
|
114
131
|
if lazy_op is not None:
|
|
115
132
|
self.apply_lazy_ops(lazy_op)
|
|
116
|
-
|
|
117
|
-
lazy_ops = [] if lazy_op is None else [lazy_op]
|
|
118
|
-
self.lazy_ops = lazy_ops
|
|
133
|
+
self.lazy_ops = [lazy_op]
|
|
119
134
|
|
|
120
135
|
self.freeze()
|
|
121
136
|
|
|
@@ -134,8 +149,66 @@ class Array(Freezable):
|
|
|
134
149
|
return self._attrs
|
|
135
150
|
|
|
136
151
|
@property
|
|
137
|
-
def chunk_shape(self) -> Coordinate:
|
|
138
|
-
|
|
152
|
+
def chunk_shape(self) -> Optional[Coordinate]:
|
|
153
|
+
"""The write-safe chunk shape: the shard shape if sharded, else the chunk
|
|
154
|
+
shape (an ``open_ds(chunks=...)`` override is reported as given). ``None``
|
|
155
|
+
for a view that is not writeable or that lazy operations moved off the
|
|
156
|
+
stored chunk grid."""
|
|
157
|
+
if not self.is_writeable or not self._on_chunk_grid:
|
|
158
|
+
return None
|
|
159
|
+
shards = self.shard_shape
|
|
160
|
+
return shards if shards is not None else self._dask_chunk_shape()
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def inner_chunk_shape(self) -> Optional[Coordinate]:
|
|
164
|
+
"""Chunk shape for efficient reads. Can be smaller than the write-safe
|
|
165
|
+
:attr:`chunk_shape` in the case of sharding. ``None`` for a view that
|
|
166
|
+
lazy operations moved off the stored chunk grid."""
|
|
167
|
+
return self._dask_chunk_shape() if self._on_chunk_grid else None
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def shard_shape(self) -> Optional[Coordinate]:
|
|
171
|
+
"""The shard shape in the case of sharded datasets. ``None`` for non
|
|
172
|
+
sharded or unwritable datasets, or off the stored chunk grid. See
|
|
173
|
+
https://zarr.readthedocs.io/en/stable/user-guide/performance.html#sharding
|
|
174
|
+
"""
|
|
175
|
+
shards = getattr(self._source_data, "shards", None)
|
|
176
|
+
if shards is None or not self.is_writeable or not self._on_chunk_grid:
|
|
177
|
+
return None
|
|
178
|
+
return Coordinate(
|
|
179
|
+
s for s, uncollapsed in zip(shards, self._uncollapsed_dims) if uncollapsed
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def _dask_chunk_shape(self) -> Coordinate:
|
|
183
|
+
return Coordinate(self.data.chunksize) # ty: ignore[unresolved-attribute]
|
|
184
|
+
|
|
185
|
+
def _keeps_chunk_grid(self, lazy_op) -> bool:
|
|
186
|
+
"""Whether a lazy op leaves this view's chunks on the stored chunk grid:
|
|
187
|
+
dropping or truncating axes does, a start, a step, a selection list, a
|
|
188
|
+
mask or an arbitrary callable does not. A ``Roi`` does if it begins on
|
|
189
|
+
a chunk boundary."""
|
|
190
|
+
if isinstance(lazy_op, Roi):
|
|
191
|
+
if (
|
|
192
|
+
not all(self.uncollapsed_dims(physical=True))
|
|
193
|
+
or lazy_op.dims != self.spatial_dims
|
|
194
|
+
):
|
|
195
|
+
return True # apply_lazy_ops refuses the op, with its own message
|
|
196
|
+
unit = self.chunk_shape
|
|
197
|
+
if unit is None:
|
|
198
|
+
return False
|
|
199
|
+
physical = [c for c, t in zip(unit, self.types) if t in ["space", "time"]]
|
|
200
|
+
begin = (lazy_op.offset - self.offset) / self.voxel_size
|
|
201
|
+
return all(b % c == 0 for b, c in zip(begin, physical))
|
|
202
|
+
if not isinstance(lazy_op, tuple):
|
|
203
|
+
lazy_op = (lazy_op,)
|
|
204
|
+
for a in lazy_op:
|
|
205
|
+
if isinstance(a, int):
|
|
206
|
+
continue
|
|
207
|
+
if isinstance(a, slice):
|
|
208
|
+
if a.start in (None, 0) and a.step in (None, 1):
|
|
209
|
+
continue
|
|
210
|
+
return False
|
|
211
|
+
return True
|
|
139
212
|
|
|
140
213
|
def uncollapsed_dims(self, physical: bool = False) -> list[bool]:
|
|
141
214
|
"""
|
|
@@ -254,17 +327,18 @@ class Array(Freezable):
|
|
|
254
327
|
)
|
|
255
328
|
|
|
256
329
|
def apply_lazy_ops(self, lazy_op):
|
|
330
|
+
# decided before the op moves the offset, recorded only once it applied
|
|
331
|
+
on_chunk_grid = self._on_chunk_grid and self._keeps_chunk_grid(lazy_op)
|
|
257
332
|
if self._is_slice(lazy_op):
|
|
258
333
|
if not isinstance(lazy_op, tuple):
|
|
259
334
|
lazy_op = (lazy_op,)
|
|
260
|
-
|
|
335
|
+
# Pair each entry with the dimension it indexes BEFORE collapsing any: the
|
|
336
|
+
# previous in-place walk re-counted uncollapsed dims after each collapse,
|
|
337
|
+
# so a tuple with two or more ints collapsed the wrong dimensions.
|
|
338
|
+
dims = [i for i, uc in enumerate(self._uncollapsed_dims) if uc]
|
|
339
|
+
for a, i in zip(lazy_op, dims):
|
|
261
340
|
if isinstance(a, int):
|
|
262
|
-
|
|
263
|
-
if uc:
|
|
264
|
-
if ii == 0:
|
|
265
|
-
self._uncollapsed_dims[i] = False
|
|
266
|
-
break
|
|
267
|
-
ii -= 1
|
|
341
|
+
self._uncollapsed_dims[i] = False
|
|
268
342
|
self.data = self.data[lazy_op]
|
|
269
343
|
elif isinstance(lazy_op, Roi):
|
|
270
344
|
assert all(self.uncollapsed_dims(physical=True)), (
|
|
@@ -283,6 +357,7 @@ class Array(Freezable):
|
|
|
283
357
|
f"LazyOp {lazy_op} is not a supported lazy_op. "
|
|
284
358
|
f"Supported lazy_ops are: {LazyOp}"
|
|
285
359
|
)
|
|
360
|
+
self._on_chunk_grid = on_chunk_grid
|
|
286
361
|
|
|
287
362
|
def lazy_op(self, lazy_op: LazyOp):
|
|
288
363
|
"""Apply an lazy operation to this array.
|
|
@@ -330,7 +405,9 @@ class Array(Freezable):
|
|
|
330
405
|
% (roi, self.roi)
|
|
331
406
|
)
|
|
332
407
|
|
|
333
|
-
return self.data[self.__slices(roi, use_lazy_slices=False)].compute(
|
|
408
|
+
return self.data[self.__slices(roi, use_lazy_slices=False)].compute(
|
|
409
|
+
scheduler=self.scheduler
|
|
410
|
+
)
|
|
334
411
|
|
|
335
412
|
elif isinstance(key, Coordinate):
|
|
336
413
|
coordinate = key
|
|
@@ -339,23 +416,26 @@ class Array(Freezable):
|
|
|
339
416
|
raise IndexError("Requested coordinate is not contained in this array.")
|
|
340
417
|
|
|
341
418
|
index = self.__index(coordinate)
|
|
342
|
-
return self.data[index].compute()
|
|
419
|
+
return self.data[index].compute(scheduler=self.scheduler)
|
|
343
420
|
|
|
344
421
|
else:
|
|
345
|
-
return self.data[key].compute()
|
|
422
|
+
return self.data[key].compute(scheduler=self.scheduler)
|
|
346
423
|
|
|
347
|
-
def __setitem__(self, key, value: np.ndarray):
|
|
348
|
-
"""Set the data of this array
|
|
424
|
+
def __setitem__(self, key: Roi | slice | tuple, value: np.ndarray | float | int):
|
|
425
|
+
"""Set the data of this array.
|
|
349
426
|
|
|
350
427
|
Args:
|
|
351
428
|
|
|
352
|
-
key (`class:Roi`):
|
|
429
|
+
key (`class:Roi` or any numpy compatible key):
|
|
353
430
|
|
|
354
|
-
The
|
|
431
|
+
The region to write to. Can be a `Roi` for world-unit indexing,
|
|
432
|
+
or any numpy-compatible key (e.g. ``np.s_[:]``, a slice, a tuple
|
|
433
|
+
of slices).
|
|
355
434
|
|
|
356
|
-
value (``ndarray``):
|
|
435
|
+
value (``ndarray`` or scalar):
|
|
357
436
|
|
|
358
|
-
The value to write.
|
|
437
|
+
The value to write. Can be a numpy array or a scalar that will
|
|
438
|
+
be broadcast.
|
|
359
439
|
"""
|
|
360
440
|
|
|
361
441
|
if self.is_writeable:
|
|
@@ -419,7 +499,7 @@ class Array(Freezable):
|
|
|
419
499
|
if fill_value != 0:
|
|
420
500
|
data[:] = fill_value
|
|
421
501
|
|
|
422
|
-
array = Array(data, roi.offset, self.voxel_size)
|
|
502
|
+
array = Array(data, roi.offset, self.voxel_size, scheduler=self.scheduler)
|
|
423
503
|
|
|
424
504
|
shared_roi = self.roi.intersect(roi)
|
|
425
505
|
|
|
@@ -436,18 +516,20 @@ class Array(Freezable):
|
|
|
436
516
|
voxel_roi = (roi - self.offset) / self.voxel_size
|
|
437
517
|
|
|
438
518
|
if check_chunk_align:
|
|
519
|
+
chunk_shape = self.chunk_shape
|
|
520
|
+
assert chunk_shape is not None, "this view has no chunk grid to align to"
|
|
439
521
|
for d in range(roi.dims):
|
|
440
522
|
end_of_array = roi.get_end()[d] == self.roi.get_end()[d]
|
|
441
523
|
|
|
442
|
-
begin_align_with_chunks = voxel_roi.begin[d] %
|
|
443
|
-
shape_align_with_chunks = voxel_roi.shape[d] %
|
|
524
|
+
begin_align_with_chunks = voxel_roi.begin[d] % chunk_shape[d] == 0
|
|
525
|
+
shape_align_with_chunks = voxel_roi.shape[d] % chunk_shape[d] == 0
|
|
444
526
|
|
|
445
527
|
assert begin_align_with_chunks and (
|
|
446
528
|
shape_align_with_chunks or end_of_array
|
|
447
529
|
), (
|
|
448
530
|
"ROI %s (in voxels: %s) does not align with chunks of "
|
|
449
531
|
"size %s (mismatch in dimension %d)"
|
|
450
|
-
% (roi, voxel_roi,
|
|
532
|
+
% (roi, voxel_roi, chunk_shape, d)
|
|
451
533
|
)
|
|
452
534
|
|
|
453
535
|
roi_slices = tuple(
|
|
@@ -474,7 +556,7 @@ class Array(Freezable):
|
|
|
474
556
|
elif isinstance(lazy_op, list) and all([isinstance(a, int) for a in lazy_op]):
|
|
475
557
|
return True
|
|
476
558
|
elif isinstance(lazy_op, tuple) and all(
|
|
477
|
-
[self._is_slice(a, writeable) for a in lazy_op]
|
|
559
|
+
[self._is_slice(a, writeable) for a in lazy_op] # type: ignore[arg-type] # ty can't narrow parameterized tuple iteration
|
|
478
560
|
):
|
|
479
561
|
return True
|
|
480
562
|
elif (
|
|
@@ -506,3 +588,56 @@ class Array(Freezable):
|
|
|
506
588
|
f"Chunk shape ({self.chunk_shape}) must have the same "
|
|
507
589
|
f"number of dimensions as the data ({self.shape})"
|
|
508
590
|
)
|
|
591
|
+
|
|
592
|
+
def to_pixel_space(
|
|
593
|
+
self, world_loc: Roi | Coordinate | Sequence[int | float] | np.ndarray
|
|
594
|
+
) -> Roi | Coordinate | np.ndarray:
|
|
595
|
+
"""Convert a point or roi in world space into the pixel space of this array.
|
|
596
|
+
Works on sequences of floats by returning a numpy array that is not guaranteed
|
|
597
|
+
to be aligned with pixels.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
world_loc (Roi | Coordinate | Sequence[int | float]): The world space
|
|
601
|
+
point or roi to convert to pixel space
|
|
602
|
+
|
|
603
|
+
Raises:
|
|
604
|
+
ValueError: If the world location falls outside the array's roi
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
Roi | Coordinate | np.ndarray: The point or roi converted into the pixel
|
|
608
|
+
space of the array. If the input was a Coordinate or Roi, returns
|
|
609
|
+
the same type of object. If the input was a sequence of python numbers,
|
|
610
|
+
returns a numpy array with the result, which may or may not be integers.
|
|
611
|
+
"""
|
|
612
|
+
if not self.roi.contains(world_loc):
|
|
613
|
+
raise ValueError(
|
|
614
|
+
f"Given world location {world_loc} is not included in this array with world ROI {self.roi}"
|
|
615
|
+
)
|
|
616
|
+
if isinstance(world_loc, (Roi, Coordinate)):
|
|
617
|
+
return (world_loc - self.roi.offset) / self.voxel_size
|
|
618
|
+
else:
|
|
619
|
+
return (np.array(world_loc) - np.array(self.roi.offset)) / np.array(
|
|
620
|
+
self.voxel_size
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
def to_world_space(
|
|
624
|
+
self, pixel_loc: Roi | Coordinate | Sequence[int | float] | np.ndarray
|
|
625
|
+
) -> Roi | Coordinate:
|
|
626
|
+
"""Convert a point or roi from pixel space in this array to the world
|
|
627
|
+
coordinate system defined by this array's roi and voxel size.
|
|
628
|
+
Works on Coordiantes and Rois, as well as sequences of ints or floats.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
pixel_loc (Roi | Coordinate | Sequence[int | float]): A location in pixel space
|
|
632
|
+
of this array's data.
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
Roi | Coordinate: The world location of the given pixel location. If the input
|
|
636
|
+
was a Roi or Coordinate, return the same type of object. If it was a
|
|
637
|
+
sequence of python numbers, return a numpy array that may be int or float.
|
|
638
|
+
"""
|
|
639
|
+
if isinstance(pixel_loc, (Roi, Coordinate)):
|
|
640
|
+
return pixel_loc * self.voxel_size + self.roi.offset
|
|
641
|
+
return np.array(pixel_loc) * np.array(self.voxel_size) + np.array(
|
|
642
|
+
self.roi.offset
|
|
643
|
+
)
|