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.
Files changed (70) hide show
  1. spatiomic/__init__.py +22 -0
  2. spatiomic/__init__.pyi +18 -0
  3. spatiomic/_internal/__init__.py +9 -0
  4. spatiomic/_internal/_anndata_method.py +268 -0
  5. spatiomic/_internal/_check_channel_dimension.py +43 -0
  6. spatiomic/_internal/_data_method.py +85 -0
  7. spatiomic/_internal/_get_neighbor_finder.py +76 -0
  8. spatiomic/_internal/_import_package.py +55 -0
  9. spatiomic/_internal/_seed_method.py +64 -0
  10. spatiomic/_version.py +4 -0
  11. spatiomic/cluster/__init__.py +15 -0
  12. spatiomic/cluster/_agglomerative.py +94 -0
  13. spatiomic/cluster/_base.py +44 -0
  14. spatiomic/cluster/_kmeans.py +89 -0
  15. spatiomic/cluster/_leiden.py +120 -0
  16. spatiomic/data/__init__.py +13 -0
  17. spatiomic/data/_anndata_from_array.py +66 -0
  18. spatiomic/data/_read.py +205 -0
  19. spatiomic/data/_subsample.py +57 -0
  20. spatiomic/data/_subset.py +34 -0
  21. spatiomic/dimension/__init__.py +13 -0
  22. spatiomic/dimension/_base.py +101 -0
  23. spatiomic/dimension/_pca.py +240 -0
  24. spatiomic/dimension/_som.py +458 -0
  25. spatiomic/dimension/_tsne.py +123 -0
  26. spatiomic/dimension/_umap.py +155 -0
  27. spatiomic/neighbor/__init__.py +9 -0
  28. spatiomic/neighbor/_knn_graph.py +200 -0
  29. spatiomic/neighbor/_snn_graph.py +146 -0
  30. spatiomic/plot/__init__.py +41 -0
  31. spatiomic/plot/_cluster_contributor_histogram.py +64 -0
  32. spatiomic/plot/_cluster_contributors.py +209 -0
  33. spatiomic/plot/_cluster_image.py +75 -0
  34. spatiomic/plot/_cluster_legend.py +55 -0
  35. spatiomic/plot/_cluster_location.py +73 -0
  36. spatiomic/plot/_cluster_scatter.py +83 -0
  37. spatiomic/plot/_cluster_selection.py +126 -0
  38. spatiomic/plot/_colormap.py +116 -0
  39. spatiomic/plot/_connectivity_graph.py +125 -0
  40. spatiomic/plot/_marker_expression.py +77 -0
  41. spatiomic/plot/_registration_similarity.py +101 -0
  42. spatiomic/plot/_registration_slope.py +84 -0
  43. spatiomic/plot/_som_clusters.py +70 -0
  44. spatiomic/plot/_som_distance.py +59 -0
  45. spatiomic/plot/_som_marker_expression.py +57 -0
  46. spatiomic/plot/_spatial_graph.py +128 -0
  47. spatiomic/plot/_volcano.py +213 -0
  48. spatiomic/process/__init__.py +20 -0
  49. spatiomic/process/_base.py +88 -0
  50. spatiomic/process/_clip.py +296 -0
  51. spatiomic/process/_log1p.py +116 -0
  52. spatiomic/process/_normalize.py +204 -0
  53. spatiomic/process/_register.py +383 -0
  54. spatiomic/process/_zscore.py +173 -0
  55. spatiomic/spatial/__init__.py +30 -0
  56. spatiomic/spatial/_autocorrelation.py +95 -0
  57. spatiomic/spatial/_bivariate_correlation.py +128 -0
  58. spatiomic/spatial/_local_autocorrelation.py +95 -0
  59. spatiomic/spatial/_local_heteroskedasticity.py +105 -0
  60. spatiomic/spatial/_spatial_weights.py +136 -0
  61. spatiomic/spatial/_vicinity_composition.py +191 -0
  62. spatiomic/spatial/_vicinity_graph.py +70 -0
  63. spatiomic/tool/__init__.py +11 -0
  64. spatiomic/tool/_count_clusters.py +50 -0
  65. spatiomic/tool/_get_stats.py +376 -0
  66. spatiomic/tool/_mean_cluster_intensity.py +59 -0
  67. spatiomic-0.4.0.dist-info/METADATA +159 -0
  68. spatiomic-0.4.0.dist-info/RECORD +70 -0
  69. spatiomic-0.4.0.dist-info/WHEEL +4 -0
  70. 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,4 @@
1
+ """Version information for the spatiomic package."""
2
+
3
+ # This package will follow Semantic Versioning after version 1.0.0: https://semver.org/
4
+ __version__ = "0.4.0"
@@ -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
+ ]