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.
- dextrademixer-0.0.1/LICENSE +29 -0
- dextrademixer-0.0.1/PKG-INFO +183 -0
- dextrademixer-0.0.1/README.md +143 -0
- dextrademixer-0.0.1/dextrademixer/__init__.py +4 -0
- dextrademixer-0.0.1/dextrademixer/model/ApMHCDeconvolution.py +207 -0
- dextrademixer-0.0.1/dextrademixer/model/BEAM.py +193 -0
- dextrademixer-0.0.1/dextrademixer/model/DextraDemixer.py +1050 -0
- dextrademixer-0.0.1/dextrademixer/model/DextraDemixerMulti.py +233 -0
- dextrademixer-0.0.1/dextrademixer/model/ICON.py +227 -0
- dextrademixer-0.0.1/dextrademixer/model/ITRAP.py +364 -0
- dextrademixer-0.0.1/dextrademixer/model/__init__.py +15 -0
- dextrademixer-0.0.1/dextrademixer/utils/__init__.py +3 -0
- dextrademixer-0.0.1/dextrademixer/utils/registry.py +11 -0
- dextrademixer-0.0.1/dextrademixer/utils/simulation.py +408 -0
- dextrademixer-0.0.1/dextrademixer/utils/utils.py +469 -0
- dextrademixer-0.0.1/dextrademixer.egg-info/PKG-INFO +183 -0
- dextrademixer-0.0.1/dextrademixer.egg-info/SOURCES.txt +20 -0
- dextrademixer-0.0.1/dextrademixer.egg-info/dependency_links.txt +1 -0
- dextrademixer-0.0.1/dextrademixer.egg-info/requires.txt +20 -0
- dextrademixer-0.0.1/dextrademixer.egg-info/top_level.txt +1 -0
- dextrademixer-0.0.1/pyproject.toml +71 -0
- dextrademixer-0.0.1/setup.cfg +4 -0
|
@@ -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
|
+
[](https://doi.org/10.64898/2026.06.23.733339)
|
|
44
|
+
[](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
|
+
[](https://doi.org/10.64898/2026.06.23.733339)
|
|
4
|
+
[](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,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
|
+
|