From 2089d25c401a377d51ab600b2a7beb5f9f573fe9 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Fri, 12 Sep 2025 14:42:43 +1000 Subject: [PATCH 1/7] Add leiden clustering. --- stlearn/tl/clustering/__init__.py | 1 + stlearn/tl/clustering/leiden.py | 94 +++++++++++++++++++++++++++++++ stlearn/tl/clustering/louvain.py | 8 ++- 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 stlearn/tl/clustering/leiden.py diff --git a/stlearn/tl/clustering/__init__.py b/stlearn/tl/clustering/__init__.py index d68d6df2..062244fd 100644 --- a/stlearn/tl/clustering/__init__.py +++ b/stlearn/tl/clustering/__init__.py @@ -5,5 +5,6 @@ __all__ = [ "kmeans", "louvain", + "leiden", "annotate_interactive", ] diff --git a/stlearn/tl/clustering/leiden.py b/stlearn/tl/clustering/leiden.py new file mode 100644 index 00000000..50936f40 --- /dev/null +++ b/stlearn/tl/clustering/leiden.py @@ -0,0 +1,94 @@ +from collections.abc import Mapping, Sequence +from types import MappingProxyType +from typing import Any, Literal + +import scanpy +from anndata import AnnData +from louvain.VertexPartition import MutableVertexPartition +from numpy.random.mtrand import RandomState +from scipy.sparse import spmatrix + + +def louvain( + adata: AnnData, + resolution: float | None = None, + random_state: int | RandomState | None = 0, + restrict_to: tuple[str, Sequence[str]] | None = None, + key_added: str = "leiden", + adjacency: spmatrix | None = None, + directed: bool = True, + use_weights: bool = False, + partition_type: type[MutableVertexPartition] | None = None, + obsp: str | None = None, + copy: bool = False, +) -> AnnData | None: + """\ + Wrap function scanpy.tl.leiden + + This requires having ran :func:`~scanpy.pp.neighbors` or + :func:`~scanpy.external.pp.bbknn` first, + or explicitly passing a ``adjacency`` matrix. + Parameters + ---------- + adata: + The annotated data matrix. + resolution: + A parameter value controlling the coarseness of the clustering. + Higher values lead to more clusters. + Set to `None` if overriding `partition_type` + to one that doesn’t accept a `resolution_parameter`. + random_state: + Change the initialization of the optimization. + restrict_to: + Restrict the cluster to the categories within the key for sample + annotation, tuple needs to contain ``(obs_key, list_of_categories)``. + key_added: + Key under which to add the cluster labels. (default: ``'leiden'``) + adjacency: + Sparse adjacency matrix of the graph, defaults to + ``adata.uns['neighbors']['connectivities']``. + directed: + Interpret the ``adjacency`` matrix as directed graph? + use_weights: + Use weights from knn graph. + partition_type: + Type of partition to use. + Defaults to :class:`~leidenalg.RBConfigurationVertexPartition`. + For the available options, consult the documentation for + :func:`~leidenalg.find_partition`. + obsp: + Use .obsp[obsp] as adjacency. You can't specify both + `obsp` and `neighbors_key` at the same time. + copy: + Copy adata or modify it inplace. + Returns + ------- + :obj:`None` + By default (``copy=False``), updates ``adata`` with the following fields: + ``adata.obs['leiden' | key_added]`` (:class:`pandas.Series`, dtype ``category``) + Array of dim (number of samples) that stores the subgroup id + (``'0'``, ``'1'``, ...) for each cell. + :class:`~anndata.AnnData` + When ``copy=True`` is set, a copy of ``adata`` with those fields is returned. + """ + + print("Applying Leiden cluster ...") + adata = scanpy.tl.leiden( + adata, + resolution=resolution, + restrict_to=restrict_to, + random_state=random_state, + key_added=key_added, + adjacency=adjacency, + directed=directed, + use_weights=use_weights, + partition_type=partition_type, + obsp=obsp, + copy=copy, + ) + + print( + "Leiden cluster is done! The labels are stored in adata.obs['%s']" % key_added + ) + + return adata diff --git a/stlearn/tl/clustering/louvain.py b/stlearn/tl/clustering/louvain.py index 78e973dd..09c7db54 100644 --- a/stlearn/tl/clustering/louvain.py +++ b/stlearn/tl/clustering/louvain.py @@ -21,6 +21,7 @@ def louvain( use_weights: bool = False, partition_type: type[MutableVertexPartition] | None = None, partition_kwargs: Mapping[str, Any] = MappingProxyType({}), + obsp: str | None = None, copy: bool = False, ) -> AnnData | None: """\ @@ -64,6 +65,9 @@ def louvain( partition_kwargs: Key word arguments to pass to partitioning, if ``vtraag`` method is being used. + obsp: + Use .obsp[obsp] as adjacency. You can't specify both + `obsp` and `neighbors_key` at the same time. copy: Copy adata or modify it inplace. Returns @@ -77,6 +81,7 @@ def louvain( When ``copy=True`` is set, a copy of ``adata`` with those fields is returned. """ + print("Applying Louvain cluster ...") adata = scanpy.tl.louvain( adata, resolution=resolution, @@ -89,10 +94,9 @@ def louvain( use_weights=use_weights, partition_type=partition_type, partition_kwargs=partition_kwargs, + obsp=obsp, copy=copy, ) - - print("Applying Louvain cluster ...") print( "Louvain cluster is done! The labels are stored in adata.obs['%s']" % key_added ) From e4d48b47acb4e426c42e80c97cb66134a204054a Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 12:21:38 +1000 Subject: [PATCH 2/7] Fix documentation and slight refactor. --- stlearn/spatial/SME/__init__.py | 4 +- stlearn/spatial/SME/_weighting_matrix.py | 123 ++++++++---------- stlearn/spatial/SME/normalize.py | 34 +++-- .../spatial/SME/{impute.py => pseudo_spot.py} | 80 +----------- stlearn/spatial/SME/sme_impute0.py | 84 ++++++++++++ 5 files changed, 168 insertions(+), 157 deletions(-) rename stlearn/spatial/SME/{impute.py => pseudo_spot.py} (77%) create mode 100644 stlearn/spatial/SME/sme_impute0.py diff --git a/stlearn/spatial/SME/__init__.py b/stlearn/spatial/SME/__init__.py index 8fffb497..27f1dc29 100644 --- a/stlearn/spatial/SME/__init__.py +++ b/stlearn/spatial/SME/__init__.py @@ -1,8 +1,8 @@ -from .impute import SME_impute0, pseudo_spot +from .impute import pseudo_spot +from .sme_impute0 import SME_impute0 from .normalize import SME_normalize __all__ = [ "SME_normalize", - "SME_impute0", "pseudo_spot", ] diff --git a/stlearn/spatial/SME/_weighting_matrix.py b/stlearn/spatial/SME/_weighting_matrix.py index 12848161..4df84279 100644 --- a/stlearn/spatial/SME/_weighting_matrix.py +++ b/stlearn/spatial/SME/_weighting_matrix.py @@ -4,6 +4,8 @@ from anndata import AnnData from sklearn.metrics import pairwise_distances from tqdm import tqdm +from sklearn.linear_model import LinearRegression +import math _PLATFORM = Literal["Visium", "Old_ST"] _WEIGHTING_MATRIX = Literal[ @@ -17,17 +19,7 @@ ] -def calculate_weight_matrix( - adata: AnnData, - adata_imputed: AnnData | None = None, - pseudo_spots: bool = False, - platform: _PLATFORM = "Visium", -) -> AnnData | None: - import math - - from sklearn.linear_model import LinearRegression - - rate: float +def row_col_by_platform(adata, platform): if platform == "Visium": img_row = adata.obs["imagerow"] img_col = adata.obs["imagecol"] @@ -46,64 +38,61 @@ def calculate_weight_matrix( {platform!r} does not support. """ ) - reg_row = LinearRegression().fit(array_row.values.reshape(-1, 1), img_row) - reg_col = LinearRegression().fit(array_col.values.reshape(-1, 1), img_col) - - if pseudo_spots and adata_imputed: - pd = pairwise_distances( - adata_imputed.obs[["imagecol", "imagerow"]], - adata.obs[["imagecol", "imagerow"]], - metric="euclidean", - ) - unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) - pd_norm = np.where(pd >= unit, 0, 1) - - md = 1 - pairwise_distances( - adata_imputed.obsm["X_morphology"], - adata.obsm["X_morphology"], - metric="cosine", - ) - md[md < 0] = 0 - - adata_imputed.uns["physical_distance"] = pd_norm - adata_imputed.uns["morphological_distance"] = md - - adata_imputed.uns["weights_matrix_all"] = ( - adata_imputed.uns["physical_distance"] - * adata_imputed.uns["morphological_distance"] - ) - - else: - pd = pairwise_distances(adata.obs[["imagecol", "imagerow"]], metric="euclidean") - unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) - pd_norm = np.where(pd >= rate * unit, 0, 1) - - md = 1 - pairwise_distances(adata.obsm["X_morphology"], metric="cosine") - md[md < 0] = 0 - - gd = 1 - pairwise_distances(adata.obsm["X_pca"], metric="correlation") - adata.uns["gene_expression_correlation"] = gd - adata.uns["physical_distance"] = pd_norm - adata.uns["morphological_distance"] = md - - adata.uns["weights_matrix_all"] = ( - adata.uns["physical_distance"] - * adata.uns["morphological_distance"] - * adata.uns["gene_expression_correlation"] - ) - adata.uns["weights_matrix_pd_gd"] = ( - adata.uns["physical_distance"] * adata.uns["gene_expression_correlation"] - ) - adata.uns["weights_matrix_pd_md"] = ( - adata.uns["physical_distance"] * adata.uns["morphological_distance"] - ) - adata.uns["weights_matrix_gd_md"] = ( - adata.uns["gene_expression_correlation"] - * adata.uns["morphological_distance"] - ) - return adata + return reg_col, reg_row, rate + + +def weight_matrix(adata, platform): + reg_col, reg_row, rate = row_col_by_platform(adata, platform) + pd = pairwise_distances(adata.obs[["imagecol", "imagerow"]], metric="euclidean") + unit = math.sqrt(reg_row.coef_ ** 2 + reg_col.coef_ ** 2) + pd_norm = np.where(pd >= rate * unit, 0, 1) + md = 1 - pairwise_distances(adata.obsm["X_morphology"], metric="cosine") + md[md < 0] = 0 + gd = 1 - pairwise_distances(adata.obsm["X_pca"], metric="correlation") + adata.uns["gene_expression_correlation"] = gd + adata.uns["physical_distance"] = pd_norm + adata.uns["morphological_distance"] = md + adata.uns["weights_matrix_all"] = ( + adata.uns["physical_distance"] + * adata.uns["morphological_distance"] + * adata.uns["gene_expression_correlation"] + ) + adata.uns["weights_matrix_pd_gd"] = ( + adata.uns["physical_distance"] * adata.uns["gene_expression_correlation"] + ) + adata.uns["weights_matrix_pd_md"] = ( + adata.uns["physical_distance"] * adata.uns["morphological_distance"] + ) + adata.uns["weights_matrix_gd_md"] = ( + adata.uns["gene_expression_correlation"] + * adata.uns["morphological_distance"] + ) + + +def weight_matrix_imputed(adata, adata_imputed, platform): + reg_col, reg_row, _ = row_col_by_platform(adata, platform) + + pd = pairwise_distances( + adata_imputed.obs[["imagecol", "imagerow"]], + adata.obs[["imagecol", "imagerow"]], + metric="euclidean", + ) + unit = math.sqrt(reg_row.coef_ ** 2 + reg_col.coef_ ** 2) + pd_norm = np.where(pd >= unit, 0, 1) + md = 1 - pairwise_distances( + adata_imputed.obsm["X_morphology"], + adata.obsm["X_morphology"], + metric="cosine", + ) + md[md < 0] = 0 + adata_imputed.uns["physical_distance"] = pd_norm + adata_imputed.uns["morphological_distance"] = md + adata_imputed.uns["weights_matrix_all"] = ( + adata_imputed.uns["physical_distance"] + * adata_imputed.uns["morphological_distance"] + ) def impute_neighbour( diff --git a/stlearn/spatial/SME/normalize.py b/stlearn/spatial/SME/normalize.py index 39f65207..c097b390 100644 --- a/stlearn/spatial/SME/normalize.py +++ b/stlearn/spatial/SME/normalize.py @@ -6,8 +6,7 @@ from ._weighting_matrix import ( _PLATFORM, _WEIGHTING_MATRIX, - calculate_weight_matrix, - impute_neighbour, + impute_neighbour, weight_matrix, ) @@ -19,8 +18,12 @@ def SME_normalize( copy: bool = False, ) -> AnnData | None: """\ - using spatial location (S), tissue morphological feature (M) and gene - expression (E) information to normalize data. + Reduce technical noise by spatially smoothing all expression values using + spatial, morphological, and expression (SME) information. + + This function modified ALL expression values by averaging each spot's expression + with weighted contributions from similar neighbors. It modifies ALL expression + values to reduce technical noise across the entire dataset. Parameters ---------- @@ -28,19 +31,24 @@ def SME_normalize( Annotated data matrix. use_data: Input data, can be `raw` counts or log transformed data - weights: - Weighting matrix for imputation. - if `weights_matrix_all`, matrix combined all information from spatial - location (S), tissue morphological feature (M) and gene expression (E) - if `weights_matrix_pd_md`, matrix combined information from spatial - location (S), tissue morphological feature (M) + weights : _WEIGHTING_MATRIX, default="weights_matrix_all" + Strategy for computing neighbor similarity weights: + - "weights_matrix_all": Combines spatial location (S) + + morphological features (M) + gene expression correlation (E). + - "weights_matrix_pd_gd": Physical distance + gene expression correlation only. + - "weights_matrix_pd_md": Physical distance + morphological features only. + - "weights_matrix_gd_md": Gene expression + morphological features only. + - "gene_expression_correlation": Expression similarity only. + - "physical_distance": Spatial proximity only. + - "morphological_distance": Tissue morphology similarity only. platform: `Visium` or `Old_ST` copy: - Return a copy instead of writing to adata. + If True, return a copy instead of writing to adata. If False, modify adata + in place and return None. Returns ------- - Anndata + AnnData or None """ adata = adata.copy() if copy else adata @@ -60,7 +68,7 @@ def SME_normalize( else: count_embed = adata.obsm[use_data] - calculate_weight_matrix(adata, platform=platform) + weight_matrix(adata, platform=platform) impute_neighbour(adata, count_embed=count_embed, weights=weights) diff --git a/stlearn/spatial/SME/impute.py b/stlearn/spatial/SME/pseudo_spot.py similarity index 77% rename from stlearn/spatial/SME/impute.py rename to stlearn/spatial/SME/pseudo_spot.py index 68a20dc3..7151d37d 100644 --- a/stlearn/spatial/SME/impute.py +++ b/stlearn/spatial/SME/pseudo_spot.py @@ -12,79 +12,10 @@ from ._weighting_matrix import ( _PLATFORM, _WEIGHTING_MATRIX, - calculate_weight_matrix, impute_neighbour, + weight_matrix_imputed ) - -def SME_impute0( - adata: AnnData, - use_data: str = "raw", - weights: _WEIGHTING_MATRIX = "weights_matrix_all", - platform: _PLATFORM = "Visium", - copy: bool = False, -) -> AnnData | None: - """\ - using spatial location (S), tissue morphological feature (M) and gene - expression (E) information to impute missing values - - Parameters - ---------- - adata - Annotated data matrix. - use_data - input data, can be `raw` counts or log transformed data - weights - weighting matrix for imputation. - if `weights_matrix_all`, matrix combined all information from spatial - location (S), tissue morphological feature (M) and gene expression (E) - if `weights_matrix_pd_md`, matrix combined information from spatial - location (S), tissue morphological feature (M) - platform - `Visium` or `Old_ST` - copy - Return a copy instead of writing to adata. - Returns - ------- - Anndata - """ - adata = adata.copy() if copy else adata - - if use_data == "raw": - if isinstance(adata.X, csr_matrix): - count_embed = adata.X.toarray() - elif isinstance(adata.X, np.ndarray): - count_embed = adata.X - elif isinstance(adata.X, pd.Dataframe): - count_embed = adata.X.values - else: - raise ValueError( - f"""\ - {type(adata.X)} is not a valid type. - """ - ) - else: - count_embed = adata.obsm[use_data] - - calculate_weight_matrix(adata, platform=platform) - - impute_neighbour(adata, count_embed=count_embed, weights=weights) - - imputed_data = adata.obsm["imputed_data"].astype(float) - mask = count_embed != 0 - count_embed_ = count_embed.astype(float) - count_embed_[count_embed_ == 0] = np.nan - adjusted_count_matrix = np.nanmean(np.array([count_embed_, imputed_data]), axis=0) - adjusted_count_matrix[mask] = count_embed[mask] - - key_added = use_data + "_SME_imputed" - adata.obsm[key_added] = adjusted_count_matrix - - print("The data adjusted by SME is added to adata.obsm['" + key_added + "']") - - return adata if copy else None - - _COPY = Literal["pseudo_spot_adata", "combined_adata"] @@ -98,9 +29,8 @@ def pseudo_spot( copy: _COPY = "pseudo_spot_adata", ) -> AnnData | None: """\ - using spatial location (S), tissue morphological feature (M) and gene - expression (E) information to impute gap between spots and increase resolution - for gene detection + Improve spatial resolution by imputing (creating) new spots from existing ones + using spatial, morphological, and expression (SME) information. Parameters ---------- @@ -306,8 +236,8 @@ def pseudo_spot( else: count_embed = adata.obsm[use_data] - calculate_weight_matrix( - adata, pseudo_spot_adata, pseudo_spots=True, platform=platform + weight_matrix_imputed( + adata, pseudo_spot_adata, platform=platform ) impute_neighbour(pseudo_spot_adata, count_embed=count_embed, weights=weights) diff --git a/stlearn/spatial/SME/sme_impute0.py b/stlearn/spatial/SME/sme_impute0.py new file mode 100644 index 00000000..33442201 --- /dev/null +++ b/stlearn/spatial/SME/sme_impute0.py @@ -0,0 +1,84 @@ +import numpy as np +import pandas as pd +from anndata import AnnData +from scipy.sparse import csr_matrix + +from stlearn.spatial.SME._weighting_matrix import _WEIGHTING_MATRIX, _PLATFORM, \ + weight_matrix, impute_neighbour + + +def SME_impute0( + adata: AnnData, + use_data: str = "raw", + weights: _WEIGHTING_MATRIX = "weights_matrix_all", + platform: _PLATFORM = "Visium", + copy: bool = False, +) -> AnnData | None: + """\ + Fill missing/zero expression values using spatial, morphological, + and expression (SME) information when you what to correct for technical noise + (dropouts) without altering existing biological signals. + + This function replaces only zero/missing values with spatially-informed + predictions while preserving all original non-zero expression measurements. + + Parameters + ---------- + adata : + Annotated data matrix must contain obsm["X_morphology"] and obsm["X_pca"]. + use_data : + input data, can be `raw` counts or log transformed data + weights : _WEIGHTING_MATRIX, default="weights_matrix_all" + Strategy for computing neighbor similarity weights: + - "weights_matrix_all": Combines spatial location (S) + + morphological features (M) + gene expression correlation (E). + - "weights_matrix_pd_gd": Physical distance + gene expression correlation only. + - "weights_matrix_pd_md": Physical distance + morphological features only. + - "weights_matrix_gd_md": Gene expression + morphological features only. + - "gene_expression_correlation": Expression similarity only. + - "physical_distance": Spatial proximity only. + - "morphological_distance": Tissue morphology similarity only. + platform : + `Visium` or `Old_ST` + copy : + If True, return a copy instead of writing to adata. If False, modify adata + in place and return None. + Returns + ------- + AnnData or None + """ + adata = adata.copy() if copy else adata + + if use_data == "raw": + if isinstance(adata.X, csr_matrix): + count_embed = adata.X.toarray() + elif isinstance(adata.X, np.ndarray): + count_embed = adata.X + elif isinstance(adata.X, pd.Dataframe): + count_embed = adata.X.values + else: + raise ValueError( + f"""\ + {type(adata.X)} is not a valid type. + """ + ) + else: + count_embed = adata.obsm[use_data] + + weight_matrix(adata, platform=platform) + + impute_neighbour(adata, count_embed=count_embed, weights=weights) + + imputed_data = adata.obsm["imputed_data"].astype(float) + mask = count_embed != 0 + count_embed_ = count_embed.astype(float) + count_embed_[count_embed_ == 0] = np.nan + adjusted_count_matrix = np.nanmean(np.array([count_embed_, imputed_data]), axis=0) + adjusted_count_matrix[mask] = count_embed[mask] + + key_added = use_data + "_SME_imputed" + adata.obsm[key_added] = adjusted_count_matrix + + print("The data adjusted by SME is added to adata.obsm['" + key_added + "']") + + return adata if copy else None From 5b2c5458787868bc685b69e1bd5909f7fb787c45 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 12:32:11 +1000 Subject: [PATCH 3/7] Fix style. --- stlearn/spatial/SME/__init__.py | 5 +++-- stlearn/spatial/SME/_weighting_matrix.py | 11 +++++------ stlearn/spatial/SME/pseudo_spot.py | 6 ++---- stlearn/spatial/SME/sme_impute0.py | 8 ++++++-- .../spatial/SME/{normalize.py => sme_normalize.py} | 3 ++- stlearn/tl/clustering/leiden.py | 4 +--- 6 files changed, 19 insertions(+), 18 deletions(-) rename stlearn/spatial/SME/{normalize.py => sme_normalize.py} (98%) diff --git a/stlearn/spatial/SME/__init__.py b/stlearn/spatial/SME/__init__.py index 27f1dc29..988a9467 100644 --- a/stlearn/spatial/SME/__init__.py +++ b/stlearn/spatial/SME/__init__.py @@ -1,8 +1,9 @@ -from .impute import pseudo_spot +from .pseudo_spot import pseudo_spot from .sme_impute0 import SME_impute0 -from .normalize import SME_normalize +from .sme_normalize import SME_normalize __all__ = [ "SME_normalize", + "SME_impute0", "pseudo_spot", ] diff --git a/stlearn/spatial/SME/_weighting_matrix.py b/stlearn/spatial/SME/_weighting_matrix.py index 4df84279..306558d8 100644 --- a/stlearn/spatial/SME/_weighting_matrix.py +++ b/stlearn/spatial/SME/_weighting_matrix.py @@ -1,11 +1,11 @@ +import math from typing import Literal import numpy as np from anndata import AnnData +from sklearn.linear_model import LinearRegression from sklearn.metrics import pairwise_distances from tqdm import tqdm -from sklearn.linear_model import LinearRegression -import math _PLATFORM = Literal["Visium", "Old_ST"] _WEIGHTING_MATRIX = Literal[ @@ -46,7 +46,7 @@ def row_col_by_platform(adata, platform): def weight_matrix(adata, platform): reg_col, reg_row, rate = row_col_by_platform(adata, platform) pd = pairwise_distances(adata.obs[["imagecol", "imagerow"]], metric="euclidean") - unit = math.sqrt(reg_row.coef_ ** 2 + reg_col.coef_ ** 2) + unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) pd_norm = np.where(pd >= rate * unit, 0, 1) md = 1 - pairwise_distances(adata.obsm["X_morphology"], metric="cosine") md[md < 0] = 0 @@ -66,8 +66,7 @@ def weight_matrix(adata, platform): adata.uns["physical_distance"] * adata.uns["morphological_distance"] ) adata.uns["weights_matrix_gd_md"] = ( - adata.uns["gene_expression_correlation"] - * adata.uns["morphological_distance"] + adata.uns["gene_expression_correlation"] * adata.uns["morphological_distance"] ) @@ -79,7 +78,7 @@ def weight_matrix_imputed(adata, adata_imputed, platform): adata.obs[["imagecol", "imagerow"]], metric="euclidean", ) - unit = math.sqrt(reg_row.coef_ ** 2 + reg_col.coef_ ** 2) + unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) pd_norm = np.where(pd >= unit, 0, 1) md = 1 - pairwise_distances( adata_imputed.obsm["X_morphology"], diff --git a/stlearn/spatial/SME/pseudo_spot.py b/stlearn/spatial/SME/pseudo_spot.py index 7151d37d..5d154fa7 100644 --- a/stlearn/spatial/SME/pseudo_spot.py +++ b/stlearn/spatial/SME/pseudo_spot.py @@ -13,7 +13,7 @@ _PLATFORM, _WEIGHTING_MATRIX, impute_neighbour, - weight_matrix_imputed + weight_matrix_imputed, ) _COPY = Literal["pseudo_spot_adata", "combined_adata"] @@ -236,9 +236,7 @@ def pseudo_spot( else: count_embed = adata.obsm[use_data] - weight_matrix_imputed( - adata, pseudo_spot_adata, platform=platform - ) + weight_matrix_imputed(adata, pseudo_spot_adata, platform=platform) impute_neighbour(pseudo_spot_adata, count_embed=count_embed, weights=weights) diff --git a/stlearn/spatial/SME/sme_impute0.py b/stlearn/spatial/SME/sme_impute0.py index 33442201..f01ed331 100644 --- a/stlearn/spatial/SME/sme_impute0.py +++ b/stlearn/spatial/SME/sme_impute0.py @@ -3,8 +3,12 @@ from anndata import AnnData from scipy.sparse import csr_matrix -from stlearn.spatial.SME._weighting_matrix import _WEIGHTING_MATRIX, _PLATFORM, \ - weight_matrix, impute_neighbour +from stlearn.spatial.SME._weighting_matrix import ( + _PLATFORM, + _WEIGHTING_MATRIX, + impute_neighbour, + weight_matrix, +) def SME_impute0( diff --git a/stlearn/spatial/SME/normalize.py b/stlearn/spatial/SME/sme_normalize.py similarity index 98% rename from stlearn/spatial/SME/normalize.py rename to stlearn/spatial/SME/sme_normalize.py index c097b390..3fcb06d7 100644 --- a/stlearn/spatial/SME/normalize.py +++ b/stlearn/spatial/SME/sme_normalize.py @@ -6,7 +6,8 @@ from ._weighting_matrix import ( _PLATFORM, _WEIGHTING_MATRIX, - impute_neighbour, weight_matrix, + impute_neighbour, + weight_matrix, ) diff --git a/stlearn/tl/clustering/leiden.py b/stlearn/tl/clustering/leiden.py index 50936f40..f49170bc 100644 --- a/stlearn/tl/clustering/leiden.py +++ b/stlearn/tl/clustering/leiden.py @@ -1,6 +1,4 @@ -from collections.abc import Mapping, Sequence -from types import MappingProxyType -from typing import Any, Literal +from collections.abc import Sequence import scanpy from anndata import AnnData From 5ab4bb85dbfe49a6972dbf6d30878c1b31b6532e Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 12:34:18 +1000 Subject: [PATCH 4/7] Update metadata. --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ddb5e570..d1705b72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "stlearn" -version = "1.1.1" +version = "1.1.2" authors = [ {name = "Genomics and Machine Learning lab", email = "andrew.newman@uq.edu.au"}, ] @@ -14,7 +14,7 @@ license = {text = "BSD license"} requires-python = "~=3.10.0" keywords = ["stlearn"] classifiers = [ - "Development Status :: 2 - Pre-Alpha", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", From 7447f7cee1a977095b3ae0a7a95f37ba579de589 Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 12:35:28 +1000 Subject: [PATCH 5/7] Update history. --- HISTORY.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 191c3c6b..dd95dbeb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,11 @@ History ======= +1.1.2 (2025-09-17) +------------------ +* Add Leiden clustering wrapper. +* Fix documentation, refactor code in spatial.SME. + 1.1.1 (2025-07-07) ------------------ * Support Python 3.10.x From 3c1c5bbc6cbcd84f6648381af187fe84b725a85d Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 13:51:43 +1000 Subject: [PATCH 6/7] Add types, fix warnings. --- requirements.txt | 3 ++- stlearn/spatial/SME/_weighting_matrix.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index eb5fccfe..6c059949 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,5 @@ tensorflow==2.14.1 keras==2.14.0 types-tensorflow>=2.8.0 imageio==2.37.0 -scipy==1.11.4 \ No newline at end of file +scipy==1.11.4 +scikit-learn==1.7.0 \ No newline at end of file diff --git a/stlearn/spatial/SME/_weighting_matrix.py b/stlearn/spatial/SME/_weighting_matrix.py index 306558d8..aa15e9d7 100644 --- a/stlearn/spatial/SME/_weighting_matrix.py +++ b/stlearn/spatial/SME/_weighting_matrix.py @@ -3,7 +3,7 @@ import numpy as np from anndata import AnnData -from sklearn.linear_model import LinearRegression +from sklearn.linear_model import LinearRegression # type: ignore from sklearn.metrics import pairwise_distances from tqdm import tqdm @@ -19,7 +19,10 @@ ] -def row_col_by_platform(adata, platform): +def row_col_by_platform( + adata, platform +) -> tuple[LinearRegression, LinearRegression, float]: + rate: float if platform == "Visium": img_row = adata.obs["imagerow"] img_col = adata.obs["imagecol"] @@ -38,15 +41,16 @@ def row_col_by_platform(adata, platform): {platform!r} does not support. """ ) - reg_row = LinearRegression().fit(array_row.values.reshape(-1, 1), img_row) - reg_col = LinearRegression().fit(array_col.values.reshape(-1, 1), img_col) + regression = LinearRegression() + reg_row: LinearRegression = regression.fit(array_row.values.reshape(-1, 1), img_row) # type: ignore + reg_col: LinearRegression = regression.fit(array_col.values.reshape(-1, 1), img_col) # type: ignore return reg_col, reg_row, rate def weight_matrix(adata, platform): reg_col, reg_row, rate = row_col_by_platform(adata, platform) pd = pairwise_distances(adata.obs[["imagecol", "imagerow"]], metric="euclidean") - unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) + unit = math.sqrt(reg_row.coef_[0] ** 2 + reg_col.coef_[0] ** 2) pd_norm = np.where(pd >= rate * unit, 0, 1) md = 1 - pairwise_distances(adata.obsm["X_morphology"], metric="cosine") md[md < 0] = 0 @@ -78,7 +82,7 @@ def weight_matrix_imputed(adata, adata_imputed, platform): adata.obs[["imagecol", "imagerow"]], metric="euclidean", ) - unit = math.sqrt(reg_row.coef_**2 + reg_col.coef_**2) + unit = math.sqrt(reg_row.coef_[0] ** 2 + reg_col.coef_[0] ** 2) pd_norm = np.where(pd >= unit, 0, 1) md = 1 - pairwise_distances( adata_imputed.obsm["X_morphology"], From 502a69fd173d5b59a86a34d6d9d0de41a0e3929d Mon Sep 17 00:00:00 2001 From: Andrew Newman Date: Wed, 17 Sep 2025 13:52:50 +1000 Subject: [PATCH 7/7] Update version. --- docs/release_notes/1.1.2.rst | 6 ++++++ docs/release_notes/index.rst | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 docs/release_notes/1.1.2.rst diff --git a/docs/release_notes/1.1.2.rst b/docs/release_notes/1.1.2.rst new file mode 100644 index 00000000..f475d1c1 --- /dev/null +++ b/docs/release_notes/1.1.2.rst @@ -0,0 +1,6 @@ +1.1.2 `2025-09-17` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. rubric:: Features +* Add Leiden clustering wrapper. +* Fix documentation, refactor code in spatial.SME. \ No newline at end of file diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst index 25e378da..b57c33b4 100644 --- a/docs/release_notes/index.rst +++ b/docs/release_notes/index.rst @@ -1,6 +1,8 @@ Release Notes =================================================== +.. include:: 1.1.2.rst + .. include:: 1.1.1.rst .. include:: 0.4.6.rst