dextrademixer 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Benjamin Schubert
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: dextrademixer
3
+ Version: 0.0.1
4
+ Summary: Probabilistic identification of antigen-specific T cells from single-cell pMHC multimer data
5
+ Author-email: Yang an <yang.an@helmholtz-munich.de>, Benjamin Schubert <benjamin.schubert@helmholtz-munich.de>
6
+ Maintainer-email: Yang an <yang.an@helmholtz-munich.de>, Benjamin Schubert <benjamin.schubert@helmholtz-munich.de>
7
+ License-Expression: BSD-3-Clause
8
+ Project-URL: Homepage, https://github.com/SchubertLab/DextraDemixer
9
+ Project-URL: Issues, https://github.com/SchubertLab/DextraDemixer/issues
10
+ Project-URL: Publication, https://doi.org/10.64898/2026.06.23.733339
11
+ Project-URL: Repository, https://github.com/SchubertLab/DextraDemixer.git
12
+ Keywords: antigen specificity,pMHC multimer,single-cell,T cell
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: anndata>=0.12.0
21
+ Requires-Dist: arviz<1.0,>=0.22.0
22
+ Requires-Dist: jax>=0.5.1
23
+ Requires-Dist: matplotlib>=3.10.7
24
+ Requires-Dist: mudata>=0.3.2
25
+ Requires-Dist: muon>=0.1.7
26
+ Requires-Dist: numpy>=1.26.0
27
+ Requires-Dist: numpyro>=0.16.0
28
+ Requires-Dist: optax>=0.1.7
29
+ Requires-Dist: pandas>=2.3.3
30
+ Requires-Dist: scanpy>=1.10.0
31
+ Requires-Dist: scikit-learn>=1.7.2
32
+ Requires-Dist: scipy>=1.12.0
33
+ Requires-Dist: scirpy>=0.13.0
34
+ Requires-Dist: seaborn>=0.13.2
35
+ Requires-Dist: statsmodels>=0.14.1
36
+ Requires-Dist: tqdm>=4.68.3
37
+ Provides-Extra: test
38
+ Requires-Dist: pytest; extra == "test"
39
+ Dynamic: license-file
40
+
41
+ # DextraDemixer
42
+
43
+ [![bioRxiv](https://img.shields.io/badge/bioRxiv-10.64898%2F2026.06.23.733339-b31b1b)](https://doi.org/10.64898/2026.06.23.733339)
44
+ [![License: BSD 3-Clause](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://github.com/SchubertLab/DextraDemixer/blob/main/LICENSE)
45
+
46
+ **Probabilistic identification of antigen-specific T cells from single-cell pMHC multimer experiments.**
47
+
48
+ DextraDemixer models pMHC multimer UMI counts as a mixture of specific and nonspecific binding components. It returns a posterior binding probability and assignment for each cell, helping separate antigen-specific signal from background binding and assay noise.
49
+
50
+ The method is described in [*DextraDemixer enables accurate identification of antigen-specific T cells from pMHC multimer experiments*](https://doi.org/10.64898/2026.06.23.733339).
51
+
52
+ > [!NOTE]
53
+ > DextraDemixer is research software under active development. Interfaces may change while the package matures.
54
+
55
+ ## Highlights
56
+
57
+ - Infers cell-level posterior probabilities of antigen specificity from pMHC UMI counts.
58
+ - Supports optional negative-control multimers.
59
+ - Incorporates clonotype information through clone-level probability aggregation.
60
+ - Provides fixed-threshold and Bayesian false discovery rate (FDR) assignments.
61
+ - Works directly with [`MuData`](https://mudata.readthedocs.io/), [`AnnData`](https://anndata.readthedocs.io/), and [`Pandas DataFrames`](https://pandas.pydata.org/) objects and uses [JAX](https://jax.readthedocs.io/) and [NumPyro](https://num.pyro.ai/) for inference.
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install dextrademixer
67
+ ```
68
+
69
+ ### Alternative: Conda environment
70
+
71
+ For a conda environment.
72
+
73
+ ```bash
74
+ git clone https://github.com/SchubertLab/DextraDemixer.git
75
+ cd DextraDemixer
76
+ conda env create -f environment.yaml
77
+ conda activate dextrademixer
78
+ pip install -e . # or pip install dextrademixer
79
+ ```
80
+
81
+ The final command installs the local package in editable mode. Runtime dependency constraints are aligned between `pyproject.toml` and `environment.yaml`. To reproduce the numbers from the manuscript, use `environment_reproducible.yaml` instead: it pins the exact versions, and results can shift slightly with other JAX/NumPyro releases.
82
+
83
+ ## Quick start
84
+
85
+ ### Single pMHC
86
+
87
+ The example below fits one pMHC feature and classifies cells using a posterior-probability threshold of 0.5.
88
+
89
+ ```python
90
+ import muon as mu
91
+
92
+ from dextrademixer import DextraDemixer
93
+
94
+ mdata = mu.read("data/example_data.h5mu")
95
+
96
+ model = DextraDemixer().fit(mdata, pmhc_key="pmhc1", pmhc_modality_key="gex")
97
+ p_binder, is_binder = model.predict(threshold=0.5)
98
+
99
+ mdata.mod["gex"].obs["dextrademixer_probability"] = p_binder
100
+ mdata.mod["gex"].obs["dextrademixer_assignment"] = is_binder
101
+ ```
102
+
103
+ To include a negative-control multimer, pass `neg_ctrl_key`. To aggregate probabilities within clonotypes, pass the clonotype column as `ir_clone_key` and set `clonotype_median_p=True` when predicting:
104
+
105
+ ```python
106
+ model = DextraDemixer().fit(
107
+ mdata,
108
+ pmhc_key="pmhc1",
109
+ pmhc_modality_key="gex",
110
+ neg_ctrl_key="neg_control",
111
+ ir_clone_key="clone_id",
112
+ )
113
+
114
+ p_pred, assignment = model.predict(threshold=0.5, clonotype_median_p=True)
115
+ ```
116
+
117
+ Use either `threshold` for a fixed decision boundary or `target_fdr` for FDR-controlled assignments; do not specify both.
118
+
119
+ ### Multiple pMHCs
120
+
121
+ `DextraDemixerMulti` fits one independent `DextraDemixer` per pMHC and returns cells x pMHC tables instead of vectors. Each fit stays reachable as `model.demixers[pmhc_key]`.
122
+
123
+ ```python
124
+ from dextrademixer import DextraDemixerMulti
125
+
126
+ mdata = mu.read("data/example_multi_pmhc.h5mu")
127
+
128
+ model = DextraDemixerMulti().fit(
129
+ mdata,
130
+ pmhc_keys=["pmhc1", "pmhc2", "pmhc3"],
131
+ pmhc_modality_key="gex",
132
+ neg_ctrl_key="neg_control",
133
+ )
134
+
135
+ p_pred, assignment = model.predict(threshold=0.5, clonotype_median_p=True, max_prob=True)
136
+ ```
137
+
138
+ Because the pMHCs are fitted independently, a cell can pass the threshold for several. `max_prob=True` keeps only the pMHC with the highest probability per cell, combined with `clonotype_median_p=True` the same choice is made per clonotype.
139
+
140
+ ## Input data
141
+
142
+ DextraDemixer needs the pMHC UMI counts and, optionally, a cell-level clonotype identifier. It reads them from any of:
143
+
144
+ - A [`MuData`](https://mudata.readthedocs.io/) object: counts in the `.X` of the feature modality (`pmhc_modality_key`, `gex` by default), clonotypes in the `.obs` of the AIRR modality (`ir_modality_key`, `airr` by default).
145
+ - An [`AnnData`](https://anndata.readthedocs.io/) object: counts in `.X`, clonotypes in `.obs`; `pmhc_modality_key` and `ir_modality_key` are then unused.
146
+ - A cells x features [`DataFrame`](https://pandas.pydata.org/): counts and annotation in the same table, so every key is a column name.
147
+
148
+ `pmhc_key` (or `pmhc_keys`) and the optional `neg_ctrl_key` name the count columns, `ir_clone_key` the clonotype column, whose ids may be integers or strings. The bundled example dataset ships in all three formats: `data/example_data.h5mu`, `.h5ad` and `.csv`. `data/example_multi_pmhc.h5mu` is a simulated three-pMHC panel.
149
+
150
+ See [`Tutorial.ipynb`](https://github.com/SchubertLab/DextraDemixer/blob/main/Tutorial.ipynb) for a complete, reproducible workflow using the bundled example dataset, including configuration, model fitting, evaluation, and visualization, and [`Tutorial DextraDemixerMulti.ipynb`](https://github.com/SchubertLab/DextraDemixer/blob/main/Tutorial%20DextraDemixerMulti.ipynb) for the same with multiple pMHCs:
151
+
152
+ ```bash
153
+ jupyter lab Tutorial.ipynb
154
+ ```
155
+
156
+ ## Reproducing the manuscript
157
+
158
+ The analyses and figure-generation code used in the manuscript are maintained in the [DextraDemixer reproducibility repository](https://github.com/SchubertLab/DextraDemixer_reproducibility).
159
+
160
+ ## Contributing
161
+
162
+ Bug reports, feature requests, and pull requests are welcome through the [GitHub issue tracker](https://github.com/SchubertLab/DextraDemixer/issues). When reporting a problem, please include a minimal reproducible example and details of your operating system and Conda environment.
163
+
164
+ ## Citation
165
+
166
+ If DextraDemixer supports your research, please cite:
167
+
168
+ ```bibtex
169
+ @article{An2026DextraDemixer,
170
+ author = {An, Yang and Drost, Felix and Bonafonte-Pard{\`a}s, Irene and Grotz, Myriam and Schober, Kilian and Schubert, Benjamin},
171
+ title = {DextraDemixer enables accurate identification of antigen-specific T cells from pMHC multimer experiments},
172
+ journal = {bioRxiv},
173
+ year = {2026},
174
+ publisher = {Cold Spring Harbor Laboratory},
175
+ doi = {10.64898/2026.06.23.733339},
176
+ url = {https://doi.org/10.64898/2026.06.23.733339},
177
+ elocation-id = {2026.06.23.733339}
178
+ }
179
+ ```
180
+
181
+ ## License
182
+
183
+ DextraDemixer is distributed under the [BSD 3-Clause License](https://github.com/SchubertLab/DextraDemixer/blob/main/LICENSE).
@@ -0,0 +1,143 @@
1
+ # DextraDemixer
2
+
3
+ [![bioRxiv](https://img.shields.io/badge/bioRxiv-10.64898%2F2026.06.23.733339-b31b1b)](https://doi.org/10.64898/2026.06.23.733339)
4
+ [![License: BSD 3-Clause](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://github.com/SchubertLab/DextraDemixer/blob/main/LICENSE)
5
+
6
+ **Probabilistic identification of antigen-specific T cells from single-cell pMHC multimer experiments.**
7
+
8
+ DextraDemixer models pMHC multimer UMI counts as a mixture of specific and nonspecific binding components. It returns a posterior binding probability and assignment for each cell, helping separate antigen-specific signal from background binding and assay noise.
9
+
10
+ The method is described in [*DextraDemixer enables accurate identification of antigen-specific T cells from pMHC multimer experiments*](https://doi.org/10.64898/2026.06.23.733339).
11
+
12
+ > [!NOTE]
13
+ > DextraDemixer is research software under active development. Interfaces may change while the package matures.
14
+
15
+ ## Highlights
16
+
17
+ - Infers cell-level posterior probabilities of antigen specificity from pMHC UMI counts.
18
+ - Supports optional negative-control multimers.
19
+ - Incorporates clonotype information through clone-level probability aggregation.
20
+ - Provides fixed-threshold and Bayesian false discovery rate (FDR) assignments.
21
+ - Works directly with [`MuData`](https://mudata.readthedocs.io/), [`AnnData`](https://anndata.readthedocs.io/), and [`Pandas DataFrames`](https://pandas.pydata.org/) objects and uses [JAX](https://jax.readthedocs.io/) and [NumPyro](https://num.pyro.ai/) for inference.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install dextrademixer
27
+ ```
28
+
29
+ ### Alternative: Conda environment
30
+
31
+ For a conda environment.
32
+
33
+ ```bash
34
+ git clone https://github.com/SchubertLab/DextraDemixer.git
35
+ cd DextraDemixer
36
+ conda env create -f environment.yaml
37
+ conda activate dextrademixer
38
+ pip install -e . # or pip install dextrademixer
39
+ ```
40
+
41
+ The final command installs the local package in editable mode. Runtime dependency constraints are aligned between `pyproject.toml` and `environment.yaml`. To reproduce the numbers from the manuscript, use `environment_reproducible.yaml` instead: it pins the exact versions, and results can shift slightly with other JAX/NumPyro releases.
42
+
43
+ ## Quick start
44
+
45
+ ### Single pMHC
46
+
47
+ The example below fits one pMHC feature and classifies cells using a posterior-probability threshold of 0.5.
48
+
49
+ ```python
50
+ import muon as mu
51
+
52
+ from dextrademixer import DextraDemixer
53
+
54
+ mdata = mu.read("data/example_data.h5mu")
55
+
56
+ model = DextraDemixer().fit(mdata, pmhc_key="pmhc1", pmhc_modality_key="gex")
57
+ p_binder, is_binder = model.predict(threshold=0.5)
58
+
59
+ mdata.mod["gex"].obs["dextrademixer_probability"] = p_binder
60
+ mdata.mod["gex"].obs["dextrademixer_assignment"] = is_binder
61
+ ```
62
+
63
+ To include a negative-control multimer, pass `neg_ctrl_key`. To aggregate probabilities within clonotypes, pass the clonotype column as `ir_clone_key` and set `clonotype_median_p=True` when predicting:
64
+
65
+ ```python
66
+ model = DextraDemixer().fit(
67
+ mdata,
68
+ pmhc_key="pmhc1",
69
+ pmhc_modality_key="gex",
70
+ neg_ctrl_key="neg_control",
71
+ ir_clone_key="clone_id",
72
+ )
73
+
74
+ p_pred, assignment = model.predict(threshold=0.5, clonotype_median_p=True)
75
+ ```
76
+
77
+ Use either `threshold` for a fixed decision boundary or `target_fdr` for FDR-controlled assignments; do not specify both.
78
+
79
+ ### Multiple pMHCs
80
+
81
+ `DextraDemixerMulti` fits one independent `DextraDemixer` per pMHC and returns cells x pMHC tables instead of vectors. Each fit stays reachable as `model.demixers[pmhc_key]`.
82
+
83
+ ```python
84
+ from dextrademixer import DextraDemixerMulti
85
+
86
+ mdata = mu.read("data/example_multi_pmhc.h5mu")
87
+
88
+ model = DextraDemixerMulti().fit(
89
+ mdata,
90
+ pmhc_keys=["pmhc1", "pmhc2", "pmhc3"],
91
+ pmhc_modality_key="gex",
92
+ neg_ctrl_key="neg_control",
93
+ )
94
+
95
+ p_pred, assignment = model.predict(threshold=0.5, clonotype_median_p=True, max_prob=True)
96
+ ```
97
+
98
+ Because the pMHCs are fitted independently, a cell can pass the threshold for several. `max_prob=True` keeps only the pMHC with the highest probability per cell, combined with `clonotype_median_p=True` the same choice is made per clonotype.
99
+
100
+ ## Input data
101
+
102
+ DextraDemixer needs the pMHC UMI counts and, optionally, a cell-level clonotype identifier. It reads them from any of:
103
+
104
+ - A [`MuData`](https://mudata.readthedocs.io/) object: counts in the `.X` of the feature modality (`pmhc_modality_key`, `gex` by default), clonotypes in the `.obs` of the AIRR modality (`ir_modality_key`, `airr` by default).
105
+ - An [`AnnData`](https://anndata.readthedocs.io/) object: counts in `.X`, clonotypes in `.obs`; `pmhc_modality_key` and `ir_modality_key` are then unused.
106
+ - A cells x features [`DataFrame`](https://pandas.pydata.org/): counts and annotation in the same table, so every key is a column name.
107
+
108
+ `pmhc_key` (or `pmhc_keys`) and the optional `neg_ctrl_key` name the count columns, `ir_clone_key` the clonotype column, whose ids may be integers or strings. The bundled example dataset ships in all three formats: `data/example_data.h5mu`, `.h5ad` and `.csv`. `data/example_multi_pmhc.h5mu` is a simulated three-pMHC panel.
109
+
110
+ See [`Tutorial.ipynb`](https://github.com/SchubertLab/DextraDemixer/blob/main/Tutorial.ipynb) for a complete, reproducible workflow using the bundled example dataset, including configuration, model fitting, evaluation, and visualization, and [`Tutorial DextraDemixerMulti.ipynb`](https://github.com/SchubertLab/DextraDemixer/blob/main/Tutorial%20DextraDemixerMulti.ipynb) for the same with multiple pMHCs:
111
+
112
+ ```bash
113
+ jupyter lab Tutorial.ipynb
114
+ ```
115
+
116
+ ## Reproducing the manuscript
117
+
118
+ The analyses and figure-generation code used in the manuscript are maintained in the [DextraDemixer reproducibility repository](https://github.com/SchubertLab/DextraDemixer_reproducibility).
119
+
120
+ ## Contributing
121
+
122
+ Bug reports, feature requests, and pull requests are welcome through the [GitHub issue tracker](https://github.com/SchubertLab/DextraDemixer/issues). When reporting a problem, please include a minimal reproducible example and details of your operating system and Conda environment.
123
+
124
+ ## Citation
125
+
126
+ If DextraDemixer supports your research, please cite:
127
+
128
+ ```bibtex
129
+ @article{An2026DextraDemixer,
130
+ author = {An, Yang and Drost, Felix and Bonafonte-Pard{\`a}s, Irene and Grotz, Myriam and Schober, Kilian and Schubert, Benjamin},
131
+ title = {DextraDemixer enables accurate identification of antigen-specific T cells from pMHC multimer experiments},
132
+ journal = {bioRxiv},
133
+ year = {2026},
134
+ publisher = {Cold Spring Harbor Laboratory},
135
+ doi = {10.64898/2026.06.23.733339},
136
+ url = {https://doi.org/10.64898/2026.06.23.733339},
137
+ elocation-id = {2026.06.23.733339}
138
+ }
139
+ ```
140
+
141
+ ## License
142
+
143
+ DextraDemixer is distributed under the [BSD 3-Clause License](https://github.com/SchubertLab/DextraDemixer/blob/main/LICENSE).
@@ -0,0 +1,4 @@
1
+ from dextrademixer.model import *
2
+ from dextrademixer.model import __all__ as _model_all
3
+
4
+ __all__ = list(_model_all)
@@ -0,0 +1,207 @@
1
+ """
2
+ The interface shared by all pMHC deconvolution methods.
3
+
4
+ `ApMHCDeconvolution` fixes the call order every method follows and holds the parts that do not
5
+ depend on the model: resolving the supported input containers into counts and annotation
6
+ (`as_counts`), validating them (`_check_parameters`), and turning posterior probabilities into class
7
+ assignments by threshold or local-FDR control (`_predict_posterior_class`). DextraDemixer and the
8
+ comparison baselines BEAM, ICON and ITRAP all build on it.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import abc
13
+
14
+ from typing import TYPE_CHECKING, Tuple, Union
15
+
16
+ import anndata as ad
17
+ import mudata as md
18
+ import pandas as pd
19
+
20
+ from jax import lax
21
+ import jax.numpy as jnp
22
+
23
+ if TYPE_CHECKING:
24
+ from jax._src.typing import Array
25
+
26
+ Data = Union[md.MuData, ad.AnnData, pd.DataFrame]
27
+
28
+
29
+ class ApMHCDeconvolution:
30
+ """
31
+ Common interface of the pMHC deconvolution methods, i.e. DextraDemixer and the comparison
32
+ baselines. It fixes the call order `preprocess_model_data` -> `fit` -> `predict_posterior_class`
33
+ and provides the model-agnostic parts of it.
34
+ """
35
+
36
+ @abc.abstractmethod
37
+ def preprocess_model_data(self,
38
+ data: Data,
39
+ pmhc_key: str,
40
+ pmhc_modality_key: str = "gex",
41
+ neg_ctrl_key: str = None,
42
+ ir_modality_key: str = "airr",
43
+ ir_clone_key: str = None,
44
+ **kwargs):
45
+ """
46
+ Extracts the counts and the annotation a method needs from `data`, see `as_counts`.
47
+
48
+ Args:
49
+ data: the pMHC counts, as a MuData, an AnnData or a cells x features DataFrame.
50
+ pmhc_key: the pMHC count column to deconvolve.
51
+ pmhc_modality_key: the MuData modality holding the counts.
52
+ neg_ctrl_key: (Optional) the negative control count column.
53
+ ir_modality_key: the MuData AIRR module key.
54
+ ir_clone_key: (Optional) the `obs` column holding clonotype ids.
55
+ kwargs: method-specific extras.
56
+ """
57
+
58
+ @abc.abstractmethod
59
+ def fit(self, *args, **kwargs):
60
+ """
61
+ Fits the method on the data prepared by `preprocess_model_data`.
62
+
63
+ Returns:
64
+ self, so that `predict_posterior_class` can be chained onto the call.
65
+ """
66
+
67
+ @abc.abstractmethod
68
+ def predict_posterior_class(self,
69
+ threshold: float = None,
70
+ target_fdr: float = None
71
+ ) -> Tuple[Array, Array]:
72
+ """
73
+ Assigns each cell to the binding or non-binding class.
74
+
75
+ Args:
76
+ threshold: (Optional) probability in [0,1] above which a cell is called a binder.
77
+ target_fdr: (Optional) FDR to control instead of using a fixed threshold. Mutually
78
+ exclusive with `threshold`.
79
+
80
+ Returns:
81
+ A tuple (p_pred, assignment) of per-cell binding probabilities and 0/1 assignments.
82
+ """
83
+
84
+ def predict(self, *args, **kwargs):
85
+ """Alias for `predict_posterior_class`, for the familiar scikit-learn call name."""
86
+ return self.predict_posterior_class(*args, **kwargs)
87
+
88
+ @staticmethod
89
+ def _predict_posterior_class(p_pred: Array,
90
+ threshold: float = None,
91
+ target_fdr: float = None
92
+ ) -> Array:
93
+ """
94
+ Turns per-cell binding probabilities into 0/1 assignments, either at a fixed threshold or
95
+ at the largest threshold whose estimated FDR stays below `target_fdr`.
96
+
97
+ Args:
98
+ p_pred: per-cell posterior probability of binding, shape (n_cells,).
99
+ threshold: (Optional) probability in [0,1] above which a cell is called a binder.
100
+ target_fdr: (Optional) FDR to control instead. Mutually exclusive with `threshold`;
101
+ if neither is given, a threshold of 0.5 is used.
102
+
103
+ Returns:
104
+ The 0/1 class assignment, shape (n_cells,).
105
+
106
+ Raises:
107
+ ValueError: if both `threshold` and `target_fdr` are given, or either is outside [0,1].
108
+ """
109
+ if threshold is not None and target_fdr is not None:
110
+ raise ValueError("Please specify either a manual `threshold` or a `target_fdr` but not both.")
111
+
112
+ if threshold is not None and not (0 <= threshold <= 1):
113
+ raise ValueError(f"`threshold`must be in [0,1] but was {threshold}")
114
+
115
+ if target_fdr is not None and not (0 <= target_fdr <= 1):
116
+ raise ValueError(f"`target_fdr`must be in [0,1] but was {target_fdr}")
117
+
118
+ if threshold is None and target_fdr is None:
119
+ threshold = 0.5
120
+
121
+ # posterior probability of belonging to the binding class
122
+ if target_fdr is not None:
123
+ n_cells = p_pred.shape[0]
124
+
125
+ # Calculate the local FDR (1 - p)
126
+ lfdr = 1 - p_pred
127
+
128
+ sorted_indices = jnp.argsort(p_pred)[::-1]
129
+ sorted_p = p_pred[sorted_indices]
130
+ sorted_lfdr = lfdr[sorted_indices]
131
+
132
+ cumulative_lfdr = jnp.cumsum(sorted_lfdr)
133
+ cumulative_count = jnp.arange(1, n_cells + 1)
134
+
135
+ # Estimated FDR for each possible threshold
136
+ estimated_fdr = cumulative_lfdr / cumulative_count
137
+
138
+ # Find the largest index k such that estimated_fdr[k] <= target_fdr
139
+ valid_thresholds = estimated_fdr <= target_fdr
140
+ max_k = jnp.max(jnp.where(valid_thresholds, jnp.arange(n_cells), -1))
141
+ threshold = lax.cond(max_k >= 0, lambda: sorted_p[max_k], lambda: 1.0)
142
+
143
+ assignment = (p_pred >= threshold).astype("int32")
144
+ return assignment
145
+
146
+
147
+ @staticmethod
148
+ def as_counts(data: Data, pmhc_modality_key: str = "gex", ir_modality_key: str = "airr") -> Tuple[pd.DataFrame, pd.DataFrame]:
149
+ """
150
+ Normalizes any supported input into a (counts, obs) pair of DataFrames, so that count
151
+ columns are addressed by `counts[key]` and per-cell annotation by `obs[key]`:
152
+
153
+ - MuData: counts from the `pmhc_modality_key` modality, obs from the `ir_modality_key` modality
154
+ - AnnData: counts from `X`, obs from `obs`
155
+ - DataFrame (cells x features): obs is the frame itself, so clonotypes can be a column
156
+
157
+ Args:
158
+ data: a MuData, an AnnData or a cells x features DataFrame.
159
+ pmhc_modality_key: the MuData modality holding the counts, unused for the other types.
160
+ ir_modality_key: the MuData modality holding the annotation, unused for the other types.
161
+
162
+ Returns:
163
+ A tuple (counts, obs) of DataFrames sharing the cell order of `data`.
164
+
165
+ Raises:
166
+ TypeError: if `data` is of an unsupported type.
167
+ """
168
+ if isinstance(data, md.MuData):
169
+ counts, _ = ApMHCDeconvolution.as_counts(data.mod[pmhc_modality_key])
170
+ return counts, data.mod[ir_modality_key].obs if ir_modality_key in data.mod else data.obs
171
+ if isinstance(data, ad.AnnData):
172
+ return data.to_df(), data.obs
173
+ if isinstance(data, pd.DataFrame):
174
+ return data, data
175
+ raise TypeError(f"unsupported input type {type(data).__name__}, expected a MuData, an "
176
+ f"AnnData or a cells x features DataFrame")
177
+
178
+ @staticmethod
179
+ def _check_parameters(x, x_neg, clone_id):
180
+ """
181
+ Checks consistency of the input data before initializing the model.
182
+
183
+ Args:
184
+ x: pMHC UMI counts, shape (n_cells,).
185
+ x_neg: (Optional) negative control counts, expected shape (n_cells,).
186
+ clone_id: (Optional) clonotype ids, expected shape (n_cells,).
187
+
188
+ Raises:
189
+ ValueError: if `x` contains NaNs or if `x_neg`/`clone_id` do not match its length.
190
+ """
191
+ n_cells = x.shape[0]
192
+
193
+ if jnp.isnan(x).any():
194
+ raise ValueError("Input data `x` contains NaN values. Please remove them before fitting the model.")
195
+
196
+ if clone_id is not None:
197
+ if clone_id.shape[0] != n_cells:
198
+ raise ValueError(f"`clone_id` and count data `x` require the same size but got "
199
+ f"{clone_id.shape[0]} and {n_cells}")
200
+
201
+ if x_neg is not None:
202
+ n_cells_neg = x_neg.shape[0]
203
+
204
+ if n_cells_neg != n_cells:
205
+ raise ValueError(f"`x_neg` must have the same size as `x` but got "
206
+ f"{n_cells_neg} vs {n_cells}.")
207
+