spatiomic 0.4.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.
- spatiomic/__init__.py +22 -0
- spatiomic/__init__.pyi +18 -0
- spatiomic/_internal/__init__.py +9 -0
- spatiomic/_internal/_anndata_method.py +268 -0
- spatiomic/_internal/_check_channel_dimension.py +43 -0
- spatiomic/_internal/_data_method.py +85 -0
- spatiomic/_internal/_get_neighbor_finder.py +76 -0
- spatiomic/_internal/_import_package.py +55 -0
- spatiomic/_internal/_seed_method.py +64 -0
- spatiomic/_version.py +4 -0
- spatiomic/cluster/__init__.py +15 -0
- spatiomic/cluster/_agglomerative.py +94 -0
- spatiomic/cluster/_base.py +44 -0
- spatiomic/cluster/_kmeans.py +89 -0
- spatiomic/cluster/_leiden.py +120 -0
- spatiomic/data/__init__.py +13 -0
- spatiomic/data/_anndata_from_array.py +66 -0
- spatiomic/data/_read.py +205 -0
- spatiomic/data/_subsample.py +57 -0
- spatiomic/data/_subset.py +34 -0
- spatiomic/dimension/__init__.py +13 -0
- spatiomic/dimension/_base.py +101 -0
- spatiomic/dimension/_pca.py +240 -0
- spatiomic/dimension/_som.py +458 -0
- spatiomic/dimension/_tsne.py +123 -0
- spatiomic/dimension/_umap.py +155 -0
- spatiomic/neighbor/__init__.py +9 -0
- spatiomic/neighbor/_knn_graph.py +200 -0
- spatiomic/neighbor/_snn_graph.py +146 -0
- spatiomic/plot/__init__.py +41 -0
- spatiomic/plot/_cluster_contributor_histogram.py +64 -0
- spatiomic/plot/_cluster_contributors.py +209 -0
- spatiomic/plot/_cluster_image.py +75 -0
- spatiomic/plot/_cluster_legend.py +55 -0
- spatiomic/plot/_cluster_location.py +73 -0
- spatiomic/plot/_cluster_scatter.py +83 -0
- spatiomic/plot/_cluster_selection.py +126 -0
- spatiomic/plot/_colormap.py +116 -0
- spatiomic/plot/_connectivity_graph.py +125 -0
- spatiomic/plot/_marker_expression.py +77 -0
- spatiomic/plot/_registration_similarity.py +101 -0
- spatiomic/plot/_registration_slope.py +84 -0
- spatiomic/plot/_som_clusters.py +70 -0
- spatiomic/plot/_som_distance.py +59 -0
- spatiomic/plot/_som_marker_expression.py +57 -0
- spatiomic/plot/_spatial_graph.py +128 -0
- spatiomic/plot/_volcano.py +213 -0
- spatiomic/process/__init__.py +20 -0
- spatiomic/process/_base.py +88 -0
- spatiomic/process/_clip.py +296 -0
- spatiomic/process/_log1p.py +116 -0
- spatiomic/process/_normalize.py +204 -0
- spatiomic/process/_register.py +383 -0
- spatiomic/process/_zscore.py +173 -0
- spatiomic/spatial/__init__.py +30 -0
- spatiomic/spatial/_autocorrelation.py +95 -0
- spatiomic/spatial/_bivariate_correlation.py +128 -0
- spatiomic/spatial/_local_autocorrelation.py +95 -0
- spatiomic/spatial/_local_heteroskedasticity.py +105 -0
- spatiomic/spatial/_spatial_weights.py +136 -0
- spatiomic/spatial/_vicinity_composition.py +191 -0
- spatiomic/spatial/_vicinity_graph.py +70 -0
- spatiomic/tool/__init__.py +11 -0
- spatiomic/tool/_count_clusters.py +50 -0
- spatiomic/tool/_get_stats.py +376 -0
- spatiomic/tool/_mean_cluster_intensity.py +59 -0
- spatiomic-0.4.0.dist-info/METADATA +159 -0
- spatiomic-0.4.0.dist-info/RECORD +70 -0
- spatiomic-0.4.0.dist-info/WHEEL +4 -0
- spatiomic-0.4.0.dist-info/licenses/LICENSE +674 -0
spatiomic/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Library for spatial omics analysis. Lazy-loads submodules for faster import times."""
|
|
2
|
+
|
|
3
|
+
from importlib.util import find_spec
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
import lazy_loader as lazy
|
|
7
|
+
|
|
8
|
+
from ._version import __version__ as __version__
|
|
9
|
+
|
|
10
|
+
__getattr__, __lazy_dir__, _ = lazy.attach_stub(__name__, __file__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def __dir__() -> List[str]:
|
|
14
|
+
"""List the available submodules."""
|
|
15
|
+
return [*__lazy_dir__(), "__version__"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if find_spec("cuml") is not None:
|
|
19
|
+
import cuml # type: ignore
|
|
20
|
+
|
|
21
|
+
# Always return the cuml results as np.ndarray, this ensures compatibility between GPU and CPU algorithms
|
|
22
|
+
cuml.set_global_output_type("numpy")
|
spatiomic/__init__.pyi
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Spatiomic: Spatial omics analyses in Python."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
|
|
5
|
+
submodules = [
|
|
6
|
+
"cluster",
|
|
7
|
+
"data",
|
|
8
|
+
"dimension",
|
|
9
|
+
"neighbor",
|
|
10
|
+
"plot",
|
|
11
|
+
"process",
|
|
12
|
+
"spatial",
|
|
13
|
+
"tool",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
__all__ = submodules + ["__version__"]
|
|
17
|
+
|
|
18
|
+
from . import cluster, data, dimension, neighbor, plot, process, spatial, tool # noqa: F401
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Internal utility functions.
|
|
2
|
+
|
|
3
|
+
The functions in this module are not intended to be used directly by the user and are thus not accesible without
|
|
4
|
+
the explicit underscore file name imports from the top-level package.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
__all__: List[str] = []
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from anndata import AnnData
|
|
8
|
+
from numpy.typing import ArrayLike, NDArray
|
|
9
|
+
|
|
10
|
+
from ._import_package import import_package
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_data_from_anndata(
|
|
14
|
+
adata: AnnData,
|
|
15
|
+
input_attribute_name: Optional[str] = None,
|
|
16
|
+
input_layer_name: Optional[str] = None,
|
|
17
|
+
input_obs_name: Optional[str] = None,
|
|
18
|
+
input_obsm_name: Optional[str] = None,
|
|
19
|
+
input_var_name: Optional[str] = None,
|
|
20
|
+
input_varm_name: Optional[str] = None,
|
|
21
|
+
input_uns_name: Optional[str] = None,
|
|
22
|
+
) -> NDArray:
|
|
23
|
+
"""Extract NDArray data from AnnData object based on input parameters."""
|
|
24
|
+
if input_attribute_name is not None:
|
|
25
|
+
return_value = getattr(adata, input_attribute_name)
|
|
26
|
+
elif input_layer_name is not None:
|
|
27
|
+
return_value = adata.layers[input_layer_name]
|
|
28
|
+
elif input_obs_name is not None:
|
|
29
|
+
obs_data = adata.obs[input_obs_name]
|
|
30
|
+
return_value = obs_data.to_numpy() if isinstance(obs_data, (pd.DataFrame, pd.Series)) else obs_data
|
|
31
|
+
elif input_obsm_name is not None:
|
|
32
|
+
return_value = adata.obsm[input_obsm_name]
|
|
33
|
+
elif input_var_name is not None:
|
|
34
|
+
var_data = adata.var[input_var_name]
|
|
35
|
+
return_value = var_data.to_numpy() if isinstance(var_data, (pd.DataFrame, pd.Series)) else var_data
|
|
36
|
+
elif input_varm_name is not None:
|
|
37
|
+
return_value = adata.varm[input_varm_name]
|
|
38
|
+
elif input_uns_name is not None:
|
|
39
|
+
return_value = adata.uns[input_uns_name]
|
|
40
|
+
else:
|
|
41
|
+
return_value = adata.X.toarray()
|
|
42
|
+
|
|
43
|
+
if not isinstance(return_value, ArrayLike) and not isinstance(return_value, np.ndarray):
|
|
44
|
+
raise TypeError("Returned data must be a numpy array.")
|
|
45
|
+
elif isinstance(return_value, ArrayLike):
|
|
46
|
+
return_value = np.array(return_value)
|
|
47
|
+
|
|
48
|
+
return return_value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _store_data_in_anndata(
|
|
52
|
+
adata: AnnData,
|
|
53
|
+
result: NDArray,
|
|
54
|
+
output_layer_name: Optional[str] = None,
|
|
55
|
+
output_obsm_name: Optional[str] = None,
|
|
56
|
+
output_obs_name: Optional[str] = None,
|
|
57
|
+
output_varm_name: Optional[str] = None,
|
|
58
|
+
output_var_name: Optional[str] = None,
|
|
59
|
+
output_uns_name: Optional[str] = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Store result NDArray data into AnnData object based on output parameters."""
|
|
62
|
+
if output_layer_name is not None:
|
|
63
|
+
adata.layers[output_layer_name] = result
|
|
64
|
+
elif output_obsm_name is not None:
|
|
65
|
+
adata.obsm[output_obsm_name] = result
|
|
66
|
+
elif output_obs_name is not None:
|
|
67
|
+
adata.obs[output_obs_name] = result
|
|
68
|
+
elif output_varm_name is not None:
|
|
69
|
+
adata.varm[output_varm_name] = result
|
|
70
|
+
elif output_var_name is not None:
|
|
71
|
+
adata.var[output_var_name] = result
|
|
72
|
+
elif output_uns_name is not None:
|
|
73
|
+
adata.uns[output_uns_name] = result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def anndata_method(
|
|
77
|
+
pass_data: bool = True,
|
|
78
|
+
input_attribute: Optional[str] = None,
|
|
79
|
+
input_layer: Optional[str] = None,
|
|
80
|
+
input_obs: Optional[str] = None,
|
|
81
|
+
input_obsm: Optional[str] = None,
|
|
82
|
+
input_var: Optional[str] = None,
|
|
83
|
+
input_varm: Optional[str] = None,
|
|
84
|
+
input_uns: Optional[str] = None,
|
|
85
|
+
output_layer: Optional[str] = None,
|
|
86
|
+
output_obsm: Optional[str] = None,
|
|
87
|
+
output_obs: Optional[str] = None,
|
|
88
|
+
output_varm: Optional[str] = None,
|
|
89
|
+
output_var: Optional[str] = None,
|
|
90
|
+
output_uns: Optional[str] = None,
|
|
91
|
+
result_index: Optional[int] = None,
|
|
92
|
+
) -> Callable:
|
|
93
|
+
"""Decorator to handle AnnData input and output for methods.
|
|
94
|
+
|
|
95
|
+
This decorator simplifies the process of writing methods that can work with both AnnData objects
|
|
96
|
+
and raw NDArrays/DataFrames. It automatically extracts data from AnnData based on specified
|
|
97
|
+
input parameters and stores the result back into AnnData based on output parameters.
|
|
98
|
+
|
|
99
|
+
.. code-block:: python
|
|
100
|
+
@anndata_method(pass_data=True, input_obs="obs_column", output_uns="my_method_result")
|
|
101
|
+
def my_method(data: NDArray) -> NDArray:
|
|
102
|
+
return data
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
adata = AnnData(X=np.random.rand(10, 10))
|
|
106
|
+
adata.obs["obs_column"] = np.random.rand(10)
|
|
107
|
+
|
|
108
|
+
# The result will be stored in adata.uns["my_method_result"] but the method will still return the result
|
|
109
|
+
result = my_method(adata)
|
|
110
|
+
|
|
111
|
+
.. note::
|
|
112
|
+
User provided input/output parameter names will override the default parameter names. If the user provides
|
|
113
|
+
multiple input/output parameter names, the order of precedence is as follows:
|
|
114
|
+
input_attribute > input_layer > input_obs > input_obsm > input_var > input_varm > input_uns
|
|
115
|
+
output_layer > output_obs > output_obsm > output_var > output_varm > output_uns
|
|
116
|
+
|
|
117
|
+
.. note::
|
|
118
|
+
If the decorated method returns a tuple, the index of the element to store in AnnData can be specified
|
|
119
|
+
with the `result_index` parameter.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
pass_data (bool, optional): Whether to pass the extracted data to the decorated method. Defaults to True.
|
|
123
|
+
input_attribute (Optional[str]): Attribute name in AnnData to retrieve data from (e.g., 'raw').
|
|
124
|
+
input_layer (Optional[str]): Layer name in AnnData to retrieve data from.
|
|
125
|
+
input_obs (Optional[str]): .obs column name in AnnData to retrieve data from.
|
|
126
|
+
input_obsm (Optional[str]): .obsm name in AnnData to retrieve data from.
|
|
127
|
+
input_var (Optional[str]): .var column name in AnnData to retrieve data from.
|
|
128
|
+
input_varm (Optional[str]): .varm name in AnnData to retrieve data from.
|
|
129
|
+
input_uns (Optional[str]): .uns key in AnnData to retrieve data from.
|
|
130
|
+
output_layer (Optional[str]): Layer name in AnnData to store the result.
|
|
131
|
+
output_obsm (Optional[str]): .obsm name in AnnData to store the result.
|
|
132
|
+
output_obs (Optional[str]): .obs column name in AnnData to store the result.
|
|
133
|
+
output_varm (Optional[str]): .varm name in AnnData to store the result.
|
|
134
|
+
output_var (Optional[str]): .var column name in AnnData to store the result.
|
|
135
|
+
output_uns (Optional[str]): .uns key in AnnData to store the result.
|
|
136
|
+
result_index (Optional[int]): If the decorated method returns a tuple, this index specifies which element of the
|
|
137
|
+
tuple to store in AnnData. Defaults to None (store the first element).
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Callable: The decorated method.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
ValueError: If 'data' argument is missing when pass_data=True and no AnnData object found in arguments.
|
|
144
|
+
TypeError: If 'data' is not AnnData, DataFrame, or NDArray when pass_data=True.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def decorator(method: Callable) -> Callable:
|
|
148
|
+
@wraps(method)
|
|
149
|
+
def wrapper(
|
|
150
|
+
*args: Tuple[Any, ...],
|
|
151
|
+
**kwargs: Dict[str, Any],
|
|
152
|
+
) -> Any:
|
|
153
|
+
method_signature = inspect.signature(method)
|
|
154
|
+
parameters = method_signature.parameters
|
|
155
|
+
has_self = "self" in parameters
|
|
156
|
+
|
|
157
|
+
self_obj = None
|
|
158
|
+
processed_args: list[Any] = list(args) # Convert to list for easier manipulation
|
|
159
|
+
|
|
160
|
+
if has_self:
|
|
161
|
+
self_index = list(parameters.keys()).index("self")
|
|
162
|
+
if len(processed_args) > self_index:
|
|
163
|
+
self_obj = processed_args.pop(self_index)
|
|
164
|
+
|
|
165
|
+
if TYPE_CHECKING:
|
|
166
|
+
xp = np
|
|
167
|
+
else:
|
|
168
|
+
xp: "np" = import_package("cupy", alternative=np)
|
|
169
|
+
|
|
170
|
+
data_arg = kwargs.pop("data", None)
|
|
171
|
+
if (
|
|
172
|
+
data_arg is None
|
|
173
|
+
and processed_args
|
|
174
|
+
and isinstance(processed_args[0], (AnnData, pd.DataFrame, np.ndarray, xp.ndarray))
|
|
175
|
+
):
|
|
176
|
+
data_arg = processed_args.pop(0)
|
|
177
|
+
|
|
178
|
+
array_data = None
|
|
179
|
+
adata_obj = None
|
|
180
|
+
|
|
181
|
+
if isinstance(data_arg, AnnData):
|
|
182
|
+
adata_obj = data_arg
|
|
183
|
+
|
|
184
|
+
input_params: Dict[str, Optional[str]] = {
|
|
185
|
+
"input_attribute_name": kwargs.pop("input_attribute", input_attribute), # type: ignore
|
|
186
|
+
"input_layer_name": kwargs.pop("input_layer", input_layer), # type: ignore
|
|
187
|
+
"input_obs_name": kwargs.pop("input_obs", input_obs), # type: ignore
|
|
188
|
+
"input_obsm_name": kwargs.pop("input_obsm", input_obsm), # type: ignore
|
|
189
|
+
"input_var_name": kwargs.pop("input_var", input_var), # type: ignore
|
|
190
|
+
"input_varm_name": kwargs.pop("input_varm", input_varm), # type: ignore
|
|
191
|
+
"input_uns_name": kwargs.pop("input_uns", input_uns), # type: ignore
|
|
192
|
+
}
|
|
193
|
+
array_data = _get_data_from_anndata(adata_obj, **input_params)
|
|
194
|
+
|
|
195
|
+
output_params: Dict[str, Optional[str]] = {
|
|
196
|
+
"output_layer_name": kwargs.pop("output_layer", output_layer), # type: ignore
|
|
197
|
+
"output_obs_name": kwargs.pop("output_obs", output_obs), # type: ignore
|
|
198
|
+
"output_obsm_name": kwargs.pop("output_obsm", output_obsm), # type: ignore
|
|
199
|
+
"output_var_name": kwargs.pop("output_var", output_var), # type: ignore
|
|
200
|
+
"output_varm_name": kwargs.pop("output_varm", output_varm), # type: ignore
|
|
201
|
+
"output_uns_name": kwargs.pop("output_uns", output_uns), # type: ignore
|
|
202
|
+
}
|
|
203
|
+
result_index_parsed = kwargs.pop("result_index", result_index)
|
|
204
|
+
|
|
205
|
+
elif data_arg is not None and isinstance(data_arg, (pd.DataFrame, np.ndarray, xp.ndarray)):
|
|
206
|
+
array_data = data_arg
|
|
207
|
+
elif pass_data:
|
|
208
|
+
raise ValueError("Data argument missing.")
|
|
209
|
+
elif data_arg is not None: # this case should not raise TypeError if pass_data=False
|
|
210
|
+
raise TypeError("Data must be an AnnData object, DataFrame or NDArray if pass_data=True.")
|
|
211
|
+
|
|
212
|
+
method_args = [self_obj] if self_obj else []
|
|
213
|
+
if pass_data and array_data is not None: # Only pass data if pass_data is True and we have data to pass
|
|
214
|
+
method_args.append(array_data)
|
|
215
|
+
|
|
216
|
+
result = method(*method_args, *processed_args, **kwargs)
|
|
217
|
+
|
|
218
|
+
if adata_obj is not None:
|
|
219
|
+
ad_result_to_store = result
|
|
220
|
+
if isinstance(result, tuple):
|
|
221
|
+
result_index_parsed = 0 if result_index_parsed is None else result_index_parsed
|
|
222
|
+
if isinstance(result_index_parsed, int) and 0 <= result_index_parsed < len(result):
|
|
223
|
+
ad_result_to_store = result[result_index_parsed]
|
|
224
|
+
else:
|
|
225
|
+
ad_result_to_store = result[0] # default to first element if index invalid
|
|
226
|
+
|
|
227
|
+
if xp.__name__ == "cupy" and isinstance(ad_result_to_store, xp.ndarray):
|
|
228
|
+
ad_result_to_store = ad_result_to_store.get() # type: ignore
|
|
229
|
+
|
|
230
|
+
if isinstance(ad_result_to_store, np.ndarray): # only store numpy array-like results in AnnData
|
|
231
|
+
_store_data_in_anndata(
|
|
232
|
+
adata_obj,
|
|
233
|
+
ad_result_to_store,
|
|
234
|
+
**output_params,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
wrapper.__doc__ = method.__doc__
|
|
240
|
+
|
|
241
|
+
additional_doc = """
|
|
242
|
+
|
|
243
|
+
This method can be used with both AnnData objects and raw NDArrays/DataFrames. If an AnnData object is provided,
|
|
244
|
+
the data will be extracted based on the following parameters:
|
|
245
|
+
- input_attribute: Name of the attribute in AnnData to extract data from.
|
|
246
|
+
- input_layer: Name of the layer in AnnData to extract data from.
|
|
247
|
+
- input_obs: Column in AnnData.obs to extract data from.
|
|
248
|
+
- input_obsm: Key in AnnData.obsm to extract data from.
|
|
249
|
+
- input_var: Column in AnnData.var to extract data from.
|
|
250
|
+
- input_varm: Key in AnnData.varm to extract data from.
|
|
251
|
+
- input_uns: Key in AnnData.uns to extract data from.
|
|
252
|
+
|
|
253
|
+
The result will be stored back into the AnnData object based on the following parameters:
|
|
254
|
+
- output_layer: Name of the layer in AnnData to store the result.
|
|
255
|
+
- output_obs: Column in AnnData.obs to store the result.
|
|
256
|
+
- output_obsm: Key in AnnData.obsm to store the result.
|
|
257
|
+
- output_var: Column in AnnData.var to store the result.
|
|
258
|
+
- output_varm: Key in AnnData.varm to store the result.
|
|
259
|
+
- output_uns: Key in AnnData.uns to store the result.
|
|
260
|
+
"""
|
|
261
|
+
if wrapper.__doc__:
|
|
262
|
+
wrapper.__doc__ += additional_doc
|
|
263
|
+
else:
|
|
264
|
+
wrapper.__doc__ = additional_doc
|
|
265
|
+
|
|
266
|
+
return wrapper
|
|
267
|
+
|
|
268
|
+
return decorator
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from warnings import warn
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def check_channel_dimension(
|
|
6
|
+
data_shape: tuple,
|
|
7
|
+
dimension_count_min: Optional[int] = 2,
|
|
8
|
+
dimension_count_max: Optional[int] = None,
|
|
9
|
+
) -> None:
|
|
10
|
+
"""Check if the data has a channel dimension and warn if not or raise an error if the data is 2D and square.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
data_shape (tuple): The shape of the data.
|
|
14
|
+
dimension_count_min (Optional[int], optional): The minimum number of dimensions the data must have.
|
|
15
|
+
Defaults to 2.
|
|
16
|
+
dimension_count_max (Optional[int], optional): The maximum number of dimensions the data must have.
|
|
17
|
+
Defaults to None.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
ValueError: If the data is 2D and square.
|
|
21
|
+
|
|
22
|
+
Warns:
|
|
23
|
+
UserWarning: If the data is 2D and has no channel dimension but is not square.
|
|
24
|
+
"""
|
|
25
|
+
if dimension_count_min is not None:
|
|
26
|
+
assert len(data_shape) >= dimension_count_min, f"Data must have at least {dimension_count_min} dimensions."
|
|
27
|
+
if dimension_count_max is not None:
|
|
28
|
+
assert len(data_shape) <= dimension_count_max, f"Data must have at most {dimension_count_max} dimensions."
|
|
29
|
+
|
|
30
|
+
if len(data_shape) == 2:
|
|
31
|
+
# check if the image is close to a square
|
|
32
|
+
if abs(data_shape[0] - data_shape[1]) < max(data_shape) * 0.2:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
"Data has only two dimensions and is square, assuming image without channel dimension."
|
|
35
|
+
"Please add a channel dimension to the data for correct processing."
|
|
36
|
+
)
|
|
37
|
+
else:
|
|
38
|
+
warn(
|
|
39
|
+
"Data has only two dimensions. Assuming flat data with one channel dimension."
|
|
40
|
+
" This will yield unwanted results when called on 2D images without channel dimension.",
|
|
41
|
+
UserWarning,
|
|
42
|
+
stacklevel=2,
|
|
43
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import warnings
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from spatiomic._internal._import_package import import_package
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def data_method(method: Callable) -> Callable:
|
|
13
|
+
"""Automatically check and get the values from the data parameter for the decorated function.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
method (Callable): The decorated method that takes the processed data.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Callable: A wrapper function that invokes the passed method with the processed data.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@wraps(method)
|
|
23
|
+
def wrapper(
|
|
24
|
+
*args: Any,
|
|
25
|
+
**kwargs: dict,
|
|
26
|
+
) -> Any:
|
|
27
|
+
"""Get float NDArray data from passed data and invoke the decorated method with the processed data.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
self (object): The reference to the object containing the decorated method.
|
|
31
|
+
data (Union[NDArray, pd.DataFrame, MultiplexedImage, MultiplexedImageCollection]): The data source.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Any: The result of the invoked method.
|
|
35
|
+
"""
|
|
36
|
+
# Check if 'self' is present as an argument
|
|
37
|
+
method_signature = inspect.signature(method)
|
|
38
|
+
parameters = method_signature.parameters
|
|
39
|
+
|
|
40
|
+
has_self = "self" in parameters
|
|
41
|
+
|
|
42
|
+
if has_self:
|
|
43
|
+
self_index = list(parameters.keys()).index("self")
|
|
44
|
+
self = args[self_index]
|
|
45
|
+
args = args[:self_index] + args[self_index + 1 :]
|
|
46
|
+
else:
|
|
47
|
+
self = None
|
|
48
|
+
|
|
49
|
+
# Check if 'data' is present as a keyword argument, otherwise check if it is the first argument
|
|
50
|
+
data = kwargs.pop("data", None)
|
|
51
|
+
if data is None and len(args) > 0:
|
|
52
|
+
data = args[0]
|
|
53
|
+
args = args[1:]
|
|
54
|
+
|
|
55
|
+
if TYPE_CHECKING:
|
|
56
|
+
xp = np
|
|
57
|
+
else:
|
|
58
|
+
xp: "np" = import_package("cupy", alternative=np)
|
|
59
|
+
|
|
60
|
+
assert data is not None and (isinstance(data, (pd.DataFrame, np.ndarray, xp.ndarray))), (
|
|
61
|
+
"DataFrame or numpy array required."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# convert dataframes to numpy arrays
|
|
65
|
+
if isinstance(data, pd.DataFrame):
|
|
66
|
+
data = data.to_numpy()
|
|
67
|
+
|
|
68
|
+
# check that data is float
|
|
69
|
+
if data.dtype not in [ # type: ignore
|
|
70
|
+
np.float32,
|
|
71
|
+
np.float64,
|
|
72
|
+
xp.float32,
|
|
73
|
+
xp.float64,
|
|
74
|
+
]:
|
|
75
|
+
warnings.warn(
|
|
76
|
+
"Data does not appear to be a numpy/cupy float, casting to numpy float32.",
|
|
77
|
+
UserWarning,
|
|
78
|
+
stacklevel=2,
|
|
79
|
+
)
|
|
80
|
+
data = np.float32(data if isinstance(data, np.ndarray) else data.get()) # type: ignore
|
|
81
|
+
|
|
82
|
+
return method(self, data, *args, **kwargs) if self is not None else method(data, *args, **kwargs)
|
|
83
|
+
|
|
84
|
+
wrapper.__doc__ = method.__doc__
|
|
85
|
+
return wrapper
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Get a nearest neighbor finder."""
|
|
2
|
+
|
|
3
|
+
from typing import Callable, Literal, Union
|
|
4
|
+
from warnings import warn
|
|
5
|
+
|
|
6
|
+
from scipy.spatial.distance import correlation, cosine
|
|
7
|
+
from sklearn.neighbors import NearestNeighbors as NearestNeighborsSk
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_neighbor_finder(
|
|
11
|
+
neighbor_count: int,
|
|
12
|
+
channel_count: int,
|
|
13
|
+
distance_metric: Literal["euclidean", "manhattan", "correlation", "cosine"] = "euclidean",
|
|
14
|
+
accuracy: Literal["fast", "accurate"] = "accurate",
|
|
15
|
+
job_count: int = -1,
|
|
16
|
+
use_gpu: bool = True,
|
|
17
|
+
) -> NearestNeighborsSk:
|
|
18
|
+
"""Get the nearest neighbor finder.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
neighbor_count (int): The neighbor count for the neighborhood graph.
|
|
22
|
+
channel_count (int): The number of channels in the data.
|
|
23
|
+
distance_metric (Literal["euclidean", "manhattan", "correlation", "cosine"], optional): The distance metric
|
|
24
|
+
to be used for nearest neighbor calculation. Defaults to "euclidean".
|
|
25
|
+
accuracy (Literal["fast", "accurate"], optional): The accuracy of the nearest neighbor calculation. Used
|
|
26
|
+
only when method is `simple` or `batch_balanced`. Defaults to "accurate".
|
|
27
|
+
job_count (int, optional): Parallelization core count when method is `simple`. Defaults to -1.
|
|
28
|
+
use_gpu (bool, optional): Whether to use the GPU for nearest neighbor calculation if possible.
|
|
29
|
+
Defaults to True.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Union[NearestNeighborsSk, Callable]: The nearest neighbor finder.
|
|
33
|
+
"""
|
|
34
|
+
if use_gpu:
|
|
35
|
+
try:
|
|
36
|
+
from cuml.neighbors import NearestNeighbors # type: ignore
|
|
37
|
+
except Exception:
|
|
38
|
+
warn(
|
|
39
|
+
"Could not import cuml implementation. Switching to non-GPU implementation.", UserWarning, stacklevel=2
|
|
40
|
+
)
|
|
41
|
+
use_gpu = False
|
|
42
|
+
|
|
43
|
+
if use_gpu:
|
|
44
|
+
if distance_metric != "euclidean" and accuracy == "fast" and channel_count <= 3:
|
|
45
|
+
warn(
|
|
46
|
+
"The fast accuracy setting is not available for the specified distance metric and data shape."
|
|
47
|
+
+ " Falling back to the accurate setting.",
|
|
48
|
+
UserWarning,
|
|
49
|
+
stacklevel=2,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
neighbor_finder = NearestNeighbors(
|
|
53
|
+
n_neighbors=neighbor_count,
|
|
54
|
+
algorithm=(
|
|
55
|
+
"rbc" if distance_metric == "euclidean" and accuracy == "fast" and channel_count <= 3 else "brute"
|
|
56
|
+
),
|
|
57
|
+
metric=distance_metric,
|
|
58
|
+
output_type="numpy",
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
metric: Union[str, Callable] = distance_metric
|
|
62
|
+
|
|
63
|
+
if distance_metric == "correlation":
|
|
64
|
+
metric = correlation
|
|
65
|
+
elif distance_metric == "cosine":
|
|
66
|
+
metric = cosine
|
|
67
|
+
|
|
68
|
+
# neighbor fitting
|
|
69
|
+
neighbor_finder = NearestNeighborsSk(
|
|
70
|
+
n_neighbors=neighbor_count,
|
|
71
|
+
algorithm=("ball_tree" if accuracy == "fast" else "brute"),
|
|
72
|
+
metric=metric,
|
|
73
|
+
n_jobs=job_count,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return neighbor_finder
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from types import ModuleType
|
|
3
|
+
from typing import Callable, Optional, Tuple, Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def import_package(
|
|
7
|
+
package_name: str,
|
|
8
|
+
package_attribute: Optional[str] = None,
|
|
9
|
+
alternative: Optional[ModuleType] = None,
|
|
10
|
+
message: Optional[str] = None,
|
|
11
|
+
raise_error: bool = True,
|
|
12
|
+
return_success: bool = False,
|
|
13
|
+
) -> Optional[Union[Union[ModuleType, Callable], Tuple[Optional[Union[ModuleType, Callable]], bool]]]:
|
|
14
|
+
"""Import a package, if it fails, return an alternative and state that an alternative is being used.
|
|
15
|
+
|
|
16
|
+
If no alternative is provided, raise an error, informing the user of the missing package.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
package_name (str): The name of the package to import. May be a submodule, e.g. "numpy.random".
|
|
20
|
+
package_attribute (Optional[str], optional): The attribute of the package to return, e.g., a function or class.
|
|
21
|
+
Defaults to None.
|
|
22
|
+
alternative (ModuleType): The alternative to use if the package cannot be imported. Defaults to None.
|
|
23
|
+
message (Optional[str], optional): The message to print if the package cannot be imported. Defaults to None.
|
|
24
|
+
raise_error (bool, optional): Whether to raise an error if the package cannot be imported and no alternative is
|
|
25
|
+
provided. Defaults to True.
|
|
26
|
+
return_success (bool, optional): Whether to return a boolean indicating whether the import was successful.
|
|
27
|
+
Defaults to False.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Union[Union[ModuleType, Callable], Tuple[Union[ModuleType, Callable], bool]]: The imported package, or the
|
|
31
|
+
alternative if the package could not be imported. If return_success is True, also returns a boolean
|
|
32
|
+
indicating whether the import was successful.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ImportError: If the package cannot be imported and no alternative is provided.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
package = importlib.import_module(package_name)
|
|
39
|
+
|
|
40
|
+
if package_attribute is not None:
|
|
41
|
+
package = getattr(package, package_attribute)
|
|
42
|
+
except ImportError as excp:
|
|
43
|
+
if alternative is not None:
|
|
44
|
+
if message is not None:
|
|
45
|
+
print(message)
|
|
46
|
+
|
|
47
|
+
return alternative if not return_success else (alternative, False)
|
|
48
|
+
elif raise_error:
|
|
49
|
+
raise ImportError(
|
|
50
|
+
f"Could not import {package_name}. Please install it in order to use this functionality."
|
|
51
|
+
) from excp
|
|
52
|
+
else:
|
|
53
|
+
return None if not return_success else (None, False)
|
|
54
|
+
else:
|
|
55
|
+
return package if not return_success else (package, True)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import random
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Any, Callable, Optional
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def seed_method(method: Callable) -> Callable:
|
|
10
|
+
"""Set the `numpy` and `random` random states to a provided seed and reverts it after method execution.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
method (Callable): The method to be invoked after setting the random state.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Callable: The wrapper function that invokes the method.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@wraps(method)
|
|
20
|
+
def wrapper(
|
|
21
|
+
*args: Any,
|
|
22
|
+
**kwargs: dict,
|
|
23
|
+
) -> Any:
|
|
24
|
+
"""Set the `numpy` and `random` random states to a provided seed and reverts it after method execution.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
ref (object): The reference to the object containing the decorated method.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Any: The result of the invoked method.
|
|
31
|
+
"""
|
|
32
|
+
# Check if 'self' is present as an argument
|
|
33
|
+
method_signature = inspect.signature(method)
|
|
34
|
+
parameters = method_signature.parameters
|
|
35
|
+
|
|
36
|
+
has_self = "self" in parameters
|
|
37
|
+
|
|
38
|
+
if has_self:
|
|
39
|
+
self_index = list(parameters.keys()).index("self")
|
|
40
|
+
self = args[self_index]
|
|
41
|
+
args = args[:self_index] + args[self_index + 1 :]
|
|
42
|
+
else:
|
|
43
|
+
self = None
|
|
44
|
+
|
|
45
|
+
seed: Optional[int] = kwargs["seed"] if "seed" in kwargs and isinstance(kwargs["seed"], int) else None
|
|
46
|
+
|
|
47
|
+
random_state_np = np.random.get_state()
|
|
48
|
+
random_state_sys = random.getstate()
|
|
49
|
+
|
|
50
|
+
if seed is not None:
|
|
51
|
+
np.random.seed(seed)
|
|
52
|
+
random.seed(seed)
|
|
53
|
+
|
|
54
|
+
result = method(self, *args, **kwargs) if self is not None else method(*args, **kwargs)
|
|
55
|
+
|
|
56
|
+
# reset the state
|
|
57
|
+
if seed is not None:
|
|
58
|
+
np.random.set_state(random_state_np)
|
|
59
|
+
random.setstate(random_state_sys)
|
|
60
|
+
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
wrapper.__doc__ = method.__doc__
|
|
64
|
+
return wrapper
|
spatiomic/_version.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Make all clustering classes available in the cluster submodule."""
|
|
2
|
+
|
|
3
|
+
from spatiomic.dimension._som import Som as som
|
|
4
|
+
|
|
5
|
+
from ._agglomerative import Agglomerative as agglomerative
|
|
6
|
+
from ._kmeans import KMeans as kmeans
|
|
7
|
+
from ._leiden import Leiden as leiden
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"agglomerative",
|
|
11
|
+
"biclustering",
|
|
12
|
+
"kmeans",
|
|
13
|
+
"leiden",
|
|
14
|
+
"som",
|
|
15
|
+
]
|