ngio 0.1.6__py3-none-any.whl → 0.2.0a2__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 (84) hide show
  1. ngio/__init__.py +31 -5
  2. ngio/common/__init__.py +44 -0
  3. ngio/common/_array_pipe.py +160 -0
  4. ngio/common/_axes_transforms.py +63 -0
  5. ngio/common/_common_types.py +5 -0
  6. ngio/common/_dimensions.py +113 -0
  7. ngio/common/_pyramid.py +223 -0
  8. ngio/{core/roi.py → common/_roi.py} +22 -23
  9. ngio/common/_slicer.py +97 -0
  10. ngio/{pipes/_zoom_utils.py → common/_zoom.py} +2 -78
  11. ngio/hcs/__init__.py +60 -0
  12. ngio/images/__init__.py +23 -0
  13. ngio/images/abstract_image.py +240 -0
  14. ngio/images/create.py +251 -0
  15. ngio/images/image.py +389 -0
  16. ngio/images/label.py +236 -0
  17. ngio/images/omezarr_container.py +535 -0
  18. ngio/ome_zarr_meta/__init__.py +35 -0
  19. ngio/ome_zarr_meta/_generic_handlers.py +320 -0
  20. ngio/ome_zarr_meta/_meta_handlers.py +142 -0
  21. ngio/ome_zarr_meta/ngio_specs/__init__.py +63 -0
  22. ngio/ome_zarr_meta/ngio_specs/_axes.py +481 -0
  23. ngio/ome_zarr_meta/ngio_specs/_channels.py +378 -0
  24. ngio/ome_zarr_meta/ngio_specs/_dataset.py +134 -0
  25. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +5 -0
  26. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +434 -0
  27. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +84 -0
  28. ngio/ome_zarr_meta/v04/__init__.py +11 -0
  29. ngio/ome_zarr_meta/v04/_meta_handlers.py +54 -0
  30. ngio/ome_zarr_meta/v04/_v04_spec_utils.py +412 -0
  31. ngio/tables/__init__.py +21 -5
  32. ngio/tables/_validators.py +192 -0
  33. ngio/tables/backends/__init__.py +8 -0
  34. ngio/tables/backends/_abstract_backend.py +71 -0
  35. ngio/tables/backends/_anndata_utils.py +194 -0
  36. ngio/tables/backends/_anndata_v1.py +75 -0
  37. ngio/tables/backends/_json_v1.py +56 -0
  38. ngio/tables/backends/_table_backends.py +102 -0
  39. ngio/tables/tables_container.py +300 -0
  40. ngio/tables/v1/__init__.py +6 -5
  41. ngio/tables/v1/_feature_table.py +161 -0
  42. ngio/tables/v1/_generic_table.py +99 -182
  43. ngio/tables/v1/_masking_roi_table.py +175 -0
  44. ngio/tables/v1/_roi_table.py +226 -0
  45. ngio/utils/__init__.py +23 -10
  46. ngio/utils/_datasets.py +51 -0
  47. ngio/utils/_errors.py +10 -4
  48. ngio/utils/_zarr_utils.py +378 -0
  49. {ngio-0.1.6.dist-info → ngio-0.2.0a2.dist-info}/METADATA +18 -39
  50. ngio-0.2.0a2.dist-info/RECORD +53 -0
  51. ngio/core/__init__.py +0 -7
  52. ngio/core/dimensions.py +0 -122
  53. ngio/core/image_handler.py +0 -228
  54. ngio/core/image_like_handler.py +0 -549
  55. ngio/core/label_handler.py +0 -410
  56. ngio/core/ngff_image.py +0 -387
  57. ngio/core/utils.py +0 -287
  58. ngio/io/__init__.py +0 -19
  59. ngio/io/_zarr.py +0 -88
  60. ngio/io/_zarr_array_utils.py +0 -0
  61. ngio/io/_zarr_group_utils.py +0 -60
  62. ngio/iterators/__init__.py +0 -1
  63. ngio/ngff_meta/__init__.py +0 -27
  64. ngio/ngff_meta/fractal_image_meta.py +0 -1267
  65. ngio/ngff_meta/meta_handler.py +0 -92
  66. ngio/ngff_meta/utils.py +0 -235
  67. ngio/ngff_meta/v04/__init__.py +0 -6
  68. ngio/ngff_meta/v04/specs.py +0 -158
  69. ngio/ngff_meta/v04/zarr_utils.py +0 -376
  70. ngio/pipes/__init__.py +0 -7
  71. ngio/pipes/_slicer_transforms.py +0 -176
  72. ngio/pipes/_transforms.py +0 -33
  73. ngio/pipes/data_pipe.py +0 -52
  74. ngio/tables/_ad_reader.py +0 -80
  75. ngio/tables/_utils.py +0 -301
  76. ngio/tables/tables_group.py +0 -252
  77. ngio/tables/v1/feature_tables.py +0 -182
  78. ngio/tables/v1/masking_roi_tables.py +0 -243
  79. ngio/tables/v1/roi_tables.py +0 -285
  80. ngio/utils/_common_types.py +0 -5
  81. ngio/utils/_pydantic_utils.py +0 -52
  82. ngio-0.1.6.dist-info/RECORD +0 -44
  83. {ngio-0.1.6.dist-info → ngio-0.2.0a2.dist-info}/WHEEL +0 -0
  84. {ngio-0.1.6.dist-info → ngio-0.2.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,71 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections.abc import Collection
3
+ from typing import Literal
4
+
5
+ from anndata import AnnData
6
+ from pandas import DataFrame
7
+
8
+ from ngio.utils import ZarrGroupHandler
9
+
10
+
11
+ class AbstractTableBackend(ABC):
12
+ """Abstract class for table backends."""
13
+
14
+ def __init__(
15
+ self,
16
+ group_handler: ZarrGroupHandler,
17
+ index_key: str | None = None,
18
+ index_type: Literal["int", "str"] = "int",
19
+ ):
20
+ """Initialize the handler.
21
+
22
+ Args:
23
+ group_handler (ZarrGroupHandler): An object to handle the Zarr group
24
+ containing the table data.
25
+ index_key (str): The column name to use as the index of the DataFrame.
26
+ index_type (str): The type of the index column in the DataFrame.
27
+ """
28
+ self._group_handler = group_handler
29
+ self._index_key = index_key
30
+ self._index_type = index_type
31
+
32
+ @staticmethod
33
+ @abstractmethod
34
+ def backend_name() -> str:
35
+ """The name of the backend."""
36
+ raise NotImplementedError
37
+
38
+ @staticmethod
39
+ @abstractmethod
40
+ def implements_anndata() -> bool:
41
+ """Whether the handler implements the anndata protocol."""
42
+ raise NotImplementedError
43
+
44
+ @staticmethod
45
+ @abstractmethod
46
+ def implements_dataframe() -> bool:
47
+ """Whether the handler implements the dataframe protocol."""
48
+ raise NotImplementedError
49
+
50
+ @abstractmethod
51
+ def load_columns(self) -> list[str]:
52
+ """List all labels in the group."""
53
+ raise NotImplementedError
54
+
55
+ def load_as_anndata(self, columns: Collection[str] | None = None) -> AnnData:
56
+ """Load the metadata in the store."""
57
+ raise NotImplementedError
58
+
59
+ def load_as_dataframe(self, columns: Collection[str] | None = None) -> DataFrame:
60
+ """List all labels in the group."""
61
+ raise NotImplementedError
62
+
63
+ def write_from_dataframe(
64
+ self, table: DataFrame, metadata: dict | None = None
65
+ ) -> None:
66
+ """Consolidate the metadata in the store."""
67
+ raise NotImplementedError
68
+
69
+ def write_from_anndata(self, table: AnnData, metadata: dict | None = None) -> None:
70
+ """Consolidate the metadata in the store."""
71
+ raise NotImplementedError
@@ -0,0 +1,194 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Literal
4
+
5
+ import anndata as ad
6
+ import numpy as np
7
+ import pandas as pd
8
+ import pandas.api.types as ptypes
9
+ import zarr
10
+ from anndata import AnnData
11
+ from anndata._io.specs import read_elem
12
+ from anndata._io.utils import _read_legacy_raw
13
+ from anndata._io.zarr import read_dataframe
14
+ from anndata.compat import _clean_uns
15
+ from anndata.experimental import read_dispatched
16
+
17
+ from ngio.tables._validators import validate_index_dtype, validate_index_key
18
+ from ngio.utils import (
19
+ NgioTableValidationError,
20
+ StoreOrGroup,
21
+ open_group_wrapper,
22
+ )
23
+
24
+ if TYPE_CHECKING:
25
+ from collections.abc import Callable, Collection
26
+
27
+
28
+ def custom_read_zarr(
29
+ store: StoreOrGroup, elem_to_read: Collection[str] | None = None
30
+ ) -> AnnData:
31
+ """Read from a hierarchical Zarr array store.
32
+
33
+ # Implementation originally from https://github.com/scverse/anndata/blob/main/src/anndata/_io/zarr.py
34
+ # Original implementation would not work with remote storages so we had to copy it
35
+ # here and slightly modified it to work with remote storages.
36
+
37
+ Args:
38
+ store (StoreOrGroup): A store or group to read the AnnData from.
39
+ elem_to_read (Collection[str] | None): The elements to read from the store.
40
+ """
41
+ group, _ = open_group_wrapper(store=store, mode="r")
42
+
43
+ if elem_to_read is None:
44
+ elem_to_read = [
45
+ "X",
46
+ "obs",
47
+ "var",
48
+ "uns",
49
+ "obsm",
50
+ "varm",
51
+ "obsp",
52
+ "varp",
53
+ "layers",
54
+ ]
55
+
56
+ # Read with handling for backwards compat
57
+ def callback(func: Callable, elem_name: str, elem: Any, iospec: Any) -> Any:
58
+ if iospec.encoding_type == "anndata" or elem_name.endswith("/"):
59
+ ad_kwargs = {}
60
+ # Some of these elem fail on https
61
+ # So we only include the ones that are strictly necessary
62
+ # for fractal tables
63
+ # This fails on some https
64
+ # base_elem += list(elem.keys())
65
+ for k in elem_to_read:
66
+ v = elem.get(k)
67
+ if v is not None and not k.startswith("raw."):
68
+ ad_kwargs[k] = read_dispatched(v, callback) # type: ignore
69
+ return AnnData(**ad_kwargs)
70
+
71
+ elif elem_name.startswith("/raw."):
72
+ return None
73
+ elif elem_name in {"/obs", "/var"}:
74
+ return read_dataframe(elem)
75
+ elif elem_name == "/raw":
76
+ # Backwards compat
77
+ return _read_legacy_raw(group, func(elem), read_dataframe, func)
78
+ return func(elem)
79
+
80
+ adata = read_dispatched(group, callback=callback) # type: ignore
81
+
82
+ # Backwards compat (should figure out which version)
83
+ if "raw.X" in group:
84
+ raw = AnnData(**_read_legacy_raw(group, adata.raw, read_dataframe, read_elem)) # type: ignore
85
+ raw.obs_names = adata.obs_names # type: ignore
86
+ adata.raw = raw # type: ignore
87
+
88
+ # Backwards compat for <0.7
89
+ if isinstance(group["obs"], zarr.Array):
90
+ _clean_uns(adata)
91
+
92
+ if not isinstance(adata, AnnData):
93
+ raise ValueError(f"Expected an AnnData object, but got {type(adata)}")
94
+ return adata
95
+
96
+
97
+ def _check_for_mixed_types(series: pd.Series) -> None:
98
+ """Check if the column has mixed types."""
99
+ if series.apply(type).nunique() > 1: # type: ignore
100
+ raise NgioTableValidationError(
101
+ f"Column {series.name} has mixed types: "
102
+ f"{series.apply(type).unique()}. " # type: ignore
103
+ "Type of all elements must be the same."
104
+ )
105
+
106
+
107
+ def _check_for_supported_types(series: pd.Series) -> Literal["str", "int", "numeric"]:
108
+ """Check if the column has supported types."""
109
+ if ptypes.is_string_dtype(series):
110
+ return "str"
111
+ if ptypes.is_integer_dtype(series):
112
+ return "int"
113
+ if ptypes.is_numeric_dtype(series):
114
+ return "numeric"
115
+ raise NgioTableValidationError(
116
+ f"Column {series.name} has unsupported type: {series.dtype}."
117
+ " Supported types are string and numerics."
118
+ )
119
+
120
+
121
+ def dataframe_to_anndata(
122
+ dataframe: pd.DataFrame,
123
+ index_key: str | None = None,
124
+ overwrite: bool = False,
125
+ ) -> ad.AnnData:
126
+ """Convert a table DataFrame to an AnnData object.
127
+
128
+ Args:
129
+ dataframe (pd.DataFrame): A pandas DataFrame representing a fractal table.
130
+ index_key (str): The column name to use as the index of the DataFrame.
131
+ Default is None.
132
+ overwrite (bool): Whether to overwrite the index if a different index is found.
133
+ Default is False.
134
+ """
135
+ # Check if the index_key is present in the data frame + optional validations
136
+ dataframe = validate_index_key(dataframe, index_key, overwrite=overwrite)
137
+ dataframe = validate_index_dtype(dataframe, index_type="str")
138
+
139
+ str_columns, int_columns, num_columns = [], [], []
140
+ for c_name in dataframe.columns:
141
+ column_df = dataframe[c_name]
142
+ _check_for_mixed_types(column_df) # Mixed types are not allowed in the table
143
+ c_type = _check_for_supported_types(
144
+ column_df
145
+ ) # Only string and numeric types are allowed
146
+
147
+ if c_type == "str":
148
+ str_columns.append(c_name)
149
+
150
+ elif c_type == "int":
151
+ int_columns.append(c_name)
152
+
153
+ elif c_type == "numeric":
154
+ num_columns.append(c_name)
155
+
156
+ # Converting all observations to string
157
+ obs_df = dataframe[str_columns + int_columns]
158
+ obs_df.index = dataframe.index
159
+
160
+ x_df = dataframe[num_columns]
161
+
162
+ if x_df.dtypes.nunique() > 1:
163
+ x_df = x_df.astype("float64")
164
+
165
+ if x_df.empty:
166
+ # If there are no numeric columns, create an empty array
167
+ # to avoid AnnData failing to create the object
168
+ x_df = np.zeros((len(obs_df), 0), dtype="float64")
169
+
170
+ return ad.AnnData(X=x_df, obs=obs_df)
171
+
172
+
173
+ def anndata_to_dataframe(
174
+ anndata: ad.AnnData,
175
+ index_key: str | None = "label",
176
+ index_type: str = "int",
177
+ overwrite: bool = False,
178
+ ) -> pd.DataFrame:
179
+ """Convert a AnnData object representing a fractal table to a pandas DataFrame.
180
+
181
+ Args:
182
+ anndata (ad.AnnData): An AnnData object representing a fractal table.
183
+ index_key (str): The column name to use as the index of the DataFrame.
184
+ Default is 'label'.
185
+ index_type (str): The type of the index column in the DataFrame.
186
+ Either 'str' or 'int'. Default is 'int'.
187
+ overwrite (bool): Whether to overwrite the index if a different index is found.
188
+ Default is False.
189
+ """
190
+ dataframe = anndata.to_df()
191
+ dataframe[anndata.obs_keys()] = anndata.obs
192
+ dataframe = validate_index_key(dataframe, index_key, overwrite=overwrite)
193
+ dataframe = validate_index_dtype(dataframe, index_type)
194
+ return dataframe
@@ -0,0 +1,75 @@
1
+ from collections.abc import Collection
2
+ from pathlib import Path
3
+
4
+ from anndata import AnnData
5
+ from pandas import DataFrame
6
+
7
+ from ngio.tables.backends._abstract_backend import AbstractTableBackend
8
+ from ngio.tables.backends._anndata_utils import (
9
+ anndata_to_dataframe,
10
+ custom_read_zarr,
11
+ dataframe_to_anndata,
12
+ )
13
+
14
+
15
+ class AnnDataBackend(AbstractTableBackend):
16
+ """A class to load and write tables from/to an AnnData object."""
17
+
18
+ @staticmethod
19
+ def backend_name() -> str:
20
+ """The name of the backend."""
21
+ return "anndata_v1"
22
+
23
+ @staticmethod
24
+ def implements_anndata() -> bool:
25
+ """Whether the handler implements the anndata protocol."""
26
+ return True
27
+
28
+ @staticmethod
29
+ def implements_dataframe() -> bool:
30
+ """Whether the handler implements the dataframe protocol."""
31
+ return True
32
+
33
+ def load_columns(self) -> list[str]:
34
+ """List all labels in the group."""
35
+ return list(self.load_as_dataframe().columns)
36
+
37
+ def load_as_anndata(self, columns: Collection[str] | None = None) -> AnnData:
38
+ """Load the metadata in the store."""
39
+ anndata = custom_read_zarr(self._group_handler._group)
40
+ if columns is not None:
41
+ raise NotImplementedError(
42
+ "Selecting columns is not implemented for AnnData."
43
+ )
44
+ return anndata
45
+
46
+ def load_as_dataframe(self, columns: Collection[str] | None = None) -> DataFrame:
47
+ """List all labels in the group."""
48
+ dataframe = anndata_to_dataframe(
49
+ self.load_as_anndata(),
50
+ index_key=self._index_key,
51
+ index_type=self._index_type,
52
+ )
53
+ if columns is not None:
54
+ dataframe = dataframe[columns]
55
+ return dataframe
56
+
57
+ def write_from_dataframe(
58
+ self, table: DataFrame, metadata: dict | None = None
59
+ ) -> None:
60
+ """Consolidate the metadata in the store."""
61
+ anndata = dataframe_to_anndata(table, index_key=self._index_key)
62
+ self.write_from_anndata(anndata, metadata)
63
+
64
+ def write_from_anndata(self, table: AnnData, metadata: dict | None = None) -> None:
65
+ """Consolidate the metadata in the store."""
66
+ store = self._group_handler.store
67
+ if not isinstance(store, str | Path):
68
+ raise ValueError(
69
+ "To write an AnnData object the store must be a local path/str."
70
+ )
71
+
72
+ store = Path(store) / self._group_handler.group.path
73
+ table.write_zarr(store)
74
+ if metadata is not None:
75
+ self._group_handler.write_attrs(metadata)
@@ -0,0 +1,56 @@
1
+ from collections.abc import Collection
2
+
3
+ import pandas as pd
4
+ from pandas import DataFrame
5
+
6
+ from ngio.tables.backends._abstract_backend import AbstractTableBackend
7
+ from ngio.utils import NgioFileNotFoundError
8
+
9
+
10
+ class JsonTableBackend(AbstractTableBackend):
11
+ """A class to load and write small tables in the zarr group .attrs (json)."""
12
+
13
+ @staticmethod
14
+ def backend_name() -> str:
15
+ """The name of the backend."""
16
+ return "json_v1"
17
+
18
+ @staticmethod
19
+ def implements_anndata() -> bool:
20
+ """Whether the handler implements the anndata protocol."""
21
+ return False
22
+
23
+ @staticmethod
24
+ def implements_dataframe() -> bool:
25
+ """Whether the handler implements the dataframe protocol."""
26
+ return True
27
+
28
+ def load_columns(self) -> list[str]:
29
+ """List all labels in the group."""
30
+ return list(self.load_as_dataframe().columns)
31
+
32
+ def _get_table_group(self):
33
+ try:
34
+ table_group = self._group_handler.get_group(path="table")
35
+ except NgioFileNotFoundError:
36
+ table_group = self._group_handler.group.create_group("table")
37
+ return table_group
38
+
39
+ def load_as_dataframe(self, columns: Collection[str] | None = None) -> DataFrame:
40
+ """List all labels in the group."""
41
+ table_group = self._get_table_group()
42
+ table_dict = dict(table_group.attrs)
43
+ data_frame = pd.DataFrame.from_dict(table_dict)
44
+ if columns is not None:
45
+ data_frame = data_frame[columns]
46
+ return data_frame
47
+
48
+ def write_from_dataframe(
49
+ self, table: DataFrame, metadata: dict | None = None
50
+ ) -> None:
51
+ """Consolidate the metadata in the store."""
52
+ table_group = self._get_table_group()
53
+ table_group.attrs.clear()
54
+ table_group.attrs.update(table.to_dict())
55
+ if metadata is not None:
56
+ self._group_handler.write_attrs(metadata)
@@ -0,0 +1,102 @@
1
+ """Protocol for table backends handlers."""
2
+
3
+ from collections.abc import Collection
4
+ from typing import Literal, Protocol
5
+
6
+ from anndata import AnnData
7
+ from pandas import DataFrame
8
+
9
+ from ngio.tables.backends._anndata_v1 import AnnDataBackend
10
+ from ngio.tables.backends._json_v1 import JsonTableBackend
11
+ from ngio.utils import NgioValueError, ZarrGroupHandler
12
+
13
+
14
+ class TableBackendProtocol(Protocol):
15
+ def __init__(
16
+ self,
17
+ group_handler: ZarrGroupHandler,
18
+ index_key: str | None = None,
19
+ index_type: Literal["int", "str"] = "int",
20
+ ): ...
21
+
22
+ @staticmethod
23
+ def backend_name() -> str: ...
24
+
25
+ @staticmethod
26
+ def implements_anndata() -> bool: ...
27
+
28
+ @staticmethod
29
+ def implements_dataframe() -> bool: ...
30
+
31
+ def load_columns(self) -> list[str]: ...
32
+
33
+ def load_as_anndata(self, columns: Collection[str] | None = None) -> AnnData: ...
34
+
35
+ def load_as_dataframe(
36
+ self, columns: Collection[str] | None = None
37
+ ) -> DataFrame: ...
38
+
39
+ def write_from_dataframe(
40
+ self, table: DataFrame, metadata: dict | None = None
41
+ ) -> None: ...
42
+
43
+ def write_from_anndata(
44
+ self, table: AnnData, metadata: dict | None = None
45
+ ) -> None: ...
46
+
47
+
48
+ class ImplementedTableBackends:
49
+ """A class to manage the available table backends."""
50
+
51
+ _instance = None
52
+ _implemented_backends: dict[str, type[TableBackendProtocol]]
53
+
54
+ def __new__(cls):
55
+ """Create a new instance of the class if it does not exist."""
56
+ if cls._instance is None:
57
+ cls._instance = super().__new__(cls)
58
+ cls._instance._implemented_backends = {}
59
+ return cls._instance
60
+
61
+ @property
62
+ def available_backends(self) -> list[str]:
63
+ """Return the available table backends."""
64
+ return list(self._implemented_backends.keys())
65
+
66
+ def get_backend(
67
+ self,
68
+ backend_name: str | None,
69
+ group_handler: ZarrGroupHandler,
70
+ index_key: str | None = None,
71
+ index_type: Literal["int", "str"] = "int",
72
+ ) -> TableBackendProtocol:
73
+ """Try to get a handler for the given store based on the metadata version."""
74
+ if backend_name is None:
75
+ # Default to anndata since it is currently
76
+ # the only backend in use.
77
+ backend_name = "anndata_v1"
78
+
79
+ if backend_name not in self._implemented_backends:
80
+ raise NgioValueError(f"Table backend {backend_name} not implemented.")
81
+ handler = self._implemented_backends[backend_name](
82
+ group_handler=group_handler, index_key=index_key, index_type=index_type
83
+ )
84
+ return handler
85
+
86
+ def add_backend(
87
+ self,
88
+ table_beckend: type[TableBackendProtocol],
89
+ overwrite: bool = False,
90
+ ):
91
+ """Register a new handler."""
92
+ backend_name = table_beckend.backend_name()
93
+ if backend_name in self._implemented_backends and not overwrite:
94
+ raise NgioValueError(
95
+ f"Table backend {backend_name} already implemented. "
96
+ "Use the `overwrite=True` parameter to overwrite it."
97
+ )
98
+ self._implemented_backends[backend_name] = table_beckend
99
+
100
+
101
+ ImplementedTableBackends().add_backend(AnnDataBackend)
102
+ ImplementedTableBackends().add_backend(JsonTableBackend)