funlib.persistence 0.7.0__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 (35) hide show
  1. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/PKG-INFO +1 -1
  2. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/__init__.py +1 -1
  3. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/arrays/array.py +98 -18
  4. funlib_persistence-0.8.0/src/funlib/persistence/arrays/datasets.py +576 -0
  5. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/graphs/pgsql_graph_database.py +36 -6
  6. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/graphs/sql_graph_database.py +27 -4
  7. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/graphs/sqlite_graph_database.py +7 -0
  8. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib.persistence.egg-info/PKG-INFO +1 -1
  9. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib.persistence.egg-info/SOURCES.txt +1 -0
  10. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/tests/test_array.py +54 -0
  11. funlib_persistence-0.8.0/tests/test_datasets.py +995 -0
  12. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/tests/test_graph.py +83 -0
  13. funlib_persistence-0.8.0/tests/test_pgsql_graph_database.py +91 -0
  14. funlib_persistence-0.7.0/src/funlib/persistence/arrays/datasets.py +0 -372
  15. funlib_persistence-0.7.0/tests/test_datasets.py +0 -292
  16. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/LICENSE +0 -0
  17. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/README.md +0 -0
  18. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/pyproject.toml +0 -0
  19. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/setup.cfg +0 -0
  20. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/arrays/__init__.py +0 -0
  21. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/arrays/freezable.py +0 -0
  22. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/arrays/lazy_ops.py +0 -0
  23. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/arrays/metadata.py +0 -0
  24. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/arrays/ome_datasets.py +0 -0
  25. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/arrays/utils.py +0 -0
  26. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/graphs/__init__.py +0 -0
  27. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/graphs/graph_database.py +0 -0
  28. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/py.typed +0 -0
  29. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib/persistence/types.py +0 -0
  30. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib.persistence.egg-info/dependency_links.txt +0 -0
  31. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib.persistence.egg-info/requires.txt +0 -0
  32. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/src/funlib.persistence.egg-info/top_level.txt +0 -0
  33. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/tests/test_config.py +0 -0
  34. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/tests/test_metadata.py +0 -0
  35. {funlib_persistence-0.7.0 → funlib_persistence-0.8.0}/tests/test_slices.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: funlib.persistence
3
- Version: 0.7.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
@@ -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.7.0"
3
+ __version__ = "0.8.0"
4
4
  __version_info__ = tuple(int(i) for i in __version__.split("."))
@@ -74,6 +74,18 @@ class Array(Freezable):
74
74
  offset, voxel_size, axis_names, units, and types. Metadata
75
75
  can either be passed in or read from array attributes.
76
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
+
77
89
  """
78
90
 
79
91
  data: da.Array
@@ -89,7 +101,9 @@ class Array(Freezable):
89
101
  chunks: Optional[Union[int, Sequence[int], str]] = "auto",
90
102
  lazy_op: Optional[LazyOp] = None,
91
103
  strict_metadata: bool = False,
104
+ scheduler: Optional[str] = None,
92
105
  ):
106
+ self.scheduler = scheduler
93
107
  if not isinstance(data, da.Array):
94
108
  self.data = da.from_array(data, chunks=chunks)
95
109
  else:
@@ -110,11 +124,13 @@ class Array(Freezable):
110
124
  # only used if not reading from zarr and there is no built in `.attrs`
111
125
  self._attrs: dict[str, Any] = {}
112
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] = []
113
131
  if lazy_op is not None:
114
132
  self.apply_lazy_ops(lazy_op)
115
-
116
- lazy_ops = [] if lazy_op is None else [lazy_op]
117
- self.lazy_ops = lazy_ops
133
+ self.lazy_ops = [lazy_op]
118
134
 
119
135
  self.freeze()
120
136
 
@@ -133,9 +149,67 @@ class Array(Freezable):
133
149
  return self._attrs
134
150
 
135
151
  @property
136
- def chunk_shape(self) -> Coordinate:
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:
137
183
  return Coordinate(self.data.chunksize) # ty: ignore[unresolved-attribute]
138
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
212
+
139
213
  def uncollapsed_dims(self, physical: bool = False) -> list[bool]:
140
214
  """
141
215
  We support lazy slicing of arrays, such as `x = Array(np.ones(5,5,5))` and
@@ -253,17 +327,18 @@ class Array(Freezable):
253
327
  )
254
328
 
255
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)
256
332
  if self._is_slice(lazy_op):
257
333
  if not isinstance(lazy_op, tuple):
258
334
  lazy_op = (lazy_op,)
259
- 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):
260
340
  if isinstance(a, int):
261
- for i, uc in enumerate(self._uncollapsed_dims):
262
- if uc:
263
- if ii == 0:
264
- self._uncollapsed_dims[i] = False
265
- break
266
- ii -= 1
341
+ self._uncollapsed_dims[i] = False
267
342
  self.data = self.data[lazy_op]
268
343
  elif isinstance(lazy_op, Roi):
269
344
  assert all(self.uncollapsed_dims(physical=True)), (
@@ -282,6 +357,7 @@ class Array(Freezable):
282
357
  f"LazyOp {lazy_op} is not a supported lazy_op. "
283
358
  f"Supported lazy_ops are: {LazyOp}"
284
359
  )
360
+ self._on_chunk_grid = on_chunk_grid
285
361
 
286
362
  def lazy_op(self, lazy_op: LazyOp):
287
363
  """Apply an lazy operation to this array.
@@ -329,7 +405,9 @@ class Array(Freezable):
329
405
  % (roi, self.roi)
330
406
  )
331
407
 
332
- 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
+ )
333
411
 
334
412
  elif isinstance(key, Coordinate):
335
413
  coordinate = key
@@ -338,10 +416,10 @@ class Array(Freezable):
338
416
  raise IndexError("Requested coordinate is not contained in this array.")
339
417
 
340
418
  index = self.__index(coordinate)
341
- return self.data[index].compute()
419
+ return self.data[index].compute(scheduler=self.scheduler)
342
420
 
343
421
  else:
344
- return self.data[key].compute()
422
+ return self.data[key].compute(scheduler=self.scheduler)
345
423
 
346
424
  def __setitem__(self, key: Roi | slice | tuple, value: np.ndarray | float | int):
347
425
  """Set the data of this array.
@@ -421,7 +499,7 @@ class Array(Freezable):
421
499
  if fill_value != 0:
422
500
  data[:] = fill_value
423
501
 
424
- array = Array(data, roi.offset, self.voxel_size)
502
+ array = Array(data, roi.offset, self.voxel_size, scheduler=self.scheduler)
425
503
 
426
504
  shared_roi = self.roi.intersect(roi)
427
505
 
@@ -438,18 +516,20 @@ class Array(Freezable):
438
516
  voxel_roi = (roi - self.offset) / self.voxel_size
439
517
 
440
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"
441
521
  for d in range(roi.dims):
442
522
  end_of_array = roi.get_end()[d] == self.roi.get_end()[d]
443
523
 
444
- begin_align_with_chunks = voxel_roi.begin[d] % self.chunk_shape[d] == 0
445
- 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
446
526
 
447
527
  assert begin_align_with_chunks and (
448
528
  shape_align_with_chunks or end_of_array
449
529
  ), (
450
530
  "ROI %s (in voxels: %s) does not align with chunks of "
451
531
  "size %s (mismatch in dimension %d)"
452
- % (roi, voxel_roi, self.chunk_shape, d)
532
+ % (roi, voxel_roi, chunk_shape, d)
453
533
  )
454
534
 
455
535
  roi_slices = tuple(