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.
Files changed (39) hide show
  1. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/PKG-INFO +7 -8
  2. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/README.md +1 -1
  3. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/pyproject.toml +6 -12
  4. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/__init__.py +1 -1
  5. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/arrays/array.py +163 -28
  6. funlib_persistence-0.8.0/src/funlib/persistence/arrays/datasets.py +576 -0
  7. funlib_persistence-0.8.0/src/funlib/persistence/arrays/lazy_ops.py +6 -0
  8. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/arrays/metadata.py +31 -29
  9. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/arrays/ome_datasets.py +6 -6
  10. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/graphs/graph_database.py +69 -3
  11. funlib_persistence-0.8.0/src/funlib/persistence/graphs/pgsql_graph_database.py +390 -0
  12. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/graphs/sql_graph_database.py +288 -118
  13. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/graphs/sqlite_graph_database.py +96 -17
  14. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib.persistence.egg-info/PKG-INFO +7 -8
  15. funlib_persistence-0.8.0/src/funlib.persistence.egg-info/SOURCES.txt +31 -0
  16. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib.persistence.egg-info/requires.txt +4 -5
  17. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/tests/test_array.py +97 -1
  18. funlib_persistence-0.8.0/tests/test_datasets.py +995 -0
  19. funlib_persistence-0.8.0/tests/test_graph.py +855 -0
  20. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/tests/test_metadata.py +4 -4
  21. funlib_persistence-0.8.0/tests/test_pgsql_graph_database.py +91 -0
  22. funlib_persistence-0.6.1/funlib/persistence/arrays/datasets.py +0 -383
  23. funlib_persistence-0.6.1/funlib/persistence/arrays/lazy_ops.py +0 -5
  24. funlib_persistence-0.6.1/funlib/persistence/graphs/pgsql_graph_database.py +0 -206
  25. funlib_persistence-0.6.1/funlib.persistence.egg-info/SOURCES.txt +0 -30
  26. funlib_persistence-0.6.1/tests/test_datasets.py +0 -292
  27. funlib_persistence-0.6.1/tests/test_graph.py +0 -476
  28. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/LICENSE +0 -0
  29. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/setup.cfg +0 -0
  30. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/arrays/__init__.py +0 -0
  31. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/arrays/freezable.py +0 -0
  32. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/arrays/utils.py +0 -0
  33. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/graphs/__init__.py +0 -0
  34. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/py.typed +0 -0
  35. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib/persistence/types.py +0 -0
  36. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib.persistence.egg-info/dependency_links.txt +0 -0
  37. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0/src}/funlib.persistence.egg-info/top_level.txt +0 -0
  38. {funlib_persistence-0.6.1 → funlib_persistence-0.8.0}/tests/test_config.py +0 -0
  39. {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.6.1
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.10
8
+ Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: zarr<3,>=2
12
- Requires-Dist: numcodecs<0.16.0,>0.13
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.5
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
  [![tests](https://github.com/funkelab/funlib.persistence/actions/workflows/tests.yaml/badge.svg)](https://github.com/funkelab/funlib.persistence/actions/workflows/tests.yaml)
34
33
  [![ruff](https://github.com/funkelab/funlib.persistence/actions/workflows/ruff.yaml/badge.svg)](https://github.com/funkelab/funlib.persistence/actions/workflows/ruff.yaml)
35
- [![mypy](https://github.com/funkelab/funlib.persistence/actions/workflows/mypy.yaml/badge.svg)](https://github.com/funkelab/funlib.persistence/actions/workflows/mypy.yaml)
34
+ [![ty](https://github.com/funkelab/funlib.persistence/actions/workflows/ty.yaml/badge.svg)](https://github.com/funkelab/funlib.persistence/actions/workflows/ty.yaml)
36
35
  [![pypi](https://github.com/funkelab/funlib.persistence/actions/workflows/publish.yaml/badge.svg)](https://pypi.org/project/funlib.persistence/)
37
36
 
38
37
  # funlib.persistence
@@ -1,6 +1,6 @@
1
1
  [![tests](https://github.com/funkelab/funlib.persistence/actions/workflows/tests.yaml/badge.svg)](https://github.com/funkelab/funlib.persistence/actions/workflows/tests.yaml)
2
2
  [![ruff](https://github.com/funkelab/funlib.persistence/actions/workflows/ruff.yaml/badge.svg)](https://github.com/funkelab/funlib.persistence/actions/workflows/ruff.yaml)
3
- [![mypy](https://github.com/funkelab/funlib.persistence/actions/workflows/mypy.yaml/badge.svg)](https://github.com/funkelab/funlib.persistence/actions/workflows/mypy.yaml)
3
+ [![ty](https://github.com/funkelab/funlib.persistence/actions/workflows/ty.yaml/badge.svg)](https://github.com/funkelab/funlib.persistence/actions/workflows/ty.yaml)
4
4
  [![pypi](https://github.com/funkelab/funlib.persistence/actions/workflows/publish.yaml/badge.svg)](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.10"
16
+ requires-python = ">=3.11"
17
17
  classifiers = ["Programming Language :: Python :: 3"]
18
18
  keywords = []
19
19
 
20
20
  dependencies = [
21
- "zarr>=2,<3",
22
- # ImportError: cannot import name 'cbuffer_sizes' from 'numcodecs.blosc'
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.5",
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
+
@@ -1,4 +1,4 @@
1
1
  from .arrays import Array, open_ds, prepare_ds, open_ome_ds, prepare_ome_ds # noqa
2
2
 
3
- __version__ = "0.6.1"
3
+ __version__ = "0.8.0"
4
4
  __version_info__ = tuple(int(i) for i in __version__.split("."))
@@ -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
- return Coordinate(self.data.chunksize)
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
- for ii, a in enumerate(lazy_op):
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
- for i, uc in enumerate(self._uncollapsed_dims):
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 within the given ROI.
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 ROI to write to.
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] % self.chunk_shape[d] == 0
443
- shape_align_with_chunks = voxel_roi.shape[d] % self.chunk_shape[d] == 0
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, self.chunk_shape, d)
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
+ )