We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add layer argument to `score_genes()`/`score_genes_cell_cycle()`
### What kind of feature would you like to request?

Additional function parameters / changed functionality / changed defaults?

### Please describe your wishes

These functions are some of the few to still not have this argument. Would also help with #2909.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `scanpy/tools/_score_genes.py`
Content:
```
1 """Calculate scores based on the expression of gene lists."""
2 
3 from __future__ import annotations
4 
5 from typing import TYPE_CHECKING
6 
7 import numpy as np
8 import pandas as pd
9 from scipy.sparse import issparse
10 
11 from scanpy._utils import _check_use_raw, is_backed_type
12 
13 from .. import logging as logg
14 from .._compat import old_positionals
15 from ..get import _get_obs_rep
16 
17 if TYPE_CHECKING:
18     from collections.abc import Sequence
19     from typing import Literal
20 
21     from anndata import AnnData
22     from numpy.typing import DTypeLike, NDArray
23     from scipy.sparse import csc_matrix, csr_matrix
24 
25     from .._utils import AnyRandom
26 
27 
28 def _sparse_nanmean(
29     X: csr_matrix | csc_matrix, axis: Literal[0, 1]
30 ) -> NDArray[np.float64]:
31     """
32     np.nanmean equivalent for sparse matrices
33     """
34     if not issparse(X):
35         raise TypeError("X must be a sparse matrix")
36 
37     # count the number of nan elements per row/column (dep. on axis)
38     Z = X.copy()
39     Z.data = np.isnan(Z.data)
40     Z.eliminate_zeros()
41     n_elements = Z.shape[axis] - Z.sum(axis)
42 
43     # set the nans to 0, so that a normal .sum() works
44     Y = X.copy()
45     Y.data[np.isnan(Y.data)] = 0
46     Y.eliminate_zeros()
47 
48     # the average
49     s = Y.sum(axis, dtype="float64")  # float64 for score_genes function compatibility)
50     m = s / n_elements
51 
52     return m
53 
54 
55 @old_positionals(
56     "ctrl_size", "gene_pool", "n_bins", "score_name", "random_state", "copy", "use_raw"
57 )
58 def score_genes(
59     adata: AnnData,
60     gene_list: Sequence[str] | pd.Index[str],
61     *,
62     ctrl_size: int = 50,
63     gene_pool: Sequence[str] | pd.Index[str] | None = None,
64     n_bins: int = 25,
65     score_name: str = "score",
66     random_state: AnyRandom = 0,
67     copy: bool = False,
68     use_raw: bool | None = None,
69 ) -> AnnData | None:
70     """\
71     Score a set of genes :cite:p:`Satija2015`.
72 
73     The score is the average expression of a set of genes subtracted with the
74     average expression of a reference set of genes. The reference set is
75     randomly sampled from the `gene_pool` for each binned expression value.
76 
77     This reproduces the approach in Seurat :cite:p:`Satija2015` and has been implemented
78     for Scanpy by Davide Cittaro.
79 
80     Parameters
81     ----------
82     adata
83         The annotated data matrix.
84     gene_list
85         The list of gene names used for score calculation.
86     ctrl_size
87         Number of reference genes to be sampled from each bin. If `len(gene_list)` is not too
88         low, you can set `ctrl_size=len(gene_list)`.
89     gene_pool
90         Genes for sampling the reference set. Default is all genes.
91     n_bins
92         Number of expression level bins for sampling.
93     score_name
94         Name of the field to be added in `.obs`.
95     random_state
96         The random seed for sampling.
97     copy
98         Copy `adata` or modify it inplace.
99     use_raw
100         Whether to use `raw` attribute of `adata`. Defaults to `True` if `.raw` is present.
101 
102         .. versionchanged:: 1.4.5
103            Default value changed from `False` to `None`.
104 
105     Returns
106     -------
107     Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following field:
108 
109     `adata.obs[score_name]` : :class:`numpy.ndarray` (dtype `float`)
110         Scores of each cell.
111 
112     Examples
113     --------
114     See this `notebook <https://github.com/scverse/scanpy_usage/tree/master/180209_cell_cycle>`__.
115     """
116     start = logg.info(f"computing score {score_name!r}")
117     adata = adata.copy() if copy else adata
118     use_raw = _check_use_raw(adata, use_raw)
119     if is_backed_type(adata.X) and not use_raw:
120         raise NotImplementedError(
121             f"score_genes is not implemented for matrices of type {type(adata.X)}"
122         )
123 
124     if random_state is not None:
125         np.random.seed(random_state)
126 
127     var_names = adata.raw.var_names if use_raw else adata.var_names
128     gene_list = pd.Index([gene_list] if isinstance(gene_list, str) else gene_list)
129     genes_to_ignore = gene_list.difference(var_names, sort=False)  # first get missing
130     gene_list = gene_list.intersection(var_names)  # then restrict to present
131     if len(genes_to_ignore) > 0:
132         logg.warning(f"genes are not in var_names and ignored: {genes_to_ignore}")
133     if len(gene_list) == 0:
134         raise ValueError("No valid genes were passed for scoring.")
135 
136     if gene_pool is None:
137         gene_pool = pd.Index(var_names, dtype="string")
138     else:
139         gene_pool = pd.Index(gene_pool, dtype="string").intersection(var_names)
140     if len(gene_pool) == 0:
141         raise ValueError("No valid genes were passed for reference set.")
142 
143     # Trying here to match the Seurat approach in scoring cells.
144     # Basically we need to compare genes against random genes in a matched
145     # interval of expression.
146 
147     def get_subset(genes: pd.Index[str]):
148         x = _get_obs_rep(adata, use_raw=use_raw)
149         if len(genes) == len(var_names):
150             return x
151         idx = var_names.get_indexer(genes)
152         return x[:, idx]
153 
154     # average expression of genes
155     obs_avg = pd.Series(_nan_means(get_subset(gene_pool), axis=0), index=gene_pool)
156     # Sometimes (and I don’t know how) missing data may be there, with NaNs for missing entries
157     obs_avg = obs_avg[np.isfinite(obs_avg)]
158 
159     n_items = int(np.round(len(obs_avg) / (n_bins - 1)))
160     obs_cut = obs_avg.rank(method="min") // n_items
161     control_genes = pd.Index([], dtype="string")
162 
163     # now pick `ctrl_size` genes from every cut
164     for cut in np.unique(obs_cut.loc[gene_list]):
165         r_genes: pd.Index[str] = obs_cut[obs_cut == cut].index
166         if ctrl_size < len(r_genes):
167             r_genes = r_genes.to_series().sample(ctrl_size).index
168         control_genes = control_genes.union(r_genes.difference(gene_list))
169 
170     means_list, means_control = (
171         _nan_means(get_subset(genes), axis=1, dtype="float64")
172         for genes in (gene_list, control_genes)
173     )
174     score = means_list - means_control
175 
176     adata.obs[score_name] = pd.Series(
177         np.array(score).ravel(), index=adata.obs_names, dtype="float64"
178     )
179 
180     logg.info(
181         "    finished",
182         time=start,
183         deep=(
184             "added\n"
185             f"    {score_name!r}, score of gene set (adata.obs).\n"
186             f"    {len(control_genes)} total control genes are used."
187         ),
188     )
189     return adata if copy else None
190 
191 
192 def _nan_means(
193     x, *, axis: Literal[0, 1], dtype: DTypeLike | None = None
194 ) -> NDArray[np.float64]:
195     if issparse(x):
196         return np.array(_sparse_nanmean(x, axis=axis)).flatten()
197     return np.nanmean(x, axis=axis, dtype=dtype)
198 
199 
200 @old_positionals("s_genes", "g2m_genes", "copy")
201 def score_genes_cell_cycle(
202     adata: AnnData,
203     *,
204     s_genes: Sequence[str],
205     g2m_genes: Sequence[str],
206     copy: bool = False,
207     **kwargs,
208 ) -> AnnData | None:
209     """\
210     Score cell cycle genes :cite:p:`Satija2015`.
211 
212     Given two lists of genes associated to S phase and G2M phase, calculates
213     scores and assigns a cell cycle phase (G1, S or G2M). See
214     :func:`~scanpy.tl.score_genes` for more explanation.
215 
216     Parameters
217     ----------
218     adata
219         The annotated data matrix.
220     s_genes
221         List of genes associated with S phase.
222     g2m_genes
223         List of genes associated with G2M phase.
224     copy
225         Copy `adata` or modify it inplace.
226     **kwargs
227         Are passed to :func:`~scanpy.tl.score_genes`. `ctrl_size` is not
228         possible, as it's set as `min(len(s_genes), len(g2m_genes))`.
229 
230     Returns
231     -------
232     Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields:
233 
234     `adata.obs['S_score']` : :class:`pandas.Series` (dtype `object`)
235         The score for S phase for each cell.
236     `adata.obs['G2M_score']` : :class:`pandas.Series` (dtype `object`)
237         The score for G2M phase for each cell.
238     `adata.obs['phase']` : :class:`pandas.Series` (dtype `object`)
239         The cell cycle phase (`S`, `G2M` or `G1`) for each cell.
240 
241     See also
242     --------
243     score_genes
244 
245     Examples
246     --------
247     See this `notebook <https://github.com/scverse/scanpy_usage/tree/master/180209_cell_cycle>`__.
248     """
249     logg.info("calculating cell cycle phase")
250 
251     adata = adata.copy() if copy else adata
252     ctrl_size = min(len(s_genes), len(g2m_genes))
253     for genes, name in [(s_genes, "S_score"), (g2m_genes, "G2M_score")]:
254         score_genes(adata, genes, score_name=name, ctrl_size=ctrl_size, **kwargs)
255     scores = adata.obs[["S_score", "G2M_score"]]
256 
257     # default phase is S
258     phase = pd.Series("S", index=scores.index)
259 
260     # if G2M is higher than S, it's G2M
261     phase[scores["G2M_score"] > scores["S_score"]] = "G2M"
262 
263     # if all scores are negative, it's G1...
264     phase[np.all(scores < 0, axis=1)] = "G1"
265 
266     adata.obs["phase"] = phase
267     logg.hint("    'phase', cell cycle phase (adata.obs)")
268     return adata if copy else None
269
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/scanpy/tools/_score_genes.py b/scanpy/tools/_score_genes.py
--- a/scanpy/tools/_score_genes.py
+++ b/scanpy/tools/_score_genes.py
@@ -66,6 +66,7 @@
     random_state: AnyRandom = 0,
     copy: bool = False,
     use_raw: bool | None = None,
+    layer: str | None = None,
 ) -> AnnData | None:
     """\
     Score a set of genes :cite:p:`Satija2015`.
@@ -101,6 +102,8 @@
 
         .. versionchanged:: 1.4.5
            Default value changed from `False` to `None`.
+    layer
+        Key from `adata.layers` whose value will be used to perform tests on.
 
     Returns
     -------
@@ -145,7 +148,7 @@
     # interval of expression.
 
     def get_subset(genes: pd.Index[str]):
-        x = _get_obs_rep(adata, use_raw=use_raw)
+        x = _get_obs_rep(adata, use_raw=use_raw, layer=layer)
         if len(genes) == len(var_names):
             return x
         idx = var_names.get_indexer(genes)

{"golden_diff": "diff --git a/scanpy/tools/_score_genes.py b/scanpy/tools/_score_genes.py\n--- a/scanpy/tools/_score_genes.py\n+++ b/scanpy/tools/_score_genes.py\n@@ -66,6 +66,7 @@\n     random_state: AnyRandom = 0,\n     copy: bool = False,\n     use_raw: bool | None = None,\n+    layer: str | None = None,\n ) -> AnnData | None:\n     \"\"\"\\\n     Score a set of genes :cite:p:`Satija2015`.\n@@ -101,6 +102,8 @@\n \n         .. versionchanged:: 1.4.5\n            Default value changed from `False` to `None`.\n+    layer\n+        Key from `adata.layers` whose value will be used to perform tests on.\n \n     Returns\n     -------\n@@ -145,7 +148,7 @@\n     # interval of expression.\n \n     def get_subset(genes: pd.Index[str]):\n-        x = _get_obs_rep(adata, use_raw=use_raw)\n+        x = _get_obs_rep(adata, use_raw=use_raw, layer=layer)\n         if len(genes) == len(var_names):\n             return x\n         idx = var_names.get_indexer(genes)\n", "issue": "Add layer argument to `score_genes()`/`score_genes_cell_cycle()`\n### What kind of feature would you like to request?\n\nAdditional function parameters / changed functionality / changed defaults?\n\n### Please describe your wishes\n\nThese functions are some of the few to still not have this argument. Would also help with #2909.\n", "before_files": [{"content": "\"\"\"Calculate scores based on the expression of gene lists.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nimport numpy as np\nimport pandas as pd\nfrom scipy.sparse import issparse\n\nfrom scanpy._utils import _check_use_raw, is_backed_type\n\nfrom .. import logging as logg\nfrom .._compat import old_positionals\nfrom ..get import _get_obs_rep\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n    from typing import Literal\n\n    from anndata import AnnData\n    from numpy.typing import DTypeLike, NDArray\n    from scipy.sparse import csc_matrix, csr_matrix\n\n    from .._utils import AnyRandom\n\n\ndef _sparse_nanmean(\n    X: csr_matrix | csc_matrix, axis: Literal[0, 1]\n) -> NDArray[np.float64]:\n    \"\"\"\n    np.nanmean equivalent for sparse matrices\n    \"\"\"\n    if not issparse(X):\n        raise TypeError(\"X must be a sparse matrix\")\n\n    # count the number of nan elements per row/column (dep. on axis)\n    Z = X.copy()\n    Z.data = np.isnan(Z.data)\n    Z.eliminate_zeros()\n    n_elements = Z.shape[axis] - Z.sum(axis)\n\n    # set the nans to 0, so that a normal .sum() works\n    Y = X.copy()\n    Y.data[np.isnan(Y.data)] = 0\n    Y.eliminate_zeros()\n\n    # the average\n    s = Y.sum(axis, dtype=\"float64\")  # float64 for score_genes function compatibility)\n    m = s / n_elements\n\n    return m\n\n\n@old_positionals(\n    \"ctrl_size\", \"gene_pool\", \"n_bins\", \"score_name\", \"random_state\", \"copy\", \"use_raw\"\n)\ndef score_genes(\n    adata: AnnData,\n    gene_list: Sequence[str] | pd.Index[str],\n    *,\n    ctrl_size: int = 50,\n    gene_pool: Sequence[str] | pd.Index[str] | None = None,\n    n_bins: int = 25,\n    score_name: str = \"score\",\n    random_state: AnyRandom = 0,\n    copy: bool = False,\n    use_raw: bool | None = None,\n) -> AnnData | None:\n    \"\"\"\\\n    Score a set of genes :cite:p:`Satija2015`.\n\n    The score is the average expression of a set of genes subtracted with the\n    average expression of a reference set of genes. The reference set is\n    randomly sampled from the `gene_pool` for each binned expression value.\n\n    This reproduces the approach in Seurat :cite:p:`Satija2015` and has been implemented\n    for Scanpy by Davide Cittaro.\n\n    Parameters\n    ----------\n    adata\n        The annotated data matrix.\n    gene_list\n        The list of gene names used for score calculation.\n    ctrl_size\n        Number of reference genes to be sampled from each bin. If `len(gene_list)` is not too\n        low, you can set `ctrl_size=len(gene_list)`.\n    gene_pool\n        Genes for sampling the reference set. Default is all genes.\n    n_bins\n        Number of expression level bins for sampling.\n    score_name\n        Name of the field to be added in `.obs`.\n    random_state\n        The random seed for sampling.\n    copy\n        Copy `adata` or modify it inplace.\n    use_raw\n        Whether to use `raw` attribute of `adata`. Defaults to `True` if `.raw` is present.\n\n        .. versionchanged:: 1.4.5\n           Default value changed from `False` to `None`.\n\n    Returns\n    -------\n    Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following field:\n\n    `adata.obs[score_name]` : :class:`numpy.ndarray` (dtype `float`)\n        Scores of each cell.\n\n    Examples\n    --------\n    See this `notebook <https://github.com/scverse/scanpy_usage/tree/master/180209_cell_cycle>`__.\n    \"\"\"\n    start = logg.info(f\"computing score {score_name!r}\")\n    adata = adata.copy() if copy else adata\n    use_raw = _check_use_raw(adata, use_raw)\n    if is_backed_type(adata.X) and not use_raw:\n        raise NotImplementedError(\n            f\"score_genes is not implemented for matrices of type {type(adata.X)}\"\n        )\n\n    if random_state is not None:\n        np.random.seed(random_state)\n\n    var_names = adata.raw.var_names if use_raw else adata.var_names\n    gene_list = pd.Index([gene_list] if isinstance(gene_list, str) else gene_list)\n    genes_to_ignore = gene_list.difference(var_names, sort=False)  # first get missing\n    gene_list = gene_list.intersection(var_names)  # then restrict to present\n    if len(genes_to_ignore) > 0:\n        logg.warning(f\"genes are not in var_names and ignored: {genes_to_ignore}\")\n    if len(gene_list) == 0:\n        raise ValueError(\"No valid genes were passed for scoring.\")\n\n    if gene_pool is None:\n        gene_pool = pd.Index(var_names, dtype=\"string\")\n    else:\n        gene_pool = pd.Index(gene_pool, dtype=\"string\").intersection(var_names)\n    if len(gene_pool) == 0:\n        raise ValueError(\"No valid genes were passed for reference set.\")\n\n    # Trying here to match the Seurat approach in scoring cells.\n    # Basically we need to compare genes against random genes in a matched\n    # interval of expression.\n\n    def get_subset(genes: pd.Index[str]):\n        x = _get_obs_rep(adata, use_raw=use_raw)\n        if len(genes) == len(var_names):\n            return x\n        idx = var_names.get_indexer(genes)\n        return x[:, idx]\n\n    # average expression of genes\n    obs_avg = pd.Series(_nan_means(get_subset(gene_pool), axis=0), index=gene_pool)\n    # Sometimes (and I don\u2019t know how) missing data may be there, with NaNs for missing entries\n    obs_avg = obs_avg[np.isfinite(obs_avg)]\n\n    n_items = int(np.round(len(obs_avg) / (n_bins - 1)))\n    obs_cut = obs_avg.rank(method=\"min\") // n_items\n    control_genes = pd.Index([], dtype=\"string\")\n\n    # now pick `ctrl_size` genes from every cut\n    for cut in np.unique(obs_cut.loc[gene_list]):\n        r_genes: pd.Index[str] = obs_cut[obs_cut == cut].index\n        if ctrl_size < len(r_genes):\n            r_genes = r_genes.to_series().sample(ctrl_size).index\n        control_genes = control_genes.union(r_genes.difference(gene_list))\n\n    means_list, means_control = (\n        _nan_means(get_subset(genes), axis=1, dtype=\"float64\")\n        for genes in (gene_list, control_genes)\n    )\n    score = means_list - means_control\n\n    adata.obs[score_name] = pd.Series(\n        np.array(score).ravel(), index=adata.obs_names, dtype=\"float64\"\n    )\n\n    logg.info(\n        \"    finished\",\n        time=start,\n        deep=(\n            \"added\\n\"\n            f\"    {score_name!r}, score of gene set (adata.obs).\\n\"\n            f\"    {len(control_genes)} total control genes are used.\"\n        ),\n    )\n    return adata if copy else None\n\n\ndef _nan_means(\n    x, *, axis: Literal[0, 1], dtype: DTypeLike | None = None\n) -> NDArray[np.float64]:\n    if issparse(x):\n        return np.array(_sparse_nanmean(x, axis=axis)).flatten()\n    return np.nanmean(x, axis=axis, dtype=dtype)\n\n\n@old_positionals(\"s_genes\", \"g2m_genes\", \"copy\")\ndef score_genes_cell_cycle(\n    adata: AnnData,\n    *,\n    s_genes: Sequence[str],\n    g2m_genes: Sequence[str],\n    copy: bool = False,\n    **kwargs,\n) -> AnnData | None:\n    \"\"\"\\\n    Score cell cycle genes :cite:p:`Satija2015`.\n\n    Given two lists of genes associated to S phase and G2M phase, calculates\n    scores and assigns a cell cycle phase (G1, S or G2M). See\n    :func:`~scanpy.tl.score_genes` for more explanation.\n\n    Parameters\n    ----------\n    adata\n        The annotated data matrix.\n    s_genes\n        List of genes associated with S phase.\n    g2m_genes\n        List of genes associated with G2M phase.\n    copy\n        Copy `adata` or modify it inplace.\n    **kwargs\n        Are passed to :func:`~scanpy.tl.score_genes`. `ctrl_size` is not\n        possible, as it's set as `min(len(s_genes), len(g2m_genes))`.\n\n    Returns\n    -------\n    Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields:\n\n    `adata.obs['S_score']` : :class:`pandas.Series` (dtype `object`)\n        The score for S phase for each cell.\n    `adata.obs['G2M_score']` : :class:`pandas.Series` (dtype `object`)\n        The score for G2M phase for each cell.\n    `adata.obs['phase']` : :class:`pandas.Series` (dtype `object`)\n        The cell cycle phase (`S`, `G2M` or `G1`) for each cell.\n\n    See also\n    --------\n    score_genes\n\n    Examples\n    --------\n    See this `notebook <https://github.com/scverse/scanpy_usage/tree/master/180209_cell_cycle>`__.\n    \"\"\"\n    logg.info(\"calculating cell cycle phase\")\n\n    adata = adata.copy() if copy else adata\n    ctrl_size = min(len(s_genes), len(g2m_genes))\n    for genes, name in [(s_genes, \"S_score\"), (g2m_genes, \"G2M_score\")]:\n        score_genes(adata, genes, score_name=name, ctrl_size=ctrl_size, **kwargs)\n    scores = adata.obs[[\"S_score\", \"G2M_score\"]]\n\n    # default phase is S\n    phase = pd.Series(\"S\", index=scores.index)\n\n    # if G2M is higher than S, it's G2M\n    phase[scores[\"G2M_score\"] > scores[\"S_score\"]] = \"G2M\"\n\n    # if all scores are negative, it's G1...\n    phase[np.all(scores < 0, axis=1)] = \"G1\"\n\n    adata.obs[\"phase\"] = phase\n    logg.hint(\"    'phase', cell cycle phase (adata.obs)\")\n    return adata if copy else None\n", "path": "scanpy/tools/_score_genes.py"}], "after_files": [{"content": "\"\"\"Calculate scores based on the expression of gene lists.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nimport numpy as np\nimport pandas as pd\nfrom scipy.sparse import issparse\n\nfrom scanpy._utils import _check_use_raw, is_backed_type\n\nfrom .. import logging as logg\nfrom .._compat import old_positionals\nfrom ..get import _get_obs_rep\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n    from typing import Literal\n\n    from anndata import AnnData\n    from numpy.typing import DTypeLike, NDArray\n    from scipy.sparse import csc_matrix, csr_matrix\n\n    from .._utils import AnyRandom\n\n\ndef _sparse_nanmean(\n    X: csr_matrix | csc_matrix, axis: Literal[0, 1]\n) -> NDArray[np.float64]:\n    \"\"\"\n    np.nanmean equivalent for sparse matrices\n    \"\"\"\n    if not issparse(X):\n        raise TypeError(\"X must be a sparse matrix\")\n\n    # count the number of nan elements per row/column (dep. on axis)\n    Z = X.copy()\n    Z.data = np.isnan(Z.data)\n    Z.eliminate_zeros()\n    n_elements = Z.shape[axis] - Z.sum(axis)\n\n    # set the nans to 0, so that a normal .sum() works\n    Y = X.copy()\n    Y.data[np.isnan(Y.data)] = 0\n    Y.eliminate_zeros()\n\n    # the average\n    s = Y.sum(axis, dtype=\"float64\")  # float64 for score_genes function compatibility)\n    m = s / n_elements\n\n    return m\n\n\n@old_positionals(\n    \"ctrl_size\", \"gene_pool\", \"n_bins\", \"score_name\", \"random_state\", \"copy\", \"use_raw\"\n)\ndef score_genes(\n    adata: AnnData,\n    gene_list: Sequence[str] | pd.Index[str],\n    *,\n    ctrl_size: int = 50,\n    gene_pool: Sequence[str] | pd.Index[str] | None = None,\n    n_bins: int = 25,\n    score_name: str = \"score\",\n    random_state: AnyRandom = 0,\n    copy: bool = False,\n    use_raw: bool | None = None,\n    layer: str | None = None,\n) -> AnnData | None:\n    \"\"\"\\\n    Score a set of genes :cite:p:`Satija2015`.\n\n    The score is the average expression of a set of genes subtracted with the\n    average expression of a reference set of genes. The reference set is\n    randomly sampled from the `gene_pool` for each binned expression value.\n\n    This reproduces the approach in Seurat :cite:p:`Satija2015` and has been implemented\n    for Scanpy by Davide Cittaro.\n\n    Parameters\n    ----------\n    adata\n        The annotated data matrix.\n    gene_list\n        The list of gene names used for score calculation.\n    ctrl_size\n        Number of reference genes to be sampled from each bin. If `len(gene_list)` is not too\n        low, you can set `ctrl_size=len(gene_list)`.\n    gene_pool\n        Genes for sampling the reference set. Default is all genes.\n    n_bins\n        Number of expression level bins for sampling.\n    score_name\n        Name of the field to be added in `.obs`.\n    random_state\n        The random seed for sampling.\n    copy\n        Copy `adata` or modify it inplace.\n    use_raw\n        Whether to use `raw` attribute of `adata`. Defaults to `True` if `.raw` is present.\n\n        .. versionchanged:: 1.4.5\n           Default value changed from `False` to `None`.\n    layer\n        Key from `adata.layers` whose value will be used to perform tests on.\n\n    Returns\n    -------\n    Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following field:\n\n    `adata.obs[score_name]` : :class:`numpy.ndarray` (dtype `float`)\n        Scores of each cell.\n\n    Examples\n    --------\n    See this `notebook <https://github.com/scverse/scanpy_usage/tree/master/180209_cell_cycle>`__.\n    \"\"\"\n    start = logg.info(f\"computing score {score_name!r}\")\n    adata = adata.copy() if copy else adata\n    use_raw = _check_use_raw(adata, use_raw)\n    if is_backed_type(adata.X) and not use_raw:\n        raise NotImplementedError(\n            f\"score_genes is not implemented for matrices of type {type(adata.X)}\"\n        )\n\n    if random_state is not None:\n        np.random.seed(random_state)\n\n    var_names = adata.raw.var_names if use_raw else adata.var_names\n    gene_list = pd.Index([gene_list] if isinstance(gene_list, str) else gene_list)\n    genes_to_ignore = gene_list.difference(var_names, sort=False)  # first get missing\n    gene_list = gene_list.intersection(var_names)  # then restrict to present\n    if len(genes_to_ignore) > 0:\n        logg.warning(f\"genes are not in var_names and ignored: {genes_to_ignore}\")\n    if len(gene_list) == 0:\n        raise ValueError(\"No valid genes were passed for scoring.\")\n\n    if gene_pool is None:\n        gene_pool = pd.Index(var_names, dtype=\"string\")\n    else:\n        gene_pool = pd.Index(gene_pool, dtype=\"string\").intersection(var_names)\n    if len(gene_pool) == 0:\n        raise ValueError(\"No valid genes were passed for reference set.\")\n\n    # Trying here to match the Seurat approach in scoring cells.\n    # Basically we need to compare genes against random genes in a matched\n    # interval of expression.\n\n    def get_subset(genes: pd.Index[str]):\n        x = _get_obs_rep(adata, use_raw=use_raw, layer=layer)\n        if len(genes) == len(var_names):\n            return x\n        idx = var_names.get_indexer(genes)\n        return x[:, idx]\n\n    # average expression of genes\n    obs_avg = pd.Series(_nan_means(get_subset(gene_pool), axis=0), index=gene_pool)\n    # Sometimes (and I don\u2019t know how) missing data may be there, with NaNs for missing entries\n    obs_avg = obs_avg[np.isfinite(obs_avg)]\n\n    n_items = int(np.round(len(obs_avg) / (n_bins - 1)))\n    obs_cut = obs_avg.rank(method=\"min\") // n_items\n    control_genes = pd.Index([], dtype=\"string\")\n\n    # now pick `ctrl_size` genes from every cut\n    for cut in np.unique(obs_cut.loc[gene_list]):\n        r_genes: pd.Index[str] = obs_cut[obs_cut == cut].index\n        if ctrl_size < len(r_genes):\n            r_genes = r_genes.to_series().sample(ctrl_size).index\n        control_genes = control_genes.union(r_genes.difference(gene_list))\n\n    means_list, means_control = (\n        _nan_means(get_subset(genes), axis=1, dtype=\"float64\")\n        for genes in (gene_list, control_genes)\n    )\n    score = means_list - means_control\n\n    adata.obs[score_name] = pd.Series(\n        np.array(score).ravel(), index=adata.obs_names, dtype=\"float64\"\n    )\n\n    logg.info(\n        \"    finished\",\n        time=start,\n        deep=(\n            \"added\\n\"\n            f\"    {score_name!r}, score of gene set (adata.obs).\\n\"\n            f\"    {len(control_genes)} total control genes are used.\"\n        ),\n    )\n    return adata if copy else None\n\n\ndef _nan_means(\n    x, *, axis: Literal[0, 1], dtype: DTypeLike | None = None\n) -> NDArray[np.float64]:\n    if issparse(x):\n        return np.array(_sparse_nanmean(x, axis=axis)).flatten()\n    return np.nanmean(x, axis=axis, dtype=dtype)\n\n\n@old_positionals(\"s_genes\", \"g2m_genes\", \"copy\")\ndef score_genes_cell_cycle(\n    adata: AnnData,\n    *,\n    s_genes: Sequence[str],\n    g2m_genes: Sequence[str],\n    copy: bool = False,\n    **kwargs,\n) -> AnnData | None:\n    \"\"\"\\\n    Score cell cycle genes :cite:p:`Satija2015`.\n\n    Given two lists of genes associated to S phase and G2M phase, calculates\n    scores and assigns a cell cycle phase (G1, S or G2M). See\n    :func:`~scanpy.tl.score_genes` for more explanation.\n\n    Parameters\n    ----------\n    adata\n        The annotated data matrix.\n    s_genes\n        List of genes associated with S phase.\n    g2m_genes\n        List of genes associated with G2M phase.\n    copy\n        Copy `adata` or modify it inplace.\n    **kwargs\n        Are passed to :func:`~scanpy.tl.score_genes`. `ctrl_size` is not\n        possible, as it's set as `min(len(s_genes), len(g2m_genes))`.\n\n    Returns\n    -------\n    Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields:\n\n    `adata.obs['S_score']` : :class:`pandas.Series` (dtype `object`)\n        The score for S phase for each cell.\n    `adata.obs['G2M_score']` : :class:`pandas.Series` (dtype `object`)\n        The score for G2M phase for each cell.\n    `adata.obs['phase']` : :class:`pandas.Series` (dtype `object`)\n        The cell cycle phase (`S`, `G2M` or `G1`) for each cell.\n\n    See also\n    --------\n    score_genes\n\n    Examples\n    --------\n    See this `notebook <https://github.com/scverse/scanpy_usage/tree/master/180209_cell_cycle>`__.\n    \"\"\"\n    logg.info(\"calculating cell cycle phase\")\n\n    adata = adata.copy() if copy else adata\n    ctrl_size = min(len(s_genes), len(g2m_genes))\n    for genes, name in [(s_genes, \"S_score\"), (g2m_genes, \"G2M_score\")]:\n        score_genes(adata, genes, score_name=name, ctrl_size=ctrl_size, **kwargs)\n    scores = adata.obs[[\"S_score\", \"G2M_score\"]]\n\n    # default phase is S\n    phase = pd.Series(\"S\", index=scores.index)\n\n    # if G2M is higher than S, it's G2M\n    phase[scores[\"G2M_score\"] > scores[\"S_score\"]] = \"G2M\"\n\n    # if all scores are negative, it's G1...\n    phase[np.all(scores < 0, axis=1)] = \"G1\"\n\n    adata.obs[\"phase\"] = phase\n    logg.hint(\"    'phase', cell cycle phase (adata.obs)\")\n    return adata if copy else None\n", "path": "scanpy/tools/_score_genes.py"}]}