ngio 0.2.9__py3-none-any.whl → 0.3.0a1__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.
ngio/hcs/plate.py CHANGED
@@ -1,7 +1,18 @@
1
1
  """A module for handling the Plate Collection in an OME-Zarr file."""
2
2
 
3
- from typing import Literal, overload
4
-
3
+ import asyncio
4
+ import warnings
5
+ from collections.abc import Collection
6
+ from typing import Literal
7
+
8
+ from ngio.common import (
9
+ concatenate_image_tables,
10
+ concatenate_image_tables_as,
11
+ concatenate_image_tables_as_async,
12
+ concatenate_image_tables_async,
13
+ list_image_tables,
14
+ list_image_tables_async,
15
+ )
5
16
  from ngio.images import OmeZarrContainer
6
17
  from ngio.ome_zarr_meta import (
7
18
  ImageInWellPath,
@@ -15,12 +26,15 @@ from ngio.ome_zarr_meta import (
15
26
  path_in_well_validation,
16
27
  )
17
28
  from ngio.tables import (
29
+ ConditionTable,
18
30
  FeatureTable,
19
31
  GenericRoiTable,
20
32
  MaskingRoiTable,
21
33
  RoiTable,
22
34
  Table,
35
+ TableBackend,
23
36
  TablesContainer,
37
+ TableType,
24
38
  TypedTable,
25
39
  )
26
40
  from ngio.utils import (
@@ -186,6 +200,28 @@ class OmeZarrWell:
186
200
  )
187
201
 
188
202
 
203
+ def _buil_extras(paths: Collection[str]) -> list[dict[str, str]]:
204
+ """Build the extras for the images.
205
+
206
+ Args:
207
+ paths (Collection[str]): The paths of the images.
208
+
209
+ Returns:
210
+ list[dict[str, str]]: The extras for the images.
211
+ """
212
+ extras = []
213
+ for path in paths:
214
+ row, column, path_in_well = path.split("/")
215
+ extras.append(
216
+ {
217
+ "row": row,
218
+ "column": column,
219
+ "path_in_well": path_in_well,
220
+ }
221
+ )
222
+ return extras
223
+
224
+
189
225
  class OmeZarrPlate:
190
226
  """A class to handle the Plate Collection in an OME-Zarr file."""
191
227
 
@@ -253,6 +289,22 @@ class OmeZarrPlate:
253
289
  """Return the wells paths in the plate."""
254
290
  return self.meta.wells_paths
255
291
 
292
+ async def images_paths_async(self, acquisition: int | None = None) -> list[str]:
293
+ """Return the images paths in the plate asynchronously.
294
+
295
+ If acquisition is None, return all images paths in the plate.
296
+ Else, return the images paths in the plate for the given acquisition.
297
+
298
+ Args:
299
+ acquisition (int | None): The acquisition id to filter the images.
300
+ """
301
+ wells = await self.get_wells_async()
302
+ paths = []
303
+ for well_path, well in wells.items():
304
+ for img_path in well.paths(acquisition):
305
+ paths.append(f"{well_path}/{img_path}")
306
+ return paths
307
+
256
308
  def images_paths(self, acquisition: int | None = None) -> list[str]:
257
309
  """Return the images paths in the plate.
258
310
 
@@ -262,9 +314,10 @@ class OmeZarrPlate:
262
314
  Args:
263
315
  acquisition (int | None): The acquisition id to filter the images.
264
316
  """
317
+ wells = self.get_wells()
265
318
  images = []
266
- for well_path, wells in self.get_wells().items():
267
- for img_path in wells.paths(acquisition):
319
+ for well_path, well in wells.items():
320
+ for img_path in well.paths(acquisition):
268
321
  images.append(f"{well_path}/{img_path}")
269
322
  return images
270
323
 
@@ -317,6 +370,37 @@ class OmeZarrPlate:
317
370
  group_handler = self._group_handler.derive_handler(well_path)
318
371
  return OmeZarrWell(group_handler)
319
372
 
373
+ async def get_wells_async(self) -> dict[str, OmeZarrWell]:
374
+ """Get all wells in the plate asynchronously.
375
+
376
+ This method processes wells in parallel for improved performance
377
+ when working with a large number of wells.
378
+
379
+ Returns:
380
+ dict[str, OmeZarrWell]: A dictionary of wells, where the key is the well
381
+ path and the value is the well object.
382
+ """
383
+ wells = self._group_handler.get_from_cache("wells")
384
+ if wells is not None:
385
+ return wells # type: ignore[return-value]
386
+
387
+ def process_well(well_path):
388
+ group_handler = self._group_handler.derive_handler(well_path)
389
+ well = OmeZarrWell(group_handler)
390
+ return well_path, well
391
+
392
+ wells, tasks = {}, []
393
+ for well_path in self.wells_paths():
394
+ task = asyncio.to_thread(process_well, well_path)
395
+ tasks.append(task)
396
+
397
+ results = await asyncio.gather(*tasks)
398
+ for well_path, well in results:
399
+ wells[well_path] = well
400
+
401
+ self._group_handler.add_to_cache("wells", wells)
402
+ return wells
403
+
320
404
  def get_wells(self) -> dict[str, OmeZarrWell]:
321
405
  """Get all wells in the plate.
322
406
 
@@ -324,23 +408,86 @@ class OmeZarrPlate:
324
408
  dict[str, OmeZarrWell]: A dictionary of wells, where the key is the well
325
409
  path and the value is the well object.
326
410
  """
327
- wells = {}
328
- for well_path in self.wells_paths():
411
+ wells = self._group_handler.get_from_cache("wells")
412
+ if wells is not None:
413
+ return wells # type: ignore[return-value]
414
+
415
+ def process_well(well_path):
329
416
  group_handler = self._group_handler.derive_handler(well_path)
330
417
  well = OmeZarrWell(group_handler)
418
+ return well_path, well
419
+
420
+ wells = {}
421
+ for well_path in self.wells_paths():
422
+ _, well = process_well(well_path)
331
423
  wells[well_path] = well
424
+
425
+ self._group_handler.add_to_cache("wells", wells)
332
426
  return wells
333
427
 
428
+ async def get_images_async(
429
+ self, acquisition: int | None = None
430
+ ) -> dict[str, OmeZarrContainer]:
431
+ """Get all images in the plate asynchronously.
432
+
433
+ This method processes images in parallel for improved performance
434
+ when working with a large number of images.
435
+
436
+ Args:
437
+ acquisition: The acquisition id to filter the images.
438
+
439
+ Returns:
440
+ dict[str, OmeZarrContainer]: A dictionary of images, where the key is the
441
+ image path and the value is the image object.
442
+ """
443
+ images = self._group_handler.get_from_cache("images")
444
+ if images is not None:
445
+ return images # type: ignore[return-value]
446
+
447
+ paths = await self.images_paths_async(acquisition=acquisition)
448
+
449
+ def process_image(image_path):
450
+ """Process a single image and return the image path and image object."""
451
+ img_group_handler = self._group_handler.derive_handler(image_path)
452
+ image = OmeZarrContainer(img_group_handler)
453
+ return image_path, image
454
+
455
+ images, tasks = {}, []
456
+ for image_path in paths:
457
+ task = asyncio.to_thread(process_image, image_path)
458
+ tasks.append(task)
459
+
460
+ results = await asyncio.gather(*tasks)
461
+
462
+ for image_path, image in results:
463
+ images[image_path] = image
464
+
465
+ self._group_handler.add_to_cache("images", images)
466
+ return images
467
+
334
468
  def get_images(self, acquisition: int | None = None) -> dict[str, OmeZarrContainer]:
335
469
  """Get all images in the plate.
336
470
 
337
471
  Args:
338
472
  acquisition: The acquisition id to filter the images.
339
473
  """
340
- images = {}
341
- for image_path in self.images_paths(acquisition):
474
+ images = self._group_handler.get_from_cache("images")
475
+ if images is not None:
476
+ return images # type: ignore[return-value]
477
+ paths = self.images_paths(acquisition=acquisition)
478
+
479
+ def process_image(image_path):
480
+ """Process a single image and return the image path and image object."""
342
481
  img_group_handler = self._group_handler.derive_handler(image_path)
343
- images[image_path] = OmeZarrContainer(img_group_handler)
482
+ image = OmeZarrContainer(img_group_handler)
483
+ return image_path, image
484
+
485
+ images = {}
486
+ for image_path in paths:
487
+ _, image = process_image(image_path)
488
+ images[image_path] = image
489
+
490
+ self._group_handler.add_to_cache("images", images)
344
491
  return images
345
492
 
346
493
  def get_image(
@@ -667,91 +814,122 @@ class OmeZarrPlate:
667
814
  raise NgioValidationError("No tables found in the image.")
668
815
  return self._tables_container
669
816
 
670
- @property
671
- def list_tables(self) -> list[str]:
817
+ def list_tables(self, filter_types: str | None = None) -> list[str]:
672
818
  """List all tables in the image."""
673
- return self.tables_container.list()
819
+ return self.tables_container.list(filter_types=filter_types)
674
820
 
675
821
  def list_roi_tables(self) -> list[str]:
676
822
  """List all ROI tables in the image."""
677
823
  return self.tables_container.list_roi_tables()
678
824
 
679
- @overload
680
- def get_table(self, name: str) -> Table: ...
825
+ def get_roi_table(self, name: str) -> RoiTable:
826
+ """Get a ROI table from the image.
827
+
828
+ Args:
829
+ name (str): The name of the table.
830
+ """
831
+ table = self.tables_container.get(name=name, strict=True)
832
+ if not isinstance(table, RoiTable):
833
+ raise NgioValueError(f"Table {name} is not a ROI table. Got {type(table)}")
834
+ return table
681
835
 
682
- @overload
683
- def get_table(self, name: str, check_type: None) -> Table: ...
836
+ def get_masking_roi_table(self, name: str) -> MaskingRoiTable:
837
+ """Get a masking ROI table from the image.
684
838
 
685
- @overload
686
- def get_table(self, name: str, check_type: Literal["roi_table"]) -> RoiTable: ...
839
+ Args:
840
+ name (str): The name of the table.
841
+ """
842
+ table = self.tables_container.get(name=name, strict=True)
843
+ if not isinstance(table, MaskingRoiTable):
844
+ raise NgioValueError(
845
+ f"Table {name} is not a masking ROI table. Got {type(table)}"
846
+ )
847
+ return table
687
848
 
688
- @overload
689
- def get_table(
690
- self, name: str, check_type: Literal["masking_roi_table"]
691
- ) -> MaskingRoiTable: ...
849
+ def get_feature_table(self, name: str) -> FeatureTable:
850
+ """Get a feature table from the image.
692
851
 
693
- @overload
694
- def get_table(
695
- self, name: str, check_type: Literal["feature_table"]
696
- ) -> FeatureTable: ...
852
+ Args:
853
+ name (str): The name of the table.
854
+ """
855
+ table = self.tables_container.get(name=name, strict=True)
856
+ if not isinstance(table, FeatureTable):
857
+ raise NgioValueError(
858
+ f"Table {name} is not a feature table. Got {type(table)}"
859
+ )
860
+ return table
697
861
 
698
- @overload
699
- def get_table(
700
- self, name: str, check_type: Literal["generic_roi_table"]
701
- ) -> GenericRoiTable: ...
862
+ def get_generic_roi_table(self, name: str) -> GenericRoiTable:
863
+ """Get a generic ROI table from the image.
864
+
865
+ Args:
866
+ name (str): The name of the table.
867
+ """
868
+ table = self.tables_container.get(name=name, strict=True)
869
+ if not isinstance(table, GenericRoiTable):
870
+ raise NgioValueError(
871
+ f"Table {name} is not a generic ROI table. Got {type(table)}"
872
+ )
873
+ return table
874
+
875
+ def get_condition_table(self, name: str) -> ConditionTable:
876
+ """Get a condition table from the image.
877
+
878
+ Args:
879
+ name (str): The name of the table.
880
+ """
881
+ table = self.tables_container.get(name=name, strict=True)
882
+ if not isinstance(table, ConditionTable):
883
+ raise NgioValueError(
884
+ f"Table {name} is not a condition table. Got {type(table)}"
885
+ )
886
+ return table
702
887
 
703
888
  def get_table(self, name: str, check_type: TypedTable | None = None) -> Table:
704
889
  """Get a table from the image.
705
890
 
706
891
  Args:
707
892
  name (str): The name of the table.
708
- check_type (TypedTable | None): The type of the table. If None, the
709
- type is not checked. If a type is provided, the table must be of that
710
- type.
711
- """
712
- if check_type is None:
713
- table = self.tables_container.get(name, strict=False)
714
- return table
715
-
716
- table = self.tables_container.get(name, strict=True)
717
- match check_type:
718
- case "roi_table":
719
- if not isinstance(table, RoiTable):
720
- raise NgioValueError(
721
- f"Table '{name}' is not a ROI table. Found type: {table.type()}"
722
- )
723
- return table
724
- case "masking_roi_table":
725
- if not isinstance(table, MaskingRoiTable):
726
- raise NgioValueError(
727
- f"Table '{name}' is not a masking ROI table. "
728
- f"Found type: {table.type()}"
729
- )
730
- return table
731
-
732
- case "generic_roi_table":
733
- if not isinstance(table, GenericRoiTable):
734
- raise NgioValueError(
735
- f"Table '{name}' is not a generic ROI table. "
736
- f"Found type: {table.type()}"
737
- )
738
- return table
739
-
740
- case "feature_table":
741
- if not isinstance(table, FeatureTable):
742
- raise NgioValueError(
743
- f"Table '{name}' is not a feature table. "
744
- f"Found type: {table.type()}"
745
- )
746
- return table
747
- case _:
748
- raise NgioValueError(f"Unknown check_type: {check_type}")
893
+ check_type (TypedTable | None): Deprecated. Please use
894
+ 'get_table_as' instead, or one of the type specific
895
+ get_*table() methods.
896
+
897
+ """
898
+ if check_type is not None:
899
+ warnings.warn(
900
+ "The 'check_type' argument is deprecated, and will be removed in "
901
+ "ngio=0.3. Use 'get_table_as' instead or one of the "
902
+ "type specific get_*table() methods.",
903
+ DeprecationWarning,
904
+ stacklevel=2,
905
+ )
906
+ return self.tables_container.get(name=name, strict=False)
907
+
908
+ def get_table_as(
909
+ self,
910
+ name: str,
911
+ table_cls: type[TableType],
912
+ backend: TableBackend | None = None,
913
+ ) -> TableType:
914
+ """Get a table from the image as a specific type.
915
+
916
+ Args:
917
+ name (str): The name of the table.
918
+ table_cls (type[TableType]): The type of the table.
919
+ backend (TableBackend | None): The backend to use. If None,
920
+ the default backend is used.
921
+ """
922
+ return self.tables_container.get_as(
923
+ name=name,
924
+ table_cls=table_cls,
925
+ backend=backend,
926
+ )
749
927
 
750
928
  def add_table(
751
929
  self,
752
930
  name: str,
753
931
  table: Table,
754
- backend: str | None = None,
932
+ backend: TableBackend = "anndata",
755
933
  overwrite: bool = False,
756
934
  ) -> None:
757
935
  """Add a table to the image."""
@@ -759,6 +937,186 @@ class OmeZarrPlate:
759
937
  name=name, table=table, backend=backend, overwrite=overwrite
760
938
  )
761
939
 
940
+ def list_image_tables(
941
+ self,
942
+ acquisition: int | None = None,
943
+ filter_types: str | None = None,
944
+ mode: Literal["common", "all"] = "common",
945
+ ) -> list[str]:
946
+ """List all image tables in the image.
947
+
948
+ Args:
949
+ acquisition (int | None): The acquisition id to filter the images.
950
+ filter_types (str | None): The type of tables to filter. If None,
951
+ return all tables. Defaults to None.
952
+ mode (Literal["common", "all"]): The mode to use for listing the tables.
953
+ If 'common', return only common tables between all images.
954
+ If 'all', return all tables. Defaults to 'common'.
955
+ """
956
+ images = self.get_images(acquisition=acquisition)
957
+ return list_image_tables(
958
+ images=images.values(),
959
+ filter_types=filter_types,
960
+ mode=mode,
961
+ )
962
+
963
+ async def list_image_tables_async(
964
+ self,
965
+ acquisition: int | None = None,
966
+ filter_types: str | None = None,
967
+ mode: Literal["common", "all"] = "common",
968
+ ) -> list[str]:
969
+ """List all image tables in the image asynchronously.
970
+
971
+ Args:
972
+ acquisition (int | None): The acquisition id to filter the images.
973
+ filter_types (str | None): The type of tables to filter. If None,
974
+ return all tables. Defaults to None.
975
+ mode (Literal["common", "all"]): The mode to use for listing the tables.
976
+ If 'common', return only common tables between all images.
977
+ If 'all', return all tables. Defaults to 'common'.
978
+ """
979
+ images = await self.get_images_async(acquisition=acquisition)
980
+ return await list_image_tables_async(
981
+ images=images.values(),
982
+ filter_types=filter_types,
983
+ mode=mode,
984
+ )
985
+
986
+ def concatenate_image_tables(
987
+ self,
988
+ table_name: str,
989
+ acquisition: int | None = None,
990
+ strict: bool = True,
991
+ index_key: str | None = None,
992
+ mode: Literal["eager", "lazy"] = "eager",
993
+ ) -> Table:
994
+ """Concatenate tables from all images in the plate.
995
+
996
+ Args:
997
+ table_name: The name of the table to concatenate.
998
+ index_key: The key to use for the index of the concatenated table.
999
+ acquisition: The acquisition id to filter the images.
1000
+ strict: If True, raise an error if the table is not found in the image.
1001
+ index_key: If a string is provided, a new index column will be created
1002
+ new_index_pattern = {row}_{column}_{path_in_well}_{label}
1003
+ mode: The mode to use for concatenation. Can be 'eager' or 'lazy'.
1004
+ if 'eager', the table will be loaded into memory.
1005
+ if 'lazy', the table will be loaded as a lazy frame.
1006
+ """
1007
+ images = self.get_images(acquisition=acquisition)
1008
+ extras = _buil_extras(images.keys())
1009
+ return concatenate_image_tables(
1010
+ images=images.values(),
1011
+ extras=extras,
1012
+ table_name=table_name,
1013
+ index_key=index_key,
1014
+ strict=strict,
1015
+ mode=mode,
1016
+ )
1017
+
1018
+ def concatenate_image_tables_as(
1019
+ self,
1020
+ table_name: str,
1021
+ table_cls: type[TableType],
1022
+ acquisition: int | None = None,
1023
+ index_key: str | None = None,
1024
+ strict: bool = True,
1025
+ mode: Literal["eager", "lazy"] = "eager",
1026
+ ) -> TableType:
1027
+ """Concatenate tables from all images in the plate as a specific type.
1028
+
1029
+ Args:
1030
+ table_name: The name of the table to concatenate.
1031
+ table_cls: The type of the table to concatenate.
1032
+ index_key: The key to use for the index of the concatenated table.
1033
+ acquisition: The acquisition id to filter the images.
1034
+ index_key: If a string is provided, a new index column will be created
1035
+ new_index_pattern = {row}_{column}_{path_in_well}_{label}
1036
+ strict: If True, raise an error if the table is not found in the image.
1037
+ mode: The mode to use for concatenation. Can be 'eager' or 'lazy'.
1038
+ if 'eager', the table will be loaded into memory.
1039
+ if 'lazy', the table will be loaded as a lazy frame.
1040
+ """
1041
+ images = self.get_images(acquisition=acquisition)
1042
+ extras = _buil_extras(images.keys())
1043
+ return concatenate_image_tables_as(
1044
+ images=images.values(),
1045
+ extras=extras,
1046
+ table_name=table_name,
1047
+ table_cls=table_cls,
1048
+ index_key=index_key,
1049
+ strict=strict,
1050
+ mode=mode,
1051
+ )
1052
+
1053
+ async def concatenate_image_tables_async(
1054
+ self,
1055
+ table_name: str,
1056
+ acquisition: int | None = None,
1057
+ index_key: str | None = None,
1058
+ strict: bool = True,
1059
+ mode: Literal["eager", "lazy"] = "eager",
1060
+ ) -> Table:
1061
+ """Concatenate tables from all images in the plate asynchronously.
1062
+
1063
+ Args:
1064
+ table_name: The name of the table to concatenate.
1065
+ index_key: The key to use for the index of the concatenated table.
1066
+ acquisition: The acquisition id to filter the images.
1067
+ index_key: If a string is provided, a new index column will be created
1068
+ new_index_pattern = {row}_{column}_{path_in_well}_{label}
1069
+ strict: If True, raise an error if the table is not found in the image.
1070
+ mode: The mode to use for concatenation. Can be 'eager' or 'lazy'.
1071
+ if 'eager', the table will be loaded into memory.
1072
+ if 'lazy', the table will be loaded as a lazy frame.
1073
+ """
1074
+ images = await self.get_images_async(acquisition=acquisition)
1075
+ extras = _buil_extras(images.keys())
1076
+ return await concatenate_image_tables_async(
1077
+ images=images.values(),
1078
+ extras=extras,
1079
+ table_name=table_name,
1080
+ index_key=index_key,
1081
+ strict=strict,
1082
+ mode=mode,
1083
+ )
1084
+
1085
+ async def concatenate_image_tables_as_async(
1086
+ self,
1087
+ table_name: str,
1088
+ table_cls: type[TableType],
1089
+ acquisition: int | None = None,
1090
+ index_key: str | None = None,
1091
+ strict: bool = True,
1092
+ mode: Literal["eager", "lazy"] = "eager",
1093
+ ) -> TableType:
1094
+ """Concatenate tables from all images in the plate as a specific type.
1095
+
1096
+ Args:
1097
+ table_name: The name of the table to concatenate.
1098
+ table_cls: The type of the table to concatenate.
1099
+ index_key: The key to use for the index of the concatenated table.
1100
+ acquisition: The acquisition id to filter the images.
1101
+ index_key: If a string is provided, a new index column will be created
1102
+ new_index_pattern = {row}_{column}_{path_in_well}_{label}
1103
+ strict: If True, raise an error if the table is not found in the image.
1104
+ mode: The mode to use for concatenation. Can be 'eager' or 'lazy'.
1105
+ if 'eager', the table will be loaded into memory.
1106
+ if 'lazy', the table will be loaded as a lazy frame.
1107
+ """
1108
+ images = await self.get_images_async(acquisition=acquisition)
1109
+ extras = _buil_extras(images.keys())
1110
+ return await concatenate_image_tables_as_async(
1111
+ images=images.values(),
1112
+ extras=extras,
1113
+ table_name=table_name,
1114
+ table_cls=table_cls,
1115
+ index_key=index_key,
1116
+ strict=strict,
1117
+ mode=mode,
1118
+ )
1119
+
762
1120
 
763
1121
  def open_ome_zarr_plate(
764
1122
  store: StoreOrGroup,