From 4bd98b60c3b7bb24218da7ff420c9c2a35640ee8 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Fri, 22 Mar 2024 09:36:57 -0400 Subject: [PATCH 01/43] Add docstrings --- docs/references.rst | 4 + scanpy/tests/test_embedding.py | 19 +++ scanpy/tools/__init__.py | 3 +- scanpy/tools/_umap.py | 216 +++++++++++++++++++++++++++++++-- 4 files changed, 232 insertions(+), 10 deletions(-) diff --git a/docs/references.rst b/docs/references.rst index 80fdc892de..b557971071 100644 --- a/docs/references.rst +++ b/docs/references.rst @@ -133,6 +133,10 @@ References *UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction*, `arXiv `__. +.. [Narayan21] Narayan *et al.* (2021), + *Assessing single-cell transcriptomic variability through density-preserving data visualization*, + `Nature Biotechnology `__. + .. [Moignard15] Moignard *et al.* (2015), *Decoding the regulatory network of early blood development from single-cell gene expression measurements*, `Nature Biotechnology `__. diff --git a/scanpy/tests/test_embedding.py b/scanpy/tests/test_embedding.py index 530f013304..f889ef3023 100644 --- a/scanpy/tests/test_embedding.py +++ b/scanpy/tests/test_embedding.py @@ -68,3 +68,22 @@ def test_diffmap(): sc.tl.diffmap(pbmc, random_state=1234) d3 = pbmc.obsm["X_diffmap"].copy() assert_raises(AssertionError, assert_array_equal, d1, d3) + +def test_densmap(): + pbmc = pbmc68k_reduced() + + sc.tl.densmap(pbmc) + d1 = pbmc.obsm["X_densmap"].copy() + sc.tl.densmap(pbmc) + d2 = pbmc.obsm["X_densmap"].copy() + assert_array_equal(d1, d2) + + # Checking if specifying random_state works, arrays shouldn't be equal + sc.tl.densmap(pbmc, random_state=1234) + d3 = pbmc.obsm["X_densmap"].copy() + assert_raises(AssertionError, assert_array_equal, d1, d3) + + # Checking if specifying dens_lambda works, arrays shouldn't be equal + sc.tl.densmap(pbmc, dens_lambda=2.3456) + d4 = pbmc.obsm["X_densmap"].copy() + assert_raises(AssertionError, assert_array_equal, d1, d4) \ No newline at end of file diff --git a/scanpy/tools/__init__.py b/scanpy/tools/__init__.py index 9ca74faf81..ba3ca30b63 100644 --- a/scanpy/tools/__init__.py +++ b/scanpy/tools/__init__.py @@ -24,7 +24,7 @@ from ._score_genes import score_genes, score_genes_cell_cycle from ._sim import sim from ._tsne import tsne -from ._umap import umap +from ._umap import umap, densmap def __getattr__(name: str) -> Any: @@ -53,4 +53,5 @@ def __getattr__(name: str) -> Any: "sim", "tsne", "umap", + "densmap", ] diff --git a/scanpy/tools/_umap.py b/scanpy/tools/_umap.py index 8105c3abd8..2b7daf8584 100644 --- a/scanpy/tools/_umap.py +++ b/scanpy/tools/_umap.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Literal, TypedDict import numpy as np from sklearn.utils import check_array, check_random_state @@ -17,6 +17,12 @@ _InitPos = Literal["paga", "spectral", "random"] +_MethodKwds = TypedDict("_MethodKwds", { + "dens_lambda": float, + "dens_frac": float, + "dens_var_shift": float, +}, total=False) + @old_positionals( "min_dist", @@ -49,8 +55,9 @@ def umap( a: float | None = None, b: float | None = None, copy: bool = False, - method: Literal["umap", "rapids"] = "umap", + method: Literal["umap", "rapids", "densmap"] = "umap", neighbors_key: str | None = None, + method_kwds: _MethodKwds | None = None, ) -> AnnData | None: """\ Embed the neighborhood graph using UMAP [McInnes18]_. @@ -124,6 +131,8 @@ def umap( ``'umap'`` Umap’s simplical set embedding. + ``'densmap'`` + Umap’s simplical set embedding with densmap=True. ``'rapids'`` GPU accelerated implementation. @@ -135,15 +144,42 @@ def umap( (default storage places for pp.neighbors). If specified, umap looks .uns[neighbors_key] for neighbors settings and .obsp[.uns[neighbors_key]['connectivities_key']] for connectivities. + + method_kwds + Additional method parameters. + + If method is ``'densmap'``, the following parameters are available: + + ``dens_lambda`` : `float`, optional (default: 2.0) + Controls the regularization weight of the density correlation term + in densMAP. Higher values prioritize density preservation over the + UMAP objective, and vice versa for values closer to zero. Setting this + parameter to zero is equivalent to running the original UMAP algorithm. + ``dens_frac`` : `float`, optional (default: 0.3) + Controls the fraction of epochs (between 0 and 1) where the + density-augmented objective is used in densMAP. The first + (1 - dens_frac) fraction of epochs optimize the original UMAP objective + before introducing the density correlation term. + ``dens_var_shift`` : `float`, optional (default: 0.1) + A small constant added to the variance of local radii in the + embedding when calculating the density correlation objective to + prevent numerical instability from dividing by a small number Returns ------- - Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields unless method is 'densmap': `adata.obsm['X_umap']` : :class:`numpy.ndarray` (dtype `float`) UMAP coordinates of data. `adata.uns['umap']` : :class:`dict` UMAP parameters. + + When method is 'densmap', sets the following fields: + + `adata.obsm['X_densmap']` : :class:`numpy.ndarray` (dtype `float`) + densMAP coordinates of data. + `adata.uns['densmap']` : :class:`dict` + densMAP parameters. """ adata = adata.copy() if copy else adata @@ -176,7 +212,13 @@ def umap( else: a = a b = b - adata.uns["umap"] = {"params": {"a": a, "b": b}} + + uns_name = "densmap" if method == "densmap" else "umap" + obsm_key = "X_densmap" if method == "densmap" else "X_umap" + obsm_name = "densMAP" if method == "densmap" else "UMAP" + + adata.uns[uns_name] = {"params": { "a": a, "b": b }} + if isinstance(init_pos, str) and init_pos in adata.obsm.keys(): init_coords = adata.obsm[init_pos] elif isinstance(init_pos, str) and init_pos == "paga": @@ -199,11 +241,31 @@ def umap( n_pcs=neigh_params.get("n_pcs", None), silent=True, ) - if method == "umap": + + densmap_kwds = { + "graph_dists": neighbors["distances"], + "n_neighbors": neigh_params.get("n_neighbors", 15), + # Default params from umap package + # Reference: https://github.com/lmcinnes/umap/blob/868e55cb614f361a0d31540c1f4a4b175136025c/umap/umap_.py#L1692 + # If user provided method_kwds, the user-provided values should + # overwrite the default values specified above. + "lambda": method_kwds.get("dens_lambda", 2.0), + "frac": method_kwds.get("dens_frac", 0.3), + "var_shift": method_kwds.get("dens_var_shift", 0.1), + } if method == "densmap" else {} + if method == "densmap": + adata.uns[uns_name]["params"].update({ + "dens_lambda": densmap_kwds["lambda"], + "dens_frac": densmap_kwds["frac"], + "dens_var_shift": densmap_kwds["var_shift"], + }) + + if method == "umap" or method == "densmap": # the data matrix X is really only used for determining the number of connected components # for the init condition in the UMAP embedding default_epochs = 500 if neighbors["connectivities"].shape[0] <= 10000 else 200 n_epochs = default_epochs if maxiter is None else maxiter + X_umap, _ = simplicial_set_embedding( data=X, graph=neighbors["connectivities"].tocoo(), @@ -218,8 +280,8 @@ def umap( random_state=random_state, metric=neigh_params.get("metric", "euclidean"), metric_kwds=neigh_params.get("metric_kwds", {}), - densmap=False, - densmap_kwds={}, + densmap=(method == "densmap"), + densmap_kwds=densmap_kwds, output_dens=False, verbose=settings.verbosity > 3, ) @@ -257,10 +319,146 @@ def umap( random_state=random_state, ) X_umap = umap.fit_transform(X_contiguous) - adata.obsm["X_umap"] = X_umap # annotate samples with UMAP coordinates + + adata.obsm[obsm_key] = X_umap # annotate samples with UMAP coordinates logg.info( " finished", time=start, - deep=("added\n" " 'X_umap', UMAP coordinates (adata.obsm)"), + deep=("added\n" f" '{obsm_key}', {obsm_name} coordinates (adata.obsm)"), ) return adata if copy else None + +# Convenience function for densMAP +def densmap( + adata: AnnData, + *, + min_dist: float = 0.5, + spread: float = 1.0, + n_components: int = 2, + maxiter: int | None = None, + alpha: float = 1.0, + gamma: float = 1.0, + negative_sample_rate: int = 5, + init_pos: _InitPos | np.ndarray | None = "spectral", + random_state: AnyRandom = 0, + a: float | None = None, + b: float | None = None, + copy: bool = False, + neighbors_key: str | None = None, + dens_lambda: float | None = 2.0, + dens_frac: float | None = 0.3, + dens_var_shift: float | None = 0.1, +) -> AnnData | None: + """\ + Embed the neighborhood graph using densMAP [Narayan21]_. + + We use the mplementation of densMAP defined in + `umap-learn `__ + [McInnes18]_. + + Parameters + ---------- + adata + Annotated data matrix. + min_dist + The effective minimum distance between embedded points. Smaller values + will result in a more clustered/clumped embedding where nearby points on + the manifold are drawn closer together, while larger values will result + on a more even dispersal of points. The value should be set relative to + the ``spread`` value, which determines the scale at which embedded + points will be spread out. The default of in the `umap-learn` package is + 0.1. + spread + The effective scale of embedded points. In combination with `min_dist` + this determines how clustered/clumped the embedded points are. + n_components + The number of dimensions of the embedding. + maxiter + The number of iterations (epochs) of the optimization. Called `n_epochs` + in the original UMAP. + alpha + The initial learning rate for the embedding optimization. + gamma + Weighting applied to negative samples in low dimensional embedding + optimization. Values higher than one will result in greater weight + being given to negative samples. + negative_sample_rate + The number of negative edge/1-simplex samples to use per positive + edge/1-simplex sample in optimizing the low dimensional embedding. + init_pos + How to initialize the low dimensional embedding. Called `init` in the + original UMAP. Options are: + + * Any key for `adata.obsm`. + * 'paga': positions from :func:`~scanpy.pl.paga`. + * 'spectral': use a spectral embedding of the graph. + * 'random': assign initial embedding positions at random. + * A numpy array of initial embedding positions. + random_state + If `int`, `random_state` is the seed used by the random number generator; + If `RandomState` or `Generator`, `random_state` is the random number generator; + If `None`, the random number generator is the `RandomState` instance used + by `np.random`. + a + More specific parameters controlling the embedding. If `None` these + values are set automatically as determined by `min_dist` and + `spread`. + b + More specific parameters controlling the embedding. If `None` these + values are set automatically as determined by `min_dist` and + `spread`. + copy + Return a copy instead of writing to adata. + neighbors_key + If not specified, umap looks .uns['neighbors'] for neighbors settings + and .obsp['connectivities'] for connectivities + (default storage places for pp.neighbors). + If specified, umap looks .uns[neighbors_key] for neighbors settings and + .obsp[.uns[neighbors_key]['connectivities_key']] for connectivities. + dens_lambda + Controls the regularization weight of the density correlation term + in densMAP. Higher values prioritize density preservation over the + UMAP objective, and vice versa for values closer to zero. Setting this + parameter to zero is equivalent to running the original UMAP algorithm. + dens_frac + Controls the fraction of epochs (between 0 and 1) where the + density-augmented objective is used in densMAP. The first + (1 - dens_frac) fraction of epochs optimize the original UMAP objective + before introducing the density correlation term. + dens_var_shift + A small constant added to the variance of local radii in the + embedding when calculating the density correlation objective to + prevent numerical instability from dividing by a small number + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + + `adata.obsm['X_densmap']` : :class:`numpy.ndarray` (dtype `float`) + DensMAP coordinates of data. + `adata.uns['densmap']` : :class:`dict` + DensMAP parameters. + + """ + return umap( + adata, + min_dist=min_dist, + spread=spread, + n_components=n_components, + maxiter=maxiter, + alpha=alpha, + gamma=gamma, + negative_sample_rate=negative_sample_rate, + init_pos=init_pos, + random_state=random_state, + a=a, + b=b, + copy=copy, + method="densmap", + method_kwds={ + "dens_lambda": dens_lambda, + "dens_frac": dens_frac, + "dens_var_shift": dens_var_shift, + }, + neighbors_key=neighbors_key, + ) \ No newline at end of file From b55fa957c49b15b708b197764f6ba240b8e46cb6 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Fri, 22 Mar 2024 09:39:41 -0400 Subject: [PATCH 02/43] More tests --- scanpy/tests/test_embedding.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scanpy/tests/test_embedding.py b/scanpy/tests/test_embedding.py index f889ef3023..3796355885 100644 --- a/scanpy/tests/test_embedding.py +++ b/scanpy/tests/test_embedding.py @@ -78,12 +78,17 @@ def test_densmap(): d2 = pbmc.obsm["X_densmap"].copy() assert_array_equal(d1, d2) + # Should have same result via sc.tl.umap with "densmap" as method + sc.tl.umap(pbmc, method="densmap") + d3 = pbmc.obsm["X_densmap"].copy() + assert_array_equal(d1, d3) + # Checking if specifying random_state works, arrays shouldn't be equal sc.tl.densmap(pbmc, random_state=1234) - d3 = pbmc.obsm["X_densmap"].copy() - assert_raises(AssertionError, assert_array_equal, d1, d3) + d4 = pbmc.obsm["X_densmap"].copy() + assert_raises(AssertionError, assert_array_equal, d1, d4) # Checking if specifying dens_lambda works, arrays shouldn't be equal sc.tl.densmap(pbmc, dens_lambda=2.3456) - d4 = pbmc.obsm["X_densmap"].copy() - assert_raises(AssertionError, assert_array_equal, d1, d4) \ No newline at end of file + d5 = pbmc.obsm["X_densmap"].copy() + assert_raises(AssertionError, assert_array_equal, d1, d5) \ No newline at end of file From e1a0797a422c79b1282c7c16b478012fa1e94d41 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Fri, 22 Mar 2024 09:42:46 -0400 Subject: [PATCH 03/43] Fixes for tests --- scanpy/tools/_umap.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scanpy/tools/_umap.py b/scanpy/tools/_umap.py index 2b7daf8584..15d3ff7413 100644 --- a/scanpy/tools/_umap.py +++ b/scanpy/tools/_umap.py @@ -231,7 +231,7 @@ def umap( init_coords = check_array(init_coords, dtype=np.float32, accept_sparse=False) if random_state != 0: - adata.uns["umap"]["params"]["random_state"] = random_state + adata.uns[uns_name]["params"]["random_state"] = random_state random_state = check_random_state(random_state) neigh_params = neighbors["params"] @@ -242,6 +242,9 @@ def umap( silent=True, ) + if method_kwds is None: + method_kwds = {} + densmap_kwds = { "graph_dists": neighbors["distances"], "n_neighbors": neigh_params.get("n_neighbors", 15), From f140bdadd2cf5a924ea621ac5feb45d2eadfd8e6 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Fri, 22 Mar 2024 09:48:56 -0400 Subject: [PATCH 04/43] Typo --- scanpy/tools/_umap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanpy/tools/_umap.py b/scanpy/tools/_umap.py index 15d3ff7413..123f1d3891 100644 --- a/scanpy/tools/_umap.py +++ b/scanpy/tools/_umap.py @@ -355,7 +355,7 @@ def densmap( """\ Embed the neighborhood graph using densMAP [Narayan21]_. - We use the mplementation of densMAP defined in + We use the implementation of densMAP defined in `umap-learn `__ [McInnes18]_. From 745e79ef47a8cc41082b066736f2519fdedf6a5e Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Fri, 22 Mar 2024 09:53:03 -0400 Subject: [PATCH 05/43] Linting --- scanpy/tools/_umap.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/scanpy/tools/_umap.py b/scanpy/tools/_umap.py index 123f1d3891..f41246a642 100644 --- a/scanpy/tools/_umap.py +++ b/scanpy/tools/_umap.py @@ -144,12 +144,12 @@ def umap( (default storage places for pp.neighbors). If specified, umap looks .uns[neighbors_key] for neighbors settings and .obsp[.uns[neighbors_key]['connectivities_key']] for connectivities. - + method_kwds Additional method parameters. If method is ``'densmap'``, the following parameters are available: - + ``dens_lambda`` : `float`, optional (default: 2.0) Controls the regularization weight of the density correlation term in densMAP. Higher values prioritize density preservation over the @@ -173,7 +173,7 @@ def umap( UMAP coordinates of data. `adata.uns['umap']` : :class:`dict` UMAP parameters. - + When method is 'densmap', sets the following fields: `adata.obsm['X_densmap']` : :class:`numpy.ndarray` (dtype `float`) @@ -212,12 +212,12 @@ def umap( else: a = a b = b - + uns_name = "densmap" if method == "densmap" else "umap" obsm_key = "X_densmap" if method == "densmap" else "X_umap" obsm_name = "densMAP" if method == "densmap" else "UMAP" - adata.uns[uns_name] = {"params": { "a": a, "b": b }} + adata.uns[uns_name] = {"params": {"a": a, "b": b}} if isinstance(init_pos, str) and init_pos in adata.obsm.keys(): init_coords = adata.obsm[init_pos] @@ -332,6 +332,8 @@ def umap( return adata if copy else None # Convenience function for densMAP + + def densmap( adata: AnnData, *, @@ -464,4 +466,4 @@ def densmap( "dens_var_shift": dens_var_shift, }, neighbors_key=neighbors_key, - ) \ No newline at end of file + ) From c63e745947899c7f2ae1764f512f60b2742daedc Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Fri, 22 Mar 2024 09:56:39 -0400 Subject: [PATCH 06/43] Ruff --- scanpy/tools/_umap.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scanpy/tools/_umap.py b/scanpy/tools/_umap.py index f41246a642..c43ee6f9e6 100644 --- a/scanpy/tools/_umap.py +++ b/scanpy/tools/_umap.py @@ -17,11 +17,10 @@ _InitPos = Literal["paga", "spectral", "random"] -_MethodKwds = TypedDict("_MethodKwds", { - "dens_lambda": float, - "dens_frac": float, - "dens_var_shift": float, -}, total=False) +class _MethodKwds(TypedDict, total=False): + dens_lambda: float + dens_frac: float + dens_var_shift: float @old_positionals( From 66cf7fd8198d2b00fc778d76d5bda65d6b695401 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Fri, 22 Mar 2024 09:59:24 -0400 Subject: [PATCH 07/43] More linting --- scanpy/tests/test_embedding.py | 3 ++- scanpy/tools/_umap.py | 40 ++++++++++++++++++++-------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/scanpy/tests/test_embedding.py b/scanpy/tests/test_embedding.py index 3796355885..f1bd6ed085 100644 --- a/scanpy/tests/test_embedding.py +++ b/scanpy/tests/test_embedding.py @@ -69,6 +69,7 @@ def test_diffmap(): d3 = pbmc.obsm["X_diffmap"].copy() assert_raises(AssertionError, assert_array_equal, d1, d3) + def test_densmap(): pbmc = pbmc68k_reduced() @@ -91,4 +92,4 @@ def test_densmap(): # Checking if specifying dens_lambda works, arrays shouldn't be equal sc.tl.densmap(pbmc, dens_lambda=2.3456) d5 = pbmc.obsm["X_densmap"].copy() - assert_raises(AssertionError, assert_array_equal, d1, d5) \ No newline at end of file + assert_raises(AssertionError, assert_array_equal, d1, d5) diff --git a/scanpy/tools/_umap.py b/scanpy/tools/_umap.py index c43ee6f9e6..85f587263e 100644 --- a/scanpy/tools/_umap.py +++ b/scanpy/tools/_umap.py @@ -17,6 +17,7 @@ _InitPos = Literal["paga", "spectral", "random"] + class _MethodKwds(TypedDict, total=False): dens_lambda: float dens_frac: float @@ -244,23 +245,29 @@ def umap( if method_kwds is None: method_kwds = {} - densmap_kwds = { - "graph_dists": neighbors["distances"], - "n_neighbors": neigh_params.get("n_neighbors", 15), - # Default params from umap package - # Reference: https://github.com/lmcinnes/umap/blob/868e55cb614f361a0d31540c1f4a4b175136025c/umap/umap_.py#L1692 - # If user provided method_kwds, the user-provided values should - # overwrite the default values specified above. - "lambda": method_kwds.get("dens_lambda", 2.0), - "frac": method_kwds.get("dens_frac", 0.3), - "var_shift": method_kwds.get("dens_var_shift", 0.1), - } if method == "densmap" else {} + densmap_kwds = ( + { + "graph_dists": neighbors["distances"], + "n_neighbors": neigh_params.get("n_neighbors", 15), + # Default params from umap package + # Reference: https://github.com/lmcinnes/umap/blob/868e55cb614f361a0d31540c1f4a4b175136025c/umap/umap_.py#L1692 + # If user provided method_kwds, the user-provided values should + # overwrite the default values specified above. + "lambda": method_kwds.get("dens_lambda", 2.0), + "frac": method_kwds.get("dens_frac", 0.3), + "var_shift": method_kwds.get("dens_var_shift", 0.1), + } + if method == "densmap" + else {} + ) if method == "densmap": - adata.uns[uns_name]["params"].update({ - "dens_lambda": densmap_kwds["lambda"], - "dens_frac": densmap_kwds["frac"], - "dens_var_shift": densmap_kwds["var_shift"], - }) + adata.uns[uns_name]["params"].update( + { + "dens_lambda": densmap_kwds["lambda"], + "dens_frac": densmap_kwds["frac"], + "dens_var_shift": densmap_kwds["var_shift"], + } + ) if method == "umap" or method == "densmap": # the data matrix X is really only used for determining the number of connected components @@ -330,6 +337,7 @@ def umap( ) return adata if copy else None + # Convenience function for densMAP From d705887eaced84849b3d08aa178cc58b63b53877 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:00:49 -0400 Subject: [PATCH 08/43] More linting --- scanpy/tools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanpy/tools/__init__.py b/scanpy/tools/__init__.py index ba3ca30b63..0ece88f80a 100644 --- a/scanpy/tools/__init__.py +++ b/scanpy/tools/__init__.py @@ -24,7 +24,7 @@ from ._score_genes import score_genes, score_genes_cell_cycle from ._sim import sim from ._tsne import tsne -from ._umap import umap, densmap +from ._umap import densmap, umap def __getattr__(name: str) -> Any: From ec2b6d9edadcf1c37d95d36541604af3affa09c6 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Thu, 16 May 2024 08:43:30 -0400 Subject: [PATCH 09/43] Update references.bib --- docs/references.bib | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/references.bib b/docs/references.bib index 561537cd92..e563766050 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -518,6 +518,19 @@ @article{Muraro2016 pages = {385--394.e3}, } +@article{Narayan2021, + title = {Assessing single-cell transcriptomic variability through density-preserving data visualization}, + author = {Narayan, Ashwin and Berger, Bonnie and Cho, Hyunghoon}, + journal = {Nature biotechnology}, + volume = {39}, + number = {6}, + pages = {765--774}, + year = {2021}, + publisher = {Nature Publishing Group US New York}, + url = {https://doi.org/10.1038/s41587-020-00801-7}, + doi = {10.1038/s41587-020-00801-7}, +} + @article{Nowotschin2019, author = {Nowotschin, Sonja and Setty, Manu and Kuo, Ying-Yi and Liu, Vincent and Garg, Vidur and Sharma, Roshan and Simon, Claire S. and Saiz, Nestor and Gardner, Rui and Boutet, Stéphane C. and Church, Deanna M. and Hoodless, Pamela A. and Hadjantonakis, Anna-Katerina and Pe’er, Dana}, title = {The emergent landscape of the mouse gut endoderm at single-cell resolution}, From b41ddbdaa7c8900841e7de497eb72e9aa861cb1b Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Thu, 16 May 2024 08:48:28 -0400 Subject: [PATCH 10/43] Update references.bib --- docs/references.bib | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/references.bib b/docs/references.bib index e563766050..c1fbf1ece5 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -519,16 +519,17 @@ @article{Muraro2016 } @article{Narayan2021, + author = {Narayan, Ashwin and Berger, Bonnie and Cho, Hyunghoon}, title = {Assessing single-cell transcriptomic variability through density-preserving data visualization}, - author = {Narayan, Ashwin and Berger, Bonnie and Cho, Hyunghoon}, - journal = {Nature biotechnology}, volume = {39}, - number = {6}, - pages = {765--774}, - year = {2021}, - publisher = {Nature Publishing Group US New York}, url = {https://doi.org/10.1038/s41587-020-00801-7}, doi = {10.1038/s41587-020-00801-7}, + number = {6}, + journal = {Nature Biotechnology}, + publisher = {Springer Science and Business Media LLC}, + year = {2021}, + month = {jan}, + pages = {765--774}, } @article{Nowotschin2019, From 6ee89e7e79e91b9c6835758e49046f2b2b0262b2 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:56:01 -0400 Subject: [PATCH 11/43] Fix bug --- src/scanpy/tools/_umap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index 7c36788dc6..d4a0ba11f2 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -273,7 +273,7 @@ def umap( else {} ) if method == "densmap": - adata.uns[uns_name]["params"].update( + adata.uns[key_uns]["params"].update( { "dens_lambda": densmap_kwds["lambda"], "dens_frac": densmap_kwds["frac"], From 603afb3ea48f4bf847c1fd0bda3c8d6d7fbafb7c Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Thu, 8 Aug 2024 10:57:54 -0400 Subject: [PATCH 12/43] Update --- src/scanpy/tools/_umap.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index d4a0ba11f2..cf1f657bfd 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -226,9 +226,6 @@ def umap( if a is None or b is None: a, b = find_ab_params(spread, min_dist) - else: - a = a - b = b adata.uns[key_uns] = dict(params=dict(a=a, b=b)) if isinstance(init_pos, str) and init_pos in adata.obsm.keys(): From 4d15620517da3605acfebf9d1058eb16beec80df Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:14:05 -0500 Subject: [PATCH 13/43] Relese note --- docs/release-notes/2946.feature.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/release-notes/2946.feature.md diff --git a/docs/release-notes/2946.feature.md b/docs/release-notes/2946.feature.md new file mode 100644 index 0000000000..e2f8e478d5 --- /dev/null +++ b/docs/release-notes/2946.feature.md @@ -0,0 +1 @@ +Add DensMAP support with {func}`scanpy.tl.densmap` {smaller}`M Keller` \ No newline at end of file From e8350d2b5d4d11590e0212d132e2612c5ec15089 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:16:56 -0500 Subject: [PATCH 14/43] Formatting --- src/scanpy/tools/_umap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index fa064eb21b..2cd1885122 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -197,7 +197,8 @@ def umap( key_obsm, key_uns = ( (("X_densmap", "densmap") if method == "densmap" else ("X_umap", "umap")) - if key_added is None else [key_added] * 2 + if key_added is None + else [key_added] * 2 ) method_name = "DensMAP" if method == "densmap" else "UMAP" From 9030cb4c2280047fba42d65fe80eae943f4a10ad Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:27:42 -0500 Subject: [PATCH 15/43] Try pre-commit --- docs/references.bib | 2 +- docs/release-notes/2946.feature.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/references.bib b/docs/references.bib index b19318232f..69e8a73636 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -534,7 +534,7 @@ @article{Muraro2016 } @article{Narayan2021, - author = {Narayan, Ashwin and Berger, Bonnie and Cho, Hyunghoon}, + author = {Narayan, Ashwin and Berger, Bonnie and Cho, Hyunghoon}, title = {Assessing single-cell transcriptomic variability through density-preserving data visualization}, volume = {39}, url = {https://doi.org/10.1038/s41587-020-00801-7}, diff --git a/docs/release-notes/2946.feature.md b/docs/release-notes/2946.feature.md index e2f8e478d5..9de837d4fa 100644 --- a/docs/release-notes/2946.feature.md +++ b/docs/release-notes/2946.feature.md @@ -1 +1 @@ -Add DensMAP support with {func}`scanpy.tl.densmap` {smaller}`M Keller` \ No newline at end of file +Add DensMAP support with {func}`scanpy.tl.densmap` {smaller}`M Keller` From 967da9237ef68f346c904f6a323bfb0e1ffb5872 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:29:14 -0500 Subject: [PATCH 16/43] Fix random_State type --- src/scanpy/tools/_umap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index 2cd1885122..5057537767 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -366,7 +366,7 @@ def densmap( gamma: float = 1.0, negative_sample_rate: int = 5, init_pos: _InitPos | np.ndarray | None = "spectral", - random_state: AnyRandom = 0, + random_state: _LegacyRandom = 0, a: float | None = None, b: float | None = None, copy: bool = False, From b70ec7efd4ece6a773957444458f9f1af2672b23 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:14:09 -0500 Subject: [PATCH 17/43] Refactor type --- docs/api/tools.md | 1 + docs/conf.py | 1 + docs/release-notes/2946.feature.md | 2 +- src/scanpy/tools/__init__.py | 2 ++ src/scanpy/tools/_types.py | 9 +++++++++ src/scanpy/tools/_umap.py | 11 +++-------- 6 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 src/scanpy/tools/_types.py diff --git a/docs/api/tools.md b/docs/api/tools.md index 1d51559e5a..53b39eb567 100644 --- a/docs/api/tools.md +++ b/docs/api/tools.md @@ -20,6 +20,7 @@ Any transformation of the data matrix that is not *preprocessing*. In contrast t pp.pca tl.tsne tl.umap + tl.densmap tl.draw_graph tl.diffmap ``` diff --git a/docs/conf.py b/docs/conf.py index 2c79aa8d82..a8bf3f6b28 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -225,6 +225,7 @@ def setup(app: Sphinx): ("py:class", "scanpy._utils.Empty"), ("py:class", "numpy.random.mtrand.RandomState"), ("py:class", "scanpy.neighbors._types.KnnTransformerLike"), + ("py:class", "scanpy.tools._types.UmapMethodKwds"), # Will work once scipy 1.8 is released ("py:class", "scipy.sparse.base.spmatrix"), ("py:class", "scipy.sparse.csr.csr_matrix"), diff --git a/docs/release-notes/2946.feature.md b/docs/release-notes/2946.feature.md index 9de837d4fa..b356574215 100644 --- a/docs/release-notes/2946.feature.md +++ b/docs/release-notes/2946.feature.md @@ -1 +1 @@ -Add DensMAP support with {func}`scanpy.tl.densmap` {smaller}`M Keller` +Add DensMAP support with {func}`~scanpy.tl.densmap` {smaller}`M Keller` diff --git a/src/scanpy/tools/__init__.py b/src/scanpy/tools/__init__.py index c3e8ff8050..ca79727038 100644 --- a/src/scanpy/tools/__init__.py +++ b/src/scanpy/tools/__init__.py @@ -29,6 +29,8 @@ if TYPE_CHECKING: from typing import Any + from ._types import UmapMethodKwds # noqa: F401 + def __getattr__(name: str) -> Any: if name == "pca": diff --git a/src/scanpy/tools/_types.py b/src/scanpy/tools/_types.py new file mode 100644 index 0000000000..35ba4fd459 --- /dev/null +++ b/src/scanpy/tools/_types.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from typing import TypedDict + + +class UmapMethodKwds(TypedDict, total=False): + dens_lambda: float + dens_frac: float + dens_var_shift: float diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index 5057537767..353f16e1cf 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING, Literal, TypedDict +from typing import TYPE_CHECKING, Literal import numpy as np from sklearn.utils import check_array, check_random_state @@ -18,16 +18,11 @@ from anndata import AnnData from .._compat import _LegacyRandom + from ._types import UmapMethodKwds _InitPos = Literal["paga", "spectral", "random"] -class _MethodKwds(TypedDict, total=False): - dens_lambda: float - dens_frac: float - dens_var_shift: float - - @old_positionals( "min_dist", "spread", @@ -59,7 +54,7 @@ def umap( a: float | None = None, b: float | None = None, method: Literal["umap", "rapids", "densmap"] = "umap", - method_kwds: _MethodKwds | None = None, + method_kwds: UmapMethodKwds | None = None, key_added: str | None = None, neighbors_key: str = "neighbors", copy: bool = False, From 1784fa446dce10d06b6365cba1986a08076b1d22 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:26:59 -0500 Subject: [PATCH 18/43] Fix citation format --- src/scanpy/tools/_umap.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index 353f16e1cf..2acc654d23 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -371,11 +371,12 @@ def densmap( dens_var_shift: float | None = 0.1, ) -> AnnData | None: """\ - Embed the neighborhood graph using densMAP [Narayan21]_. + Embed the neighborhood graph using densMAP :cite:p:`Narayan21`. We use the implementation of densMAP defined in - `umap-learn `__ - [McInnes18]_. + umap-learn_ :cite:p:`McInnes2018`. + + .. _umap-learn: https://github.com/lmcinnes/umap Parameters ---------- From e92785ef12bd89acdd6d4b58a360d0b18a445f21 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:38:50 -0500 Subject: [PATCH 19/43] Fix bibtex key --- src/scanpy/tools/_umap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index 2acc654d23..7dfa85dfbe 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -371,7 +371,7 @@ def densmap( dens_var_shift: float | None = 0.1, ) -> AnnData | None: """\ - Embed the neighborhood graph using densMAP :cite:p:`Narayan21`. + Embed the neighborhood graph using densMAP :cite:p:`Narayan2021`. We use the implementation of densMAP defined in umap-learn_ :cite:p:`McInnes2018`. From 2ce47d62b237a29978d2944a872ff4d0243addd2 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Tue, 17 Dec 2024 10:03:41 -0500 Subject: [PATCH 20/43] Use partial to implement sc.tl.densmap --- src/scanpy/tools/_umap.py | 140 +------------------------------------- 1 file changed, 3 insertions(+), 137 deletions(-) diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index 7dfa85dfbe..7f1e083e28 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -1,6 +1,7 @@ from __future__ import annotations import warnings +from functools import partial from typing import TYPE_CHECKING, Literal import numpy as np @@ -131,7 +132,7 @@ def umap( ``'umap'`` Umap’s simplical set embedding. ``'densmap'`` - Umap’s simplical set embedding with densmap=True. + Umap’s simplical set embedding with densmap=True :cite:p:`Narayan2021`. ``'rapids'`` GPU accelerated implementation. @@ -348,139 +349,4 @@ def umap( # Convenience function for densMAP - - -def densmap( - adata: AnnData, - *, - min_dist: float = 0.5, - spread: float = 1.0, - n_components: int = 2, - maxiter: int | None = None, - alpha: float = 1.0, - gamma: float = 1.0, - negative_sample_rate: int = 5, - init_pos: _InitPos | np.ndarray | None = "spectral", - random_state: _LegacyRandom = 0, - a: float | None = None, - b: float | None = None, - copy: bool = False, - neighbors_key: str | None = "neighbors", - dens_lambda: float | None = 2.0, - dens_frac: float | None = 0.3, - dens_var_shift: float | None = 0.1, -) -> AnnData | None: - """\ - Embed the neighborhood graph using densMAP :cite:p:`Narayan2021`. - - We use the implementation of densMAP defined in - umap-learn_ :cite:p:`McInnes2018`. - - .. _umap-learn: https://github.com/lmcinnes/umap - - Parameters - ---------- - adata - Annotated data matrix. - min_dist - The effective minimum distance between embedded points. Smaller values - will result in a more clustered/clumped embedding where nearby points on - the manifold are drawn closer together, while larger values will result - on a more even dispersal of points. The value should be set relative to - the ``spread`` value, which determines the scale at which embedded - points will be spread out. The default of in the `umap-learn` package is - 0.1. - spread - The effective scale of embedded points. In combination with `min_dist` - this determines how clustered/clumped the embedded points are. - n_components - The number of dimensions of the embedding. - maxiter - The number of iterations (epochs) of the optimization. Called `n_epochs` - in the original UMAP. - alpha - The initial learning rate for the embedding optimization. - gamma - Weighting applied to negative samples in low dimensional embedding - optimization. Values higher than one will result in greater weight - being given to negative samples. - negative_sample_rate - The number of negative edge/1-simplex samples to use per positive - edge/1-simplex sample in optimizing the low dimensional embedding. - init_pos - How to initialize the low dimensional embedding. Called `init` in the - original UMAP. Options are: - - * Any key for `adata.obsm`. - * 'paga': positions from :func:`~scanpy.pl.paga`. - * 'spectral': use a spectral embedding of the graph. - * 'random': assign initial embedding positions at random. - * A numpy array of initial embedding positions. - random_state - If `int`, `random_state` is the seed used by the random number generator; - If `RandomState` or `Generator`, `random_state` is the random number generator; - If `None`, the random number generator is the `RandomState` instance used - by `np.random`. - a - More specific parameters controlling the embedding. If `None` these - values are set automatically as determined by `min_dist` and - `spread`. - b - More specific parameters controlling the embedding. If `None` these - values are set automatically as determined by `min_dist` and - `spread`. - copy - Return a copy instead of writing to adata. - neighbors_key - If not specified, umap looks .uns['neighbors'] for neighbors settings - and .obsp['connectivities'] for connectivities - (default storage places for pp.neighbors). - If specified, umap looks .uns[neighbors_key] for neighbors settings and - .obsp[.uns[neighbors_key]['connectivities_key']] for connectivities. - dens_lambda - Controls the regularization weight of the density correlation term - in densMAP. Higher values prioritize density preservation over the - UMAP objective, and vice versa for values closer to zero. Setting this - parameter to zero is equivalent to running the original UMAP algorithm. - dens_frac - Controls the fraction of epochs (between 0 and 1) where the - density-augmented objective is used in densMAP. The first - (1 - dens_frac) fraction of epochs optimize the original UMAP objective - before introducing the density correlation term. - dens_var_shift - A small constant added to the variance of local radii in the - embedding when calculating the density correlation objective to - prevent numerical instability from dividing by a small number - - Returns - ------- - Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: - - `adata.obsm['X_densmap']` : :class:`numpy.ndarray` (dtype `float`) - DensMAP coordinates of data. - `adata.uns['densmap']` : :class:`dict` - DensMAP parameters. - - """ - return umap( - adata, - min_dist=min_dist, - spread=spread, - n_components=n_components, - maxiter=maxiter, - alpha=alpha, - gamma=gamma, - negative_sample_rate=negative_sample_rate, - init_pos=init_pos, - random_state=random_state, - a=a, - b=b, - copy=copy, - method="densmap", - method_kwds={ - "dens_lambda": dens_lambda, - "dens_frac": dens_frac, - "dens_var_shift": dens_var_shift, - }, - neighbors_key=neighbors_key, - ) +densmap = partial(umap, method="densmap") From 5838162ee2c16d24541ec2f2a592a3846d86d0dc Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Tue, 17 Dec 2024 10:06:12 -0500 Subject: [PATCH 21/43] Update params in test --- tests/test_embedding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_embedding.py b/tests/test_embedding.py index 75a44460db..b21fdb13b7 100644 --- a/tests/test_embedding.py +++ b/tests/test_embedding.py @@ -110,6 +110,6 @@ def test_densmap(): assert_raises(AssertionError, assert_array_equal, d1, d4) # Checking if specifying dens_lambda works, arrays shouldn't be equal - sc.tl.densmap(pbmc, dens_lambda=2.3456) + sc.tl.densmap(pbmc, method_kwds=dict(dens_lambda=2.3456)) d5 = pbmc.obsm["X_densmap"].copy() assert_raises(AssertionError, assert_array_equal, d1, d5) From ba2e887a3968532330d83170d9ad5f75a7431f49 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:53:20 -0500 Subject: [PATCH 22/43] Revert convenience sc.tl.densmap --- docs/api/tools.md | 1 - docs/release-notes/2946.feature.md | 2 +- src/scanpy/tools/__init__.py | 3 +-- src/scanpy/tools/_umap.py | 5 ----- tests/test_embedding.py | 13 ++++--------- 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/docs/api/tools.md b/docs/api/tools.md index 53b39eb567..1d51559e5a 100644 --- a/docs/api/tools.md +++ b/docs/api/tools.md @@ -20,7 +20,6 @@ Any transformation of the data matrix that is not *preprocessing*. In contrast t pp.pca tl.tsne tl.umap - tl.densmap tl.draw_graph tl.diffmap ``` diff --git a/docs/release-notes/2946.feature.md b/docs/release-notes/2946.feature.md index b356574215..7a48bae2a8 100644 --- a/docs/release-notes/2946.feature.md +++ b/docs/release-notes/2946.feature.md @@ -1 +1 @@ -Add DensMAP support with {func}`~scanpy.tl.densmap` {smaller}`M Keller` +Add DensMAP support via `method="densmap"` in {func}`~scanpy.tl.umap` {smaller}`M Keller` diff --git a/src/scanpy/tools/__init__.py b/src/scanpy/tools/__init__.py index ca79727038..714ab234e4 100644 --- a/src/scanpy/tools/__init__.py +++ b/src/scanpy/tools/__init__.py @@ -24,7 +24,7 @@ from ._score_genes import score_genes, score_genes_cell_cycle from ._sim import sim from ._tsne import tsne -from ._umap import densmap, umap +from ._umap import umap if TYPE_CHECKING: from typing import Any @@ -58,5 +58,4 @@ def __getattr__(name: str) -> Any: "sim", "tsne", "umap", - "densmap", ] diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index 7f1e083e28..117278a79d 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -1,7 +1,6 @@ from __future__ import annotations import warnings -from functools import partial from typing import TYPE_CHECKING, Literal import numpy as np @@ -346,7 +345,3 @@ def umap( ), ) return adata if copy else None - - -# Convenience function for densMAP -densmap = partial(umap, method="densmap") diff --git a/tests/test_embedding.py b/tests/test_embedding.py index b21fdb13b7..f595cafccf 100644 --- a/tests/test_embedding.py +++ b/tests/test_embedding.py @@ -93,23 +93,18 @@ def test_diffmap(): def test_densmap(): pbmc = pbmc68k_reduced() - sc.tl.densmap(pbmc) + sc.tl.umap(pbmc, method="densmap") d1 = pbmc.obsm["X_densmap"].copy() - sc.tl.densmap(pbmc) + sc.tl.umap(pbmc, method="densmap") d2 = pbmc.obsm["X_densmap"].copy() assert_array_equal(d1, d2) - # Should have same result via sc.tl.umap with "densmap" as method - sc.tl.umap(pbmc, method="densmap") - d3 = pbmc.obsm["X_densmap"].copy() - assert_array_equal(d1, d3) - # Checking if specifying random_state works, arrays shouldn't be equal - sc.tl.densmap(pbmc, random_state=1234) + sc.tl.umap(pbmc, method="densmap", random_state=1234) d4 = pbmc.obsm["X_densmap"].copy() assert_raises(AssertionError, assert_array_equal, d1, d4) # Checking if specifying dens_lambda works, arrays shouldn't be equal - sc.tl.densmap(pbmc, method_kwds=dict(dens_lambda=2.3456)) + sc.tl.umap(pbmc, method="densmap", method_kwds=dict(dens_lambda=2.3456)) d5 = pbmc.obsm["X_densmap"].copy() assert_raises(AssertionError, assert_array_equal, d1, d5) From dffadfe3e4f27d24af1658225be522c6fc5921bc Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:05:43 -0500 Subject: [PATCH 23/43] Add image plotting test --- tests/_images/densmap_nocolor/expected.png | Bin 0 -> 12657 bytes tests/test_plotting.py | 8 ++++++++ 2 files changed, 8 insertions(+) create mode 100644 tests/_images/densmap_nocolor/expected.png diff --git a/tests/_images/densmap_nocolor/expected.png b/tests/_images/densmap_nocolor/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..b8c8e6c5c3cdb881af6e51d1fbd631225ca50ab6 GIT binary patch literal 12657 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 literal 0 HcmV?d00001 diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 2f0f5f60cd..908f95bdde 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -1021,6 +1021,7 @@ def pbmc_scatterplots_session() -> AnnData: sc.pp.neighbors(pbmc) sc.tl.tsne(pbmc, random_state=0, n_pcs=30) sc.tl.diffmap(pbmc) + sc.tl.umap(pbmc, method="densmap") return pbmc @@ -1171,6 +1172,13 @@ def pbmc_scatterplots(pbmc_scatterplots_session) -> AnnData: mask_obs="mask", ), ), + ( + "densmap_nocolor", + partial( + sc.pl.embedding, + basis="X_densmap", + ), + ), ], ) def test_scatterplots(image_comparer, pbmc_scatterplots, id, fn): From 44cfafcd25167f3c82e0317c44184427ed6e0e06 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:08:34 -0500 Subject: [PATCH 24/43] Fix variable naming in test --- tests/test_embedding.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_embedding.py b/tests/test_embedding.py index f595cafccf..65652b7eab 100644 --- a/tests/test_embedding.py +++ b/tests/test_embedding.py @@ -93,6 +93,7 @@ def test_diffmap(): def test_densmap(): pbmc = pbmc68k_reduced() + # Checking that the results are reproducible sc.tl.umap(pbmc, method="densmap") d1 = pbmc.obsm["X_densmap"].copy() sc.tl.umap(pbmc, method="densmap") @@ -101,10 +102,10 @@ def test_densmap(): # Checking if specifying random_state works, arrays shouldn't be equal sc.tl.umap(pbmc, method="densmap", random_state=1234) - d4 = pbmc.obsm["X_densmap"].copy() - assert_raises(AssertionError, assert_array_equal, d1, d4) + d3 = pbmc.obsm["X_densmap"].copy() + assert_raises(AssertionError, assert_array_equal, d1, d3) # Checking if specifying dens_lambda works, arrays shouldn't be equal sc.tl.umap(pbmc, method="densmap", method_kwds=dict(dens_lambda=2.3456)) - d5 = pbmc.obsm["X_densmap"].copy() - assert_raises(AssertionError, assert_array_equal, d1, d5) + d4 = pbmc.obsm["X_densmap"].copy() + assert_raises(AssertionError, assert_array_equal, d1, d4) From e4b81f5c29e278feab46537796269d2900b0df43 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Sat, 4 Jan 2025 11:21:10 -0500 Subject: [PATCH 25/43] Frameon --- tests/_images/densmap_nocolor/expected.png | Bin 12657 -> 10993 bytes tests/test_plotting.py | 1 + 2 files changed, 1 insertion(+) diff --git a/tests/_images/densmap_nocolor/expected.png b/tests/_images/densmap_nocolor/expected.png index b8c8e6c5c3cdb881af6e51d1fbd631225ca50ab6..46b145cdfb775a7d3907a35a3c6d28a28b1015bf 100644 GIT binary patch literal 10993 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 literal 12657 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 diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 6f84657a76..66935fc45a 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -1177,6 +1177,7 @@ def pbmc_scatterplots(pbmc_scatterplots_session) -> AnnData: partial( sc.pl.embedding, basis="X_densmap", + frameon=False, ), ), ], From 0ec47b3555cba678c266c325f0238ff0fe77125d Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Sun, 5 Jan 2025 11:24:11 -0500 Subject: [PATCH 26/43] Revert frameon change --- tests/_images/densmap_nocolor/expected.png | Bin 10993 -> 12657 bytes tests/test_plotting.py | 1 - 2 files changed, 1 deletion(-) diff --git a/tests/_images/densmap_nocolor/expected.png b/tests/_images/densmap_nocolor/expected.png index 46b145cdfb775a7d3907a35a3c6d28a28b1015bf..b8c8e6c5c3cdb881af6e51d1fbd631225ca50ab6 100644 GIT binary patch literal 12657 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 literal 10993 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 diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 66935fc45a..6f84657a76 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -1177,7 +1177,6 @@ def pbmc_scatterplots(pbmc_scatterplots_session) -> AnnData: partial( sc.pl.embedding, basis="X_densmap", - frameon=False, ), ), ], From 41525659b9c03fd826de4f655952b47a9628d69f Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:17:54 -0500 Subject: [PATCH 27/43] Change type name --- docs/conf.py | 2 +- src/scanpy/tools/__init__.py | 2 +- src/scanpy/tools/_types.py | 2 +- src/scanpy/tools/_umap.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 06bcd62798..75c09ba89f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -227,7 +227,7 @@ def setup(app: Sphinx): ("py:class", "scanpy._utils.Empty"), ("py:class", "numpy.random.mtrand.RandomState"), ("py:class", "scanpy.neighbors._types.KnnTransformerLike"), - ("py:class", "scanpy.tools._types.UmapMethodKwds"), + ("py:class", "scanpy.tools._types.DensmapMethodKwds"), # Will work once scipy 1.8 is released ("py:class", "scipy.sparse.base.spmatrix"), ("py:class", "scipy.sparse.csr.csr_matrix"), diff --git a/src/scanpy/tools/__init__.py b/src/scanpy/tools/__init__.py index 714ab234e4..4a242dea12 100644 --- a/src/scanpy/tools/__init__.py +++ b/src/scanpy/tools/__init__.py @@ -29,7 +29,7 @@ if TYPE_CHECKING: from typing import Any - from ._types import UmapMethodKwds # noqa: F401 + from ._types import DensmapMethodKwds # noqa: F401 def __getattr__(name: str) -> Any: diff --git a/src/scanpy/tools/_types.py b/src/scanpy/tools/_types.py index 35ba4fd459..eed8313e9c 100644 --- a/src/scanpy/tools/_types.py +++ b/src/scanpy/tools/_types.py @@ -3,7 +3,7 @@ from typing import TypedDict -class UmapMethodKwds(TypedDict, total=False): +class DensmapMethodKwds(TypedDict, total=False): dens_lambda: float dens_frac: float dens_var_shift: float diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index 649ddcc2de..c1c6ef388b 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -18,7 +18,7 @@ from anndata import AnnData from .._compat import _LegacyRandom - from ._types import UmapMethodKwds + from ._types import DensmapMethodKwds _InitPos = Literal["paga", "spectral", "random"] @@ -54,7 +54,7 @@ def umap( a: float | None = None, b: float | None = None, method: Literal["umap", "rapids", "densmap"] = "umap", - method_kwds: UmapMethodKwds | None = None, + method_kwds: DensmapMethodKwds | None = None, key_added: str | None = None, neighbors_key: str = "neighbors", copy: bool = False, From 8f8787a22a9d726afd91c2a197c5ffe40236a4bb Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:38:03 -0500 Subject: [PATCH 28/43] Check if specific to densmap --- tests/test_plotting.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 6f84657a76..c0dd868082 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -1021,6 +1021,7 @@ def pbmc_scatterplots_session() -> AnnData: sc.pp.neighbors(pbmc) sc.tl.tsne(pbmc, random_state=0, n_pcs=30) sc.tl.diffmap(pbmc) + sc.tl.umap(pbmc, key_added="X_another_umap") sc.tl.umap(pbmc, method="densmap") return pbmc @@ -1172,6 +1173,13 @@ def pbmc_scatterplots(pbmc_scatterplots_session) -> AnnData: mask_obs="mask", ), ), + ( + "another_umap", + partial( + sc.pl.embedding, + basis="X_another_umap", + ), + ), ( "densmap_nocolor", partial( From 3b68fe22b00c37a11efdb36e23336e84efc8be71 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:40:00 -0500 Subject: [PATCH 29/43] Add expected image --- tests/_images/another_umap/expected.png | Bin 0 -> 9269 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/_images/another_umap/expected.png diff --git a/tests/_images/another_umap/expected.png b/tests/_images/another_umap/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..9f57a9a022bd42f955d047f89c5bee6196f0704f GIT binary patch literal 9269 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 Date: Wed, 22 Jan 2025 11:03:35 -0500 Subject: [PATCH 30/43] Skipif for numba --- tests/_images/another_umap/expected.png | Bin 9269 -> 8909 bytes tests/_images/densmap_nocolor/expected.png | Bin 12657 -> 9365 bytes tests/test_plotting.py | 25 +++++++++++++++++---- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/_images/another_umap/expected.png b/tests/_images/another_umap/expected.png index 9f57a9a022bd42f955d047f89c5bee6196f0704f..b9d57cc546e1abc29328bfdc22557f3c86f66ffb 100644 GIT binary patch literal 8909 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 diff --git a/tests/test_plotting.py b/tests/test_plotting.py index c0dd868082..a6cf6e6153 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -1,6 +1,7 @@ from __future__ import annotations from functools import partial +from importlib.metadata import version from itertools import chain, combinations, repeat from pathlib import Path from typing import TYPE_CHECKING @@ -1018,11 +1019,11 @@ def pbmc_scatterplots_session() -> AnnData: pbmc.layers["sparse"] = pbmc.raw.X / 2 pbmc.layers["test"] = pbmc.X.copy() + 100 pbmc.var["numbers"] = [str(x) for x in range(pbmc.shape[1])] - sc.pp.neighbors(pbmc) + sc.pp.neighbors(pbmc, random_state=np.random.RandomState(1)) sc.tl.tsne(pbmc, random_state=0, n_pcs=30) sc.tl.diffmap(pbmc) - sc.tl.umap(pbmc, key_added="X_another_umap") - sc.tl.umap(pbmc, method="densmap") + sc.tl.umap(pbmc, key_added="X_another_umap", random_state=np.random.RandomState(1)) + sc.tl.umap(pbmc, method="densmap", random_state=np.random.RandomState(1)) return pbmc @@ -1173,6 +1174,22 @@ def pbmc_scatterplots(pbmc_scatterplots_session) -> AnnData: mask_obs="mask", ), ), + ], +) +def test_scatterplots(image_comparer, pbmc_scatterplots, id, fn): + save_and_compare_images = partial(image_comparer, ROOT, tol=15) + + fn(pbmc_scatterplots, show=False) + save_and_compare_images(id) + + +@pytest.mark.skipif( + Version(version("numba")) < Version("0.61.0"), + reason="Same random_state value produces different UMAP results between numba versions. See #2946", +) +@pytest.mark.parametrize( + ("id", "fn"), + [ ( "another_umap", partial( @@ -1189,7 +1206,7 @@ def pbmc_scatterplots(pbmc_scatterplots_session) -> AnnData: ), ], ) -def test_scatterplots(image_comparer, pbmc_scatterplots, id, fn): +def test_umap_scatterplots(image_comparer, pbmc_scatterplots, id, fn): save_and_compare_images = partial(image_comparer, ROOT, tol=15) fn(pbmc_scatterplots, show=False) From 3cc91fc021b90e2db54ed3b0aed3abfc8b449598 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:06:15 -0500 Subject: [PATCH 31/43] Use pkg_version --- tests/test_plotting.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index a6cf6e6153..bb550ffdb2 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -1,7 +1,6 @@ from __future__ import annotations from functools import partial -from importlib.metadata import version from itertools import chain, combinations, repeat from pathlib import Path from typing import TYPE_CHECKING @@ -1184,7 +1183,7 @@ def test_scatterplots(image_comparer, pbmc_scatterplots, id, fn): @pytest.mark.skipif( - Version(version("numba")) < Version("0.61.0"), + pkg_version("numba") < Version("0.61.0"), reason="Same random_state value produces different UMAP results between numba versions. See #2946", ) @pytest.mark.parametrize( From e8e00e62c51fdefcf0c1dd6f7abdc0246f5e7b2a Mon Sep 17 00:00:00 2001 From: Ilan Gold Date: Thu, 23 Jan 2025 10:08:46 +0100 Subject: [PATCH 32/43] Update .azure-pipelines.yml --- .azure-pipelines.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index f0e181f1ba..8849dce427 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -109,6 +109,11 @@ jobs: artifactName: debug-data condition: eq(variables['TEST_TYPE'], 'coverage') + - task: PublishBuildArtifacts@1 + inputs: + pathToPublish: '$(Pipeline.Workspace)/s/scanpy/tests/_images' + artifactName: drop + - script: bash <(curl -s https://codecov.io/bash) displayName: 'Upload to codecov.io' condition: eq(variables['TEST_TYPE'], 'coverage') From 5df86ac27ced6b4e120f8fe73c36e94b0285f1d8 Mon Sep 17 00:00:00 2001 From: Ilan Gold Date: Thu, 23 Jan 2025 11:20:14 +0100 Subject: [PATCH 33/43] Update .azure-pipelines.yml --- .azure-pipelines.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 8849dce427..98ee2c107d 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -109,6 +109,10 @@ jobs: artifactName: debug-data condition: eq(variables['TEST_TYPE'], 'coverage') + - script: | + echo "$(Pipeline.Workspace)/s/scanpy/tests/_images" + ls $(Pipeline.Workspace)/s + - task: PublishBuildArtifacts@1 inputs: pathToPublish: '$(Pipeline.Workspace)/s/scanpy/tests/_images' From 5c2be1e52ac935e0c3253025532d2fd1875086fa Mon Sep 17 00:00:00 2001 From: Ilan Gold Date: Thu, 23 Jan 2025 11:52:51 +0100 Subject: [PATCH 34/43] Update .azure-pipelines.yml --- .azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 98ee2c107d..41cc39d077 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -115,7 +115,7 @@ jobs: - task: PublishBuildArtifacts@1 inputs: - pathToPublish: '$(Pipeline.Workspace)/s/scanpy/tests/_images' + pathToPublish: '$(Pipeline.Workspace)/s/tests/_images' artifactName: drop - script: bash <(curl -s https://codecov.io/bash) From 1e4ac263a8baea4f169b2702186ec17d7926f209 Mon Sep 17 00:00:00 2001 From: Ilan Gold Date: Thu, 23 Jan 2025 13:17:03 +0100 Subject: [PATCH 35/43] Update .azure-pipelines.yml --- .azure-pipelines.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 41cc39d077..05df762ed1 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -109,14 +109,10 @@ jobs: artifactName: debug-data condition: eq(variables['TEST_TYPE'], 'coverage') - - script: | - echo "$(Pipeline.Workspace)/s/scanpy/tests/_images" - ls $(Pipeline.Workspace)/s - - task: PublishBuildArtifacts@1 inputs: pathToPublish: '$(Pipeline.Workspace)/s/tests/_images' - artifactName: drop + artifactName: '$(DEPENDENCIES_VERSION)-$(python.version)-images' - script: bash <(curl -s https://codecov.io/bash) displayName: 'Upload to codecov.io' From e40d9f7dacb1b7f920652c60e7d1a8621c5b9ed0 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Sun, 16 Feb 2025 11:21:00 -0500 Subject: [PATCH 36/43] Add numerical test for densmap. Add test for raises ValueError --- src/scanpy/tools/_umap.py | 3 ++ tests/test_embedding.py | 53 ++++++++++++++++++++++++++++++ tests/test_plotting.py | 69 ++++----------------------------------- 3 files changed, 63 insertions(+), 62 deletions(-) diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index c1c6ef388b..79ad5d4b47 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -333,6 +333,9 @@ def umap( random_state=random_state, ) X_umap = umap.fit_transform(X_contiguous) + else: + message = f"umap method parameter invalid: {method} not supported." + raise ValueError(message) adata.obsm[key_obsm] = X_umap # annotate samples with UMAP coordinates logg.info( diff --git a/tests/test_embedding.py b/tests/test_embedding.py index 65652b7eab..1f9c6440e9 100644 --- a/tests/test_embedding.py +++ b/tests/test_embedding.py @@ -3,6 +3,7 @@ import numpy as np import pytest from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_raises +from sklearn.mixture import GaussianMixture import scanpy as sc from testing.scanpy._helpers.data import pbmc68k_reduced @@ -109,3 +110,55 @@ def test_densmap(): sc.tl.umap(pbmc, method="densmap", method_kwds=dict(dens_lambda=2.3456)) d4 = pbmc.obsm["X_densmap"].copy() assert_raises(AssertionError, assert_array_equal, d1, d4) + + +def test_umap_raises_for_unsupported_method(): + pbmc = pbmc68k_reduced() + + # Checking that umap function raises a ValueError + # if a user passes an invalid `method` parameter. + with assert_raises(ValueError): + sc.tl.umap(pbmc, method="method_does_not_exist") + + +def get_mean_ellipse_area(gm): + # Adapted from GMM covariances ellipse plotting tutorial. + # Reference: https://scikit-learn.org/stable/auto_examples/mixture/plot_gmm_covariances.html + result = [] + for i in range(gm.n_components): + covariances = gm.covariances_[i][:2, :2] + v, _ = np.linalg.eigh(covariances) + v = 2.0 * np.sqrt(2.0) * np.sqrt(v) + width = v[0] + height = v[1] + result.append(np.pi * width * height) + return np.mean(np.array(result)) + + +def test_densmap_differs_from_umap(): + pbmc = pbmc68k_reduced() + + # Check that the areas of ellipses that result from + # fitting a Gaussian mixture model to the results + # of UMAP and DensMAP are different, + # with DensMAP ellipses having a larger area on average. + random_state = 1234 + sc.tl.umap(pbmc, method="densmap", random_state=random_state) + X_densmap = pbmc.obsm["X_densmap"].copy() + sc.tl.umap(pbmc, method="umap", random_state=random_state) + X_umap = pbmc.obsm["X_umap"].copy() + + # We fit a mixture model with as many components as + # there are louvain clusters, in this case 11. + n_components = pbmc.obs["louvain"].unique().shape[0] + assert n_components == 11 + + gm_umap = GaussianMixture(n_components=n_components, random_state=random_state).fit( + X_umap + ) + gm_densmap = GaussianMixture( + n_components=n_components, random_state=random_state + ).fit(X_densmap) + mean_area_umap = get_mean_ellipse_area(gm_umap) + mean_area_densmap = get_mean_ellipse_area(gm_densmap) + assert mean_area_densmap > mean_area_umap diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 1746d074ee..2f0f5f60cd 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -28,9 +28,6 @@ if TYPE_CHECKING: from collections.abc import Callable - from matplotlib.axes import Axes - - HERE: Path = Path(__file__).parent ROOT = HERE / "_images" @@ -845,30 +842,9 @@ def test_rank_genes_groups(image_comparer, name, fn): with plt.rc_context({"axes.grid": True, "figure.figsize": (4, 4)}): fn(pbmc) - key = "ranked_genes" if name == "basic" else f"ranked_genes_{name}" - save_and_compare_images(key) - plt.close() - - -def test_rank_genes_group_axes(image_comparer): - fn = next(fn for name, fn in _RANK_GENES_GROUPS_PARAMS if name == "basic") - - save_and_compare_images = partial(image_comparer, ROOT, tol=23) - - pbmc = pbmc68k_reduced() - sc.tl.rank_genes_groups(pbmc, "louvain", n_genes=pbmc.raw.shape[1]) - - pbmc.var["symbol"] = pbmc.var.index + "__" - - fig, ax = plt.subplots(figsize=(12, 16)) - ax.set_axis_off() - with plt.rc_context({"axes.grid": True}): - axes: list[Axes] = fn(pbmc, ax=ax, show=False) - - assert len(axes) == 11 - fig.show() - save_and_compare_images("ranked_genes") - plt.close() + key = "ranked_genes" if name == "basic" else f"ranked_genes_{name}" + save_and_compare_images(key) + plt.close() @pytest.fixture(scope="session") @@ -1042,11 +1018,9 @@ def pbmc_scatterplots_session() -> AnnData: pbmc.layers["sparse"] = pbmc.raw.X / 2 pbmc.layers["test"] = pbmc.X.copy() + 100 pbmc.var["numbers"] = [str(x) for x in range(pbmc.shape[1])] - sc.pp.neighbors(pbmc, random_state=np.random.RandomState(1)) + sc.pp.neighbors(pbmc) sc.tl.tsne(pbmc, random_state=0, n_pcs=30) sc.tl.diffmap(pbmc) - sc.tl.umap(pbmc, key_added="X_another_umap", random_state=np.random.RandomState(1)) - sc.tl.umap(pbmc, method="densmap", random_state=np.random.RandomState(1)) return pbmc @@ -1206,36 +1180,6 @@ def test_scatterplots(image_comparer, pbmc_scatterplots, id, fn): save_and_compare_images(id) -@pytest.mark.skipif( - pkg_version("numba") < Version("0.61.0"), - reason="Same random_state value produces different UMAP results between numba versions. See #2946", -) -@pytest.mark.parametrize( - ("id", "fn"), - [ - ( - "another_umap", - partial( - sc.pl.embedding, - basis="X_another_umap", - ), - ), - ( - "densmap_nocolor", - partial( - sc.pl.embedding, - basis="X_densmap", - ), - ), - ], -) -def test_umap_scatterplots(image_comparer, pbmc_scatterplots, id, fn): - save_and_compare_images = partial(image_comparer, ROOT, tol=15) - - fn(pbmc_scatterplots, show=False) - save_and_compare_images(id) - - def test_scatter_embedding_groups_and_size(image_comparer): # test that the 'groups' parameter sorts # cells, such that the cells belonging to the groups are @@ -1512,10 +1456,11 @@ def test_rankings(image_comparer): # TODO: Make more generic -def test_scatter_rep(tmp_path): +def test_scatter_rep(tmpdir): """ Test to make sure I can predict when scatter reps should be the same """ + TESTDIR = Path(tmpdir) rep_args = { "raw": {"use_raw": True}, "layer": {"layer": "layer", "use_raw": False}, @@ -1530,7 +1475,7 @@ def test_scatter_rep(tmp_path): columns=["rep", "gene", "result"], ) states["outpth"] = [ - tmp_path / f"{state.gene}_{state.rep}_{state.result}.png" + TESTDIR / f"{state.gene}_{state.rep}_{state.result}.png" for state in states.itertuples() ] pattern = np.array(list(chain.from_iterable(repeat(i, 5) for i in range(3)))) From 7a8b637ee58c5c18356c2d858f5e6492dc2fc676 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Sun, 16 Feb 2025 11:23:20 -0500 Subject: [PATCH 37/43] Merge with main --- tests/test_plotting.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 2f0f5f60cd..161b493823 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -28,6 +28,9 @@ if TYPE_CHECKING: from collections.abc import Callable + from matplotlib.axes import Axes + + HERE: Path = Path(__file__).parent ROOT = HERE / "_images" @@ -842,9 +845,30 @@ def test_rank_genes_groups(image_comparer, name, fn): with plt.rc_context({"axes.grid": True, "figure.figsize": (4, 4)}): fn(pbmc) - key = "ranked_genes" if name == "basic" else f"ranked_genes_{name}" - save_and_compare_images(key) - plt.close() + key = "ranked_genes" if name == "basic" else f"ranked_genes_{name}" + save_and_compare_images(key) + plt.close() + + +def test_rank_genes_group_axes(image_comparer): + fn = next(fn for name, fn in _RANK_GENES_GROUPS_PARAMS if name == "basic") + + save_and_compare_images = partial(image_comparer, ROOT, tol=23) + + pbmc = pbmc68k_reduced() + sc.tl.rank_genes_groups(pbmc, "louvain", n_genes=pbmc.raw.shape[1]) + + pbmc.var["symbol"] = pbmc.var.index + "__" + + fig, ax = plt.subplots(figsize=(12, 16)) + ax.set_axis_off() + with plt.rc_context({"axes.grid": True}): + axes: list[Axes] = fn(pbmc, ax=ax, show=False) + + assert len(axes) == 11 + fig.show() + save_and_compare_images("ranked_genes") + plt.close() @pytest.fixture(scope="session") @@ -1456,11 +1480,10 @@ def test_rankings(image_comparer): # TODO: Make more generic -def test_scatter_rep(tmpdir): +def test_scatter_rep(tmp_path): """ Test to make sure I can predict when scatter reps should be the same """ - TESTDIR = Path(tmpdir) rep_args = { "raw": {"use_raw": True}, "layer": {"layer": "layer", "use_raw": False}, @@ -1475,7 +1498,7 @@ def test_scatter_rep(tmpdir): columns=["rep", "gene", "result"], ) states["outpth"] = [ - TESTDIR / f"{state.gene}_{state.rep}_{state.result}.png" + tmp_path / f"{state.gene}_{state.rep}_{state.result}.png" for state in states.itertuples() ] pattern = np.array(list(chain.from_iterable(repeat(i, 5) for i in range(3)))) From a8f7bb41739a0b91fd99c6ecc904507b82fe48e4 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Sun, 16 Feb 2025 11:24:51 -0500 Subject: [PATCH 38/43] Remove unused image fixtures --- tests/_images/another_umap/expected.png | Bin 8909 -> 0 bytes tests/_images/densmap_nocolor/expected.png | Bin 9365 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/_images/another_umap/expected.png delete mode 100644 tests/_images/densmap_nocolor/expected.png diff --git a/tests/_images/another_umap/expected.png b/tests/_images/another_umap/expected.png deleted file mode 100644 index b9d57cc546e1abc29328bfdc22557f3c86f66ffb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8909 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 Date: Fri, 21 Feb 2025 08:09:16 -0500 Subject: [PATCH 39/43] Update tests/test_embedding.py Co-authored-by: Ilan Gold --- tests/test_embedding.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/tests/test_embedding.py b/tests/test_embedding.py index 1f9c6440e9..dc576e9755 100644 --- a/tests/test_embedding.py +++ b/tests/test_embedding.py @@ -143,22 +143,11 @@ def test_densmap_differs_from_umap(): # of UMAP and DensMAP are different, # with DensMAP ellipses having a larger area on average. random_state = 1234 - sc.tl.umap(pbmc, method="densmap", random_state=random_state) - X_densmap = pbmc.obsm["X_densmap"].copy() - sc.tl.umap(pbmc, method="umap", random_state=random_state) - X_umap = pbmc.obsm["X_umap"].copy() - - # We fit a mixture model with as many components as - # there are louvain clusters, in this case 11. + mean_area_results = [] n_components = pbmc.obs["louvain"].unique().shape[0] - assert n_components == 11 - - gm_umap = GaussianMixture(n_components=n_components, random_state=random_state).fit( - X_umap - ) - gm_densmap = GaussianMixture( - n_components=n_components, random_state=random_state - ).fit(X_densmap) - mean_area_umap = get_mean_ellipse_area(gm_umap) - mean_area_densmap = get_mean_ellipse_area(gm_densmap) - assert mean_area_densmap > mean_area_umap + for method in ["densmap", "umap"]: + sc.tl.umap(pbmc, method=method, random_state=random_state) + X_map = pbmc.obsm[f"X_{method}"].copy() + gm = GaussianMixture(n_components=n_components, random_state=random_state).fit(X_map) + mean_area_results.append(get_mean_ellipse_area(gm)) + assert gm[0] > gm[1] From 377767290000da87b388bddc67be2f22b32bb66d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:13:05 +0000 Subject: [PATCH 40/43] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_embedding.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_embedding.py b/tests/test_embedding.py index dc576e9755..d550dded5a 100644 --- a/tests/test_embedding.py +++ b/tests/test_embedding.py @@ -148,6 +148,8 @@ def test_densmap_differs_from_umap(): for method in ["densmap", "umap"]: sc.tl.umap(pbmc, method=method, random_state=random_state) X_map = pbmc.obsm[f"X_{method}"].copy() - gm = GaussianMixture(n_components=n_components, random_state=random_state).fit(X_map) + gm = GaussianMixture(n_components=n_components, random_state=random_state).fit( + X_map + ) mean_area_results.append(get_mean_ellipse_area(gm)) assert gm[0] > gm[1] From b262bc288ef535e9f03e6ff8cb1c9fc8e16278da Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Fri, 21 Feb 2025 08:39:39 -0500 Subject: [PATCH 41/43] WIP: try adding types --- .azure-pipelines.yml | 5 ----- tests/test_embedding.py | 27 ++++++++++++++++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 05df762ed1..f0e181f1ba 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -109,11 +109,6 @@ jobs: artifactName: debug-data condition: eq(variables['TEST_TYPE'], 'coverage') - - task: PublishBuildArtifacts@1 - inputs: - pathToPublish: '$(Pipeline.Workspace)/s/tests/_images' - artifactName: '$(DEPENDENCIES_VERSION)-$(python.version)-images' - - script: bash <(curl -s https://codecov.io/bash) displayName: 'Upload to codecov.io' condition: eq(variables['TEST_TYPE'], 'coverage') diff --git a/tests/test_embedding.py b/tests/test_embedding.py index dc576e9755..abb3115481 100644 --- a/tests/test_embedding.py +++ b/tests/test_embedding.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import TYPE_CHECKING, Protocol + import numpy as np import pytest from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_raises @@ -9,6 +11,9 @@ from testing.scanpy._helpers.data import pbmc68k_reduced from testing.scanpy._pytest.marks import needs +if TYPE_CHECKING: + from numpy.typing import ArrayLike, NDArray + @pytest.mark.parametrize( ("key_added", "key_obsm", "key_uns"), @@ -121,13 +126,23 @@ def test_umap_raises_for_unsupported_method(): sc.tl.umap(pbmc, method="method_does_not_exist") -def get_mean_ellipse_area(gm): +class GaussianMixtureLike(Protocol): + @property + def n_components(self) -> int: ... + @property + def covariances_(self) -> ArrayLike: ... + + +# Given a fit Gaussian mixture model with N components, +# return the mean of ellipse areas (one ellipse per component). +def get_mean_ellipse_area(gm: GaussianMixtureLike) -> np.floating: # Adapted from GMM covariances ellipse plotting tutorial. # Reference: https://scikit-learn.org/stable/auto_examples/mixture/plot_gmm_covariances.html result = [] + covariances: NDArray[np.float64] = np.asarray(gm.covariances_, dtype=np.float64) for i in range(gm.n_components): - covariances = gm.covariances_[i][:2, :2] - v, _ = np.linalg.eigh(covariances) + component_covariances = covariances[i][:2, :2] + v, _ = np.linalg.eigh(component_covariances) v = 2.0 * np.sqrt(2.0) * np.sqrt(v) width = v[0] height = v[1] @@ -148,6 +163,8 @@ def test_densmap_differs_from_umap(): for method in ["densmap", "umap"]: sc.tl.umap(pbmc, method=method, random_state=random_state) X_map = pbmc.obsm[f"X_{method}"].copy() - gm = GaussianMixture(n_components=n_components, random_state=random_state).fit(X_map) + gm = GaussianMixture(n_components=n_components, random_state=random_state).fit( + X_map + ) mean_area_results.append(get_mean_ellipse_area(gm)) - assert gm[0] > gm[1] + assert mean_area_results[0] > mean_area_results[1] From 4967a0064d11512dfd20f233c94c9315c14c2352 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:35:53 +0000 Subject: [PATCH 42/43] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/scanpy/tools/_umap.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index 138878b084..87ff832d1b 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -18,8 +18,8 @@ from anndata import AnnData from .._compat import _LegacyRandom - from ._types import DensmapMethodKwds from .._utils.random import _LegacyRandom + from ._types import DensmapMethodKwds _InitPos = Literal["paga", "spectral", "random"] @@ -265,13 +265,11 @@ def umap( # noqa: PLR0913, PLR0915 else {} ) if method == "densmap": - adata.uns[key_uns]["params"].update( - { - "dens_lambda": densmap_kwds["lambda"], - "dens_frac": densmap_kwds["frac"], - "dens_var_shift": densmap_kwds["var_shift"], - } - ) + adata.uns[key_uns]["params"].update({ + "dens_lambda": densmap_kwds["lambda"], + "dens_frac": densmap_kwds["frac"], + "dens_var_shift": densmap_kwds["var_shift"], + }) if method == "umap" or method == "densmap": # the data matrix X is really only used for determining the number of connected components From 22fb7a7782c9719a0ddf3bccb650ff9366d68341 Mon Sep 17 00:00:00 2001 From: Mark Keller <7525285+keller-mark@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:38:17 -0400 Subject: [PATCH 43/43] Fix import statement for _LegacyRandom --- src/scanpy/tools/_umap.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scanpy/tools/_umap.py b/src/scanpy/tools/_umap.py index 87ff832d1b..63d2ed4401 100644 --- a/src/scanpy/tools/_umap.py +++ b/src/scanpy/tools/_umap.py @@ -17,7 +17,6 @@ from anndata import AnnData - from .._compat import _LegacyRandom from .._utils.random import _LegacyRandom from ._types import DensmapMethodKwds