ngio 0.2.9__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.
Files changed (38) hide show
  1. ngio/common/__init__.py +16 -0
  2. ngio/common/_array_pipe.py +50 -27
  3. ngio/common/_table_ops.py +471 -0
  4. ngio/hcs/__init__.py +1 -1
  5. ngio/hcs/{plate.py → _plate.py} +451 -78
  6. ngio/images/__init__.py +3 -3
  7. ngio/images/{image.py → _image.py} +26 -21
  8. ngio/images/{label.py → _label.py} +6 -4
  9. ngio/images/{masked_image.py → _masked_image.py} +2 -2
  10. ngio/images/{ome_zarr_container.py → _ome_zarr_container.py} +152 -86
  11. ngio/ome_zarr_meta/_meta_handlers.py +16 -8
  12. ngio/ome_zarr_meta/ngio_specs/_channels.py +41 -29
  13. ngio/tables/__init__.py +14 -2
  14. ngio/tables/_abstract_table.py +269 -0
  15. ngio/tables/{tables_container.py → _tables_container.py} +186 -100
  16. ngio/tables/backends/__init__.py +20 -0
  17. ngio/tables/backends/_abstract_backend.py +58 -80
  18. ngio/tables/backends/{_anndata_v1.py → _anndata.py} +5 -1
  19. ngio/tables/backends/_csv.py +35 -0
  20. ngio/tables/backends/{_json_v1.py → _json.py} +4 -1
  21. ngio/tables/backends/{_csv_v1.py → _non_zarr_backends.py} +61 -27
  22. ngio/tables/backends/_parquet.py +47 -0
  23. ngio/tables/backends/_table_backends.py +39 -18
  24. ngio/tables/backends/_utils.py +147 -1
  25. ngio/tables/v1/__init__.py +19 -3
  26. ngio/tables/v1/_condition_table.py +71 -0
  27. ngio/tables/v1/_feature_table.py +63 -129
  28. ngio/tables/v1/_generic_table.py +21 -159
  29. ngio/tables/v1/_roi_table.py +285 -201
  30. ngio/utils/_fractal_fsspec_store.py +29 -0
  31. {ngio-0.2.9.dist-info → ngio-0.3.0.dist-info}/METADATA +4 -3
  32. ngio-0.3.0.dist-info/RECORD +61 -0
  33. ngio/tables/_validators.py +0 -108
  34. ngio-0.2.9.dist-info/RECORD +0 -57
  35. /ngio/images/{abstract_image.py → _abstract_image.py} +0 -0
  36. /ngio/images/{create.py → _create.py} +0 -0
  37. {ngio-0.2.9.dist-info → ngio-0.3.0.dist-info}/WHEEL +0 -0
  38. {ngio-0.2.9.dist-info → ngio-0.3.0.dist-info}/licenses/LICENSE +0 -0
ngio/common/__init__.py CHANGED
@@ -24,6 +24,15 @@ from ngio.common._slicer import (
24
24
  numpy_get_slice,
25
25
  numpy_set_slice,
26
26
  )
27
+ from ngio.common._table_ops import (
28
+ concatenate_image_tables,
29
+ concatenate_image_tables_as,
30
+ concatenate_image_tables_as_async,
31
+ concatenate_image_tables_async,
32
+ conctatenate_tables,
33
+ list_image_tables,
34
+ list_image_tables_async,
35
+ )
27
36
  from ngio.common._zoom import dask_zoom, numpy_zoom
28
37
 
29
38
  __all__ = [
@@ -34,6 +43,11 @@ __all__ = [
34
43
  "SliceTransform",
35
44
  "compute_and_slices",
36
45
  "compute_masking_roi",
46
+ "concatenate_image_tables",
47
+ "concatenate_image_tables_as",
48
+ "concatenate_image_tables_as_async",
49
+ "concatenate_image_tables_async",
50
+ "conctatenate_tables",
37
51
  "consolidate_pyramid",
38
52
  "dask_get_slice",
39
53
  "dask_set_slice",
@@ -41,6 +55,8 @@ __all__ = [
41
55
  "get_masked_pipe",
42
56
  "get_pipe",
43
57
  "init_empty_pyramid",
58
+ "list_image_tables",
59
+ "list_image_tables_async",
44
60
  "numpy_get_slice",
45
61
  "numpy_set_slice",
46
62
  "numpy_zoom",
@@ -1,10 +1,11 @@
1
1
  from collections.abc import Collection, Iterable
2
2
  from typing import Literal
3
3
 
4
- import dask
5
- import dask.delayed
4
+ import dask.array as da
6
5
  import numpy as np
7
6
  import zarr
7
+ from dask.array import Array as DaskArray
8
+ from dask.delayed import Delayed, delayed
8
9
 
9
10
  from ngio.common._axes_transforms import transform_dask_array, transform_numpy_array
10
11
  from ngio.common._common_types import ArrayLike
@@ -55,26 +56,26 @@ def _numpy_get_pipe(
55
56
  slices: SliceTransform,
56
57
  transformations: tuple[AxesTransformation, ...],
57
58
  ) -> np.ndarray:
58
- array = numpy_get_slice(array, slices)
59
- return transform_numpy_array(array, transformations)
59
+ _array = numpy_get_slice(array, slices)
60
+ return transform_numpy_array(_array, transformations)
60
61
 
61
62
 
62
63
  def _delayed_numpy_get_pipe(
63
64
  array: zarr.Array,
64
65
  slices: SliceTransform,
65
66
  transformations: tuple[AxesTransformation, ...],
66
- ) -> dask.delayed:
67
- array = dask.delayed(numpy_get_slice)(array, slices)
68
- return dask.delayed(transform_numpy_array)(array, transformations)
67
+ ) -> Delayed:
68
+ _array = delayed(numpy_get_slice)(array, slices)
69
+ return delayed(transform_numpy_array)(_array, transformations)
69
70
 
70
71
 
71
72
  def _dask_get_pipe(
72
73
  array: zarr.Array,
73
74
  slices: SliceTransform,
74
75
  transformations: tuple[AxesTransformation, ...],
75
- ) -> dask.array:
76
- array = dask_get_slice(array, slices)
77
- return transform_dask_array(array, transformations)
76
+ ) -> DaskArray:
77
+ _array = dask_get_slice(array, slices)
78
+ return transform_dask_array(_array, transformations)
78
79
 
79
80
 
80
81
  def _numpy_set_pipe(
@@ -89,22 +90,22 @@ def _numpy_set_pipe(
89
90
 
90
91
  def _dask_set_pipe(
91
92
  array: zarr.Array,
92
- patch: np.ndarray,
93
+ patch: DaskArray,
93
94
  slices: SliceTransform,
94
95
  transformations: tuple[AxesTransformation, ...],
95
96
  ) -> None:
96
- patch = transform_dask_array(patch, transformations)
97
- dask_set_slice(array, patch, slices)
97
+ _patch = transform_dask_array(patch, transformations)
98
+ dask_set_slice(array, _patch, slices)
98
99
 
99
100
 
100
101
  def _delayed_numpy_set_pipe(
101
102
  array: zarr.Array,
102
- patch: np.ndarray,
103
+ patch: np.ndarray | Delayed,
103
104
  slices: SliceTransform,
104
105
  transformations: tuple[AxesTransformation, ...],
105
- ) -> dask.delayed:
106
- patch = dask.delayed(transform_numpy_array)(patch, transformations)
107
- return dask.delayed(numpy_set_slice)(array, patch, slices)
106
+ ) -> Delayed:
107
+ _patch = delayed(transform_numpy_array)(patch, transformations)
108
+ return delayed(numpy_set_slice)(array, _patch, slices)
108
109
 
109
110
 
110
111
  def get_pipe(
@@ -144,7 +145,7 @@ def set_pipe(
144
145
  slices, transformations = _compute_to_disk_transforms(
145
146
  dimensions=dimensions, axes_order=axes_order, **slice_kwargs
146
147
  )
147
- if isinstance(patch, dask.array.Array):
148
+ if isinstance(patch, DaskArray):
148
149
  _dask_set_pipe(
149
150
  array=array, patch=patch, slices=slices, transformations=transformations
150
151
  )
@@ -152,7 +153,7 @@ def set_pipe(
152
153
  _numpy_set_pipe(
153
154
  array=array, patch=patch, slices=slices, transformations=transformations
154
155
  )
155
- elif isinstance(patch, dask.delayed.Delayed):
156
+ elif isinstance(patch, Delayed):
156
157
  _delayed_numpy_set_pipe(
157
158
  array=array, patch=patch, slices=slices, transformations=transformations
158
159
  )
@@ -193,12 +194,15 @@ def _mask_pipe_common(
193
194
  **slice_kwargs,
194
195
  )
195
196
 
196
- if isinstance(array_patch, np.ndarray):
197
+ if isinstance(array_patch, np.ndarray) and isinstance(label_patch, np.ndarray):
197
198
  label_patch = np.broadcast_to(label_patch, array_patch.shape)
198
- elif isinstance(array_patch, dask.array.Array):
199
- label_patch = dask.array.broadcast_to(label_patch, array_patch.shape)
199
+ elif isinstance(array_patch, DaskArray) and isinstance(label_patch, DaskArray):
200
+ label_patch = da.broadcast_to(label_patch, array_patch.shape)
200
201
  else:
201
- raise NgioValueError(f"Mode {mode} not yet supported for masked array.")
202
+ raise NgioValueError(
203
+ "Incompatible types for array and label: "
204
+ f"{type(array_patch)} and {type(label_patch)}"
205
+ )
202
206
 
203
207
  mask = label_patch == label
204
208
  return array_patch, mask
@@ -225,7 +229,14 @@ def get_masked_pipe(
225
229
  mode=mode,
226
230
  **slice_kwargs,
227
231
  )
228
- array_patch[~mask] = 0
232
+ if isinstance(array_patch, np.ndarray):
233
+ array_patch[~mask] = 0
234
+ elif isinstance(array_patch, DaskArray):
235
+ array_patch = da.where(mask, array_patch, 0)
236
+ else:
237
+ raise NgioValueError(
238
+ "Mode not yet supported for masked array. Expected a numpy or dask array."
239
+ )
229
240
  return array_patch
230
241
 
231
242
 
@@ -240,7 +251,7 @@ def set_masked_pipe(
240
251
  axes_order: Collection[str] | None = None,
241
252
  **slice_kwargs: slice | int | Iterable[int],
242
253
  ):
243
- if isinstance(patch, dask.array.Array):
254
+ if isinstance(patch, DaskArray):
244
255
  mode = "dask"
245
256
  elif isinstance(patch, np.ndarray):
246
257
  mode = "numpy"
@@ -259,7 +270,19 @@ def set_masked_pipe(
259
270
  mode=mode,
260
271
  **slice_kwargs,
261
272
  )
262
- patch = np.where(mask, patch, array_patch)
273
+ if isinstance(patch, np.ndarray):
274
+ assert isinstance(array_patch, np.ndarray)
275
+ _patch = np.where(mask, patch, array_patch)
276
+ elif isinstance(patch, DaskArray):
277
+ _patch = da.where(mask, patch, array_patch)
278
+ else:
279
+ raise NgioValueError(
280
+ "Mode not yet supported for masked array. Expected a numpy or dask array."
281
+ )
263
282
  set_pipe(
264
- array, patch, dimensions=dimensions_array, axes_order=axes_order, **slice_kwargs
283
+ array,
284
+ _patch,
285
+ dimensions=dimensions_array,
286
+ axes_order=axes_order,
287
+ **slice_kwargs,
265
288
  )
@@ -0,0 +1,471 @@
1
+ """Aggregation and filtering operations for tables."""
2
+
3
+ import asyncio
4
+ from collections import Counter
5
+ from collections.abc import Collection
6
+ from dataclasses import dataclass, field
7
+ from typing import Literal
8
+
9
+ import pandas as pd
10
+ import polars as pl
11
+
12
+ from ngio.images._ome_zarr_container import OmeZarrContainer
13
+ from ngio.tables import Table, TableType
14
+
15
+
16
+ @dataclass
17
+ class TableWithExtras:
18
+ """A class to hold a table and its extras."""
19
+
20
+ table: Table
21
+ extras: dict[str, str] = field(default_factory=dict)
22
+
23
+
24
+ def _reindex_dataframe(
25
+ dataframe, index_cols: list[str], index_key: str | None = None
26
+ ) -> pd.DataFrame:
27
+ """Reindex a dataframe using an hash of the index columns."""
28
+ # Reindex the dataframe
29
+ old_index = dataframe.index.name
30
+ if old_index is not None:
31
+ dataframe = dataframe.reset_index()
32
+ index_cols.append(old_index)
33
+ dataframe.index = dataframe[index_cols].astype(str).agg("_".join, axis=1)
34
+
35
+ if index_key is None:
36
+ dataframe.index.name = index_key
37
+ return dataframe
38
+
39
+
40
+ def _add_const_columns(
41
+ dataframe: pd.DataFrame,
42
+ new_cols: dict[str, str],
43
+ index_key: str | None = None,
44
+ ) -> pd.DataFrame:
45
+ for col, value in new_cols.items():
46
+ dataframe[col] = value
47
+
48
+ if index_key is not None:
49
+ dataframe = _reindex_dataframe(
50
+ dataframe=dataframe,
51
+ index_cols=list(new_cols.keys()),
52
+ index_key=index_key,
53
+ )
54
+ return dataframe
55
+
56
+
57
+ def _add_const_columns_pl(
58
+ dataframe: pl.LazyFrame,
59
+ new_cols: dict[str, str],
60
+ index_key: str | None = None,
61
+ ) -> pl.LazyFrame:
62
+ dataframe = dataframe.with_columns(
63
+ [pl.lit(value, dtype=pl.String()).alias(col) for col, value in new_cols.items()]
64
+ )
65
+
66
+ if index_key is not None:
67
+ dataframe = dataframe.with_columns(
68
+ [
69
+ pl.concat_str(
70
+ [pl.col(col) for col in new_cols.keys()],
71
+ separator="_",
72
+ ).alias(index_key)
73
+ ]
74
+ )
75
+ return dataframe
76
+
77
+
78
+ def _pd_concat(
79
+ tables: Collection[TableWithExtras], index_key: str | None = None
80
+ ) -> pd.DataFrame:
81
+ """Concatenate tables from different plates into a single table."""
82
+ if len(tables) == 0:
83
+ raise ValueError("No tables to concatenate.")
84
+
85
+ dataframes = []
86
+ for table in tables:
87
+ dataframe = _add_const_columns(
88
+ dataframe=table.table.dataframe, new_cols=table.extras, index_key=index_key
89
+ )
90
+ dataframes.append(dataframe)
91
+ concatenated_table = pd.concat(dataframes, axis=0)
92
+ return concatenated_table
93
+
94
+
95
+ def _pl_concat(
96
+ tables: Collection[TableWithExtras], index_key: str | None = None
97
+ ) -> pl.LazyFrame:
98
+ """Concatenate tables from different plates into a single table."""
99
+ if len(tables) == 0:
100
+ raise ValueError("No tables to concatenate.")
101
+
102
+ dataframes = []
103
+ for table in tables:
104
+ polars_ls = _add_const_columns_pl(
105
+ dataframe=table.table.lazy_frame,
106
+ new_cols=table.extras,
107
+ index_key=index_key,
108
+ )
109
+ dataframes.append(polars_ls)
110
+
111
+ concatenated_table = pl.concat(dataframes, how="vertical")
112
+ return concatenated_table
113
+
114
+
115
+ def conctatenate_tables(
116
+ tables: Collection[TableWithExtras],
117
+ mode: Literal["eager", "lazy"] = "eager",
118
+ index_key: str | None = None,
119
+ table_cls: type[TableType] | None = None,
120
+ ) -> Table:
121
+ """Concatenate tables from different plates into a single table."""
122
+ if len(tables) == 0:
123
+ raise ValueError("No tables to concatenate.")
124
+
125
+ table0 = next(iter(tables)).table
126
+
127
+ if mode == "lazy":
128
+ concatenated_table = _pl_concat(tables=tables, index_key=index_key)
129
+ elif mode == "eager":
130
+ concatenated_table = _pd_concat(tables=tables, index_key=index_key)
131
+ else:
132
+ raise ValueError(f"Unknown mode: {mode}. Use 'eager' or 'lazy'.")
133
+
134
+ meta = table0.meta
135
+ meta.index_key = index_key
136
+ meta.index_type = "str"
137
+
138
+ if table_cls is not None:
139
+ return table_cls.from_table_data(
140
+ table_data=concatenated_table,
141
+ meta=meta,
142
+ )
143
+ return table0.from_table_data(
144
+ table_data=concatenated_table,
145
+ meta=meta,
146
+ )
147
+
148
+
149
+ def _check_images_and_extras(
150
+ images: Collection[OmeZarrContainer],
151
+ extras: Collection[dict[str, str]],
152
+ ) -> None:
153
+ """Check if the images and extras are valid."""
154
+ if len(images) == 0:
155
+ raise ValueError("No images to concatenate.")
156
+
157
+ if len(images) != len(extras):
158
+ raise ValueError("The number of images and extras must be the same.")
159
+
160
+
161
+ def _concatenate_image_tables(
162
+ images: Collection[OmeZarrContainer],
163
+ extras: Collection[dict[str, str]],
164
+ table_name: str,
165
+ table_cls: type[TableType] | None = None,
166
+ index_key: str | None = None,
167
+ strict: bool = True,
168
+ mode: Literal["eager", "lazy"] = "eager",
169
+ ) -> Table:
170
+ """Concatenate tables from different images into a single table."""
171
+ _check_images_and_extras(images=images, extras=extras)
172
+
173
+ tables = []
174
+ for image, extra in zip(images, extras, strict=True):
175
+ if not strict and table_name not in image.list_tables():
176
+ continue
177
+ table = image.get_table(table_name)
178
+ tables.append(TableWithExtras(table=table, extras=extra))
179
+
180
+ return conctatenate_tables(
181
+ tables=tables,
182
+ mode=mode,
183
+ index_key=index_key,
184
+ table_cls=table_cls,
185
+ )
186
+
187
+
188
+ def concatenate_image_tables(
189
+ images: Collection[OmeZarrContainer],
190
+ extras: Collection[dict[str, str]],
191
+ table_name: str,
192
+ index_key: str | None = None,
193
+ strict: bool = True,
194
+ mode: Literal["eager", "lazy"] = "eager",
195
+ ) -> Table:
196
+ """Concatenate tables from different images into a single table.
197
+
198
+ Args:
199
+ images: A collection of images.
200
+ extras: A collection of extras dictionaries for each image.
201
+ this will be added as columns to the table, and will be
202
+ concatenated with the table index to create a new index.
203
+ table_name: The name of the table to concatenate.
204
+ index_key: The key to use for the index of the concatenated table.
205
+ strict: If True, raise an error if the table is not found in the image.
206
+ mode: The mode to use for concatenation. Can be 'eager' or 'lazy'.
207
+ if 'eager', the table will be loaded into memory.
208
+ if 'lazy', the table will be loaded as a lazy frame.
209
+ """
210
+ return _concatenate_image_tables(
211
+ images=images,
212
+ extras=extras,
213
+ table_name=table_name,
214
+ table_cls=None,
215
+ index_key=index_key,
216
+ strict=strict,
217
+ mode=mode,
218
+ )
219
+
220
+
221
+ def concatenate_image_tables_as(
222
+ images: Collection[OmeZarrContainer],
223
+ extras: Collection[dict[str, str]],
224
+ table_name: str,
225
+ table_cls: type[TableType],
226
+ index_key: str | None = None,
227
+ strict: bool = True,
228
+ mode: Literal["eager", "lazy"] = "eager",
229
+ ) -> TableType:
230
+ """Concatenate tables from different images into a single table.
231
+
232
+ Args:
233
+ images: A collection of images.
234
+ extras: A collection of extras dictionaries for each image.
235
+ this will be added as columns to the table, and will be
236
+ concatenated with the table index to create a new index.
237
+ table_name: The name of the table to concatenate.
238
+ table_cls: The output will be casted to this class, if the new table_cls is
239
+ compatible with the table_cls of the input tables.
240
+ index_key: The key to use for the index of the concatenated table.
241
+ strict: If True, raise an error if the table is not found in the image.
242
+ mode: The mode to use for concatenation. Can be 'eager' or 'lazy'.
243
+ if 'eager', the table will be loaded into memory.
244
+ if 'lazy', the table will be loaded as a lazy frame.
245
+ """
246
+ table = _concatenate_image_tables(
247
+ images=images,
248
+ extras=extras,
249
+ table_name=table_name,
250
+ table_cls=table_cls,
251
+ index_key=index_key,
252
+ strict=strict,
253
+ mode=mode,
254
+ )
255
+ if not isinstance(table, table_cls):
256
+ raise ValueError(f"Table is not of type {table_cls}. Got {type(table)}")
257
+ return table
258
+
259
+
260
+ async def _concatenate_image_tables_async(
261
+ images: Collection[OmeZarrContainer],
262
+ extras: Collection[dict[str, str]],
263
+ table_name: str,
264
+ table_cls: type[TableType] | None = None,
265
+ index_key: str | None = None,
266
+ strict: bool = True,
267
+ mode: Literal["eager", "lazy"] = "eager",
268
+ ) -> Table:
269
+ """Concatenate tables from different images into a single table."""
270
+ _check_images_and_extras(images=images, extras=extras)
271
+
272
+ def process_image(
273
+ image: OmeZarrContainer,
274
+ table_name: str,
275
+ extra: dict[str, str],
276
+ mode: Literal["eager", "lazy"] = "eager",
277
+ strict: bool = True,
278
+ ) -> TableWithExtras | None:
279
+ """Process a single image and return the table."""
280
+ if not strict and table_name not in image.list_tables():
281
+ return None
282
+ _table = image.get_table(table_name)
283
+ if mode == "lazy":
284
+ # make sure the table is loaded lazily
285
+ # It the backend is not lazy, this will be
286
+ # loaded eagerly
287
+ _ = _table.lazy_frame
288
+ elif mode == "eager":
289
+ # make sure the table is loaded eagerly
290
+ _ = _table.dataframe
291
+ table = TableWithExtras(
292
+ table=_table,
293
+ extras=extra,
294
+ )
295
+ return table
296
+
297
+ tasks = []
298
+ for image, extra in zip(images, extras, strict=True):
299
+ task = asyncio.to_thread(
300
+ process_image,
301
+ image=image,
302
+ table_name=table_name,
303
+ extra=extra,
304
+ strict=strict,
305
+ )
306
+ tasks.append(task)
307
+ tables = await asyncio.gather(*tasks)
308
+ tables = [table for table in tables if table is not None]
309
+ return conctatenate_tables(
310
+ tables=tables,
311
+ mode=mode,
312
+ index_key=index_key,
313
+ table_cls=table_cls,
314
+ )
315
+
316
+
317
+ async def concatenate_image_tables_async(
318
+ images: Collection[OmeZarrContainer],
319
+ extras: Collection[dict[str, str]],
320
+ table_name: str,
321
+ index_key: str | None = None,
322
+ strict: bool = True,
323
+ mode: Literal["eager", "lazy"] = "eager",
324
+ ) -> Table:
325
+ """Concatenate tables from different images into a single table.
326
+
327
+ Args:
328
+ images: A collection of images.
329
+ extras: A collection of extras dictionaries for each image.
330
+ this will be added as columns to the table, and will be
331
+ concatenated with the table index to create a new index.
332
+ table_name: The name of the table to concatenate.
333
+ index_key: The key to use for the index of the concatenated table.
334
+ strict: If True, raise an error if the table is not found in the image.
335
+ mode: The mode to use for concatenation. Can be 'eager' or 'lazy'.
336
+ if 'eager', the table will be loaded into memory.
337
+ if 'lazy', the table will be loaded as a lazy frame.
338
+ """
339
+ return await _concatenate_image_tables_async(
340
+ images=images,
341
+ extras=extras,
342
+ table_name=table_name,
343
+ table_cls=None,
344
+ index_key=index_key,
345
+ strict=strict,
346
+ mode=mode,
347
+ )
348
+
349
+
350
+ async def concatenate_image_tables_as_async(
351
+ images: Collection[OmeZarrContainer],
352
+ extras: Collection[dict[str, str]],
353
+ table_name: str,
354
+ table_cls: type[TableType],
355
+ index_key: str | None = None,
356
+ strict: bool = True,
357
+ mode: Literal["eager", "lazy"] = "eager",
358
+ ) -> TableType:
359
+ """Concatenate tables from different images into a single table.
360
+
361
+ Args:
362
+ images: A collection of images.
363
+ extras: A collection of extras dictionaries for each image.
364
+ this will be added as columns to the table, and will be
365
+ concatenated with the table index to create a new index.
366
+ table_name: The name of the table to concatenate.
367
+ table_cls: The output will be casted to this class, if the new table_cls is
368
+ compatible with the table_cls of the input tables.
369
+ index_key: The key to use for the index of the concatenated table.
370
+ strict: If True, raise an error if the table is not found in the image.
371
+ mode: The mode to use for concatenation. Can be 'eager' or 'lazy'.
372
+ if 'eager', the table will be loaded into memory.
373
+ if 'lazy', the table will be loaded as a lazy frame.
374
+ """
375
+ table = await _concatenate_image_tables_async(
376
+ images=images,
377
+ extras=extras,
378
+ table_name=table_name,
379
+ table_cls=table_cls,
380
+ index_key=index_key,
381
+ strict=strict,
382
+ mode=mode,
383
+ )
384
+ if not isinstance(table, table_cls):
385
+ raise ValueError(f"Table is not of type {table_cls}. Got {type(table)}")
386
+ return table
387
+
388
+
389
+ def _tables_names_coalesce(
390
+ tables_names: list[list[str]],
391
+ mode: Literal["common", "all"] = "common",
392
+ ) -> list[str]:
393
+ num_images = len(tables_names)
394
+ if num_images == 0:
395
+ raise ValueError("No images to concatenate.")
396
+
397
+ names = [name for _table_names in tables_names for name in _table_names]
398
+ names_counts = Counter(names)
399
+
400
+ if mode == "common":
401
+ # Get the names that are present in all images
402
+ common_names = [
403
+ name for name, count in names_counts.items() if count == num_images
404
+ ]
405
+ return common_names
406
+ elif mode == "all":
407
+ # Get all names
408
+ return list(names_counts.keys())
409
+ else:
410
+ raise ValueError(f"Unknown mode: {mode}. Use 'common' or 'all'.")
411
+
412
+
413
+ def list_image_tables(
414
+ images: Collection[OmeZarrContainer],
415
+ filter_types: str | None = None,
416
+ mode: Literal["common", "all"] = "common",
417
+ ) -> list[str]:
418
+ """List all table names in the images.
419
+
420
+ Args:
421
+ images: A collection of images.
422
+ filter_types (str | None): The type of tables to filter. If None,
423
+ return all tables. Defaults to None.
424
+ mode (Literal["common", "all"]): Whether to return only common tables
425
+ between all images or all tables. Defaults to "common".
426
+ """
427
+ tables_names = []
428
+ for image in images:
429
+ tables = image.list_tables(filter_types=filter_types)
430
+ tables_names.append(tables)
431
+
432
+ return _tables_names_coalesce(
433
+ tables_names=tables_names,
434
+ mode=mode,
435
+ )
436
+
437
+
438
+ async def list_image_tables_async(
439
+ images: Collection[OmeZarrContainer],
440
+ filter_types: str | None = None,
441
+ mode: Literal["common", "all"] = "common",
442
+ ) -> list[str]:
443
+ """List all image tables in the image asynchronously.
444
+
445
+ Args:
446
+ images: A collection of images.
447
+ filter_types (str | None): The type of tables to filter. If None,
448
+ return all tables. Defaults to None.
449
+ mode (Literal["common", "all"]): Whether to return only common tables
450
+ between all images or all tables. Defaults to "common".
451
+ """
452
+ images_ids = []
453
+
454
+ # key table name, value list of paths
455
+ def process_image(
456
+ image: OmeZarrContainer, filter_types: str | None = None
457
+ ) -> list[str]:
458
+ tables = image.list_tables(filter_types=filter_types)
459
+ return tables
460
+
461
+ tasks = []
462
+ for i, image in enumerate(images):
463
+ images_ids.append(i)
464
+ task = asyncio.to_thread(process_image, image, filter_types=filter_types)
465
+ tasks.append(task)
466
+
467
+ tables_names = await asyncio.gather(*tasks)
468
+ return _tables_names_coalesce(
469
+ tables_names=tables_names,
470
+ mode=mode,
471
+ )
ngio/hcs/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """OME-Zarr HCS objects models."""
2
2
 
3
- from ngio.hcs.plate import (
3
+ from ngio.hcs._plate import (
4
4
  OmeZarrPlate,
5
5
  OmeZarrWell,
6
6
  create_empty_plate,