mirpy-lib 3.2.0__tar.gz → 3.4.0__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.
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/.gitignore +3 -3
- mirpy_lib-3.4.0/CHANGELOG.md +147 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/CLAUDE.md +43 -8
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/PKG-INFO +58 -16
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/README.md +53 -12
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/SOURCES.md +23 -1
- mirpy_lib-3.4.0/docs/_static/custom.css +9 -0
- mirpy_lib-3.4.0/docs/_static/depth_robustness.png +0 -0
- mirpy_lib-3.4.0/docs/_static/mirpy_dark.png +0 -0
- mirpy_lib-3.4.0/docs/_static/mirpy_dark.svg +1 -0
- mirpy_lib-3.4.0/docs/_static/mirpy_light.png +0 -0
- mirpy_lib-3.4.0/docs/_static/mirpy_light.svg +1 -0
- mirpy_lib-3.4.0/docs/_static/sample_embedding_diagram.png +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/docs/api.rst +44 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/docs/conf.py +5 -3
- mirpy_lib-3.4.0/docs/examples.rst +31 -0
- mirpy_lib-3.4.0/docs/index.rst +110 -0
- mirpy_lib-3.4.0/docs/usage.rst +269 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/pyproject.toml +19 -11
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/requirements.txt +1 -1
- mirpy_lib-3.4.0/setup.sh +88 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/__init__.py +1 -1
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/bench/__init__.py +3 -1
- mirpy_lib-3.4.0/src/mir/bench/eval.py +219 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/bench/metrics.py +11 -0
- mirpy_lib-3.4.0/src/mir/cli.py +207 -0
- mirpy_lib-3.4.0/src/mir/cohort.py +431 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/density.py +112 -14
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/embedding/prototypes.py +5 -4
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/embedding/tcremp.py +1 -2
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/ml/bundle.py +2 -1
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/repertoire.py +205 -24
- mirpy_lib-3.2.0/BENCHMARKS.md +0 -264
- mirpy_lib-3.2.0/CHANGELOG.md +0 -68
- mirpy_lib-3.2.0/REPERTOIRE_EMBEDDING.md +0 -186
- mirpy_lib-3.2.0/REPERTOIRE_LESSONS.md +0 -78
- mirpy_lib-3.2.0/SQRT_D_MIGRATION.md +0 -157
- mirpy_lib-3.2.0/THEORY.md +0 -322
- mirpy_lib-3.2.0/docs/index.rst +0 -68
- mirpy_lib-3.2.0/docs/usage.rst +0 -154
- mirpy_lib-3.2.0/environment.yml +0 -15
- mirpy_lib-3.2.0/setup.sh +0 -90
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/LICENSE +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/assets/mirpy_dark.png +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/assets/mirpy_dark.svg +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/assets/mirpy_light.png +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/assets/mirpy_light.svg +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/docs/Makefile +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0}/docs/requirements.txt +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/aliases.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/alleles.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/bench/theory.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/bench/vdjdb.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/distances/__init__.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/distances/germline.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/distances/junction.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/embedding/__init__.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/embedding/pca.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/embedding/presets.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/explain.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/ml/__init__.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/ml/codec.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/ml/decoder.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/ml/encoder.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/ml/set_encoder.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/ml/tokenize.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/ml/train.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/py.typed +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/__init__.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/gene_library/README.md +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/gene_library/__init__.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/gene_library/build_gene_library.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/gene_library/build_region_annotations.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/gene_library/imgt_gene_library.txt +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/gene_library/olga_gene_library.txt +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/gene_library/region_annotations.txt +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/build_germline_dist.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/human_IGH.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/human_IGK.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/human_IGL.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/human_TRA.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/human_TRB.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/human_TRD.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/human_TRG.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/mouse_IGH.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/mouse_IGK.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/mouse_IGL.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/mouse_TRA.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/mouse_TRB.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/mouse_TRD.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/germline_dist/mouse_TRG.npz +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/prototypes/.gitkeep +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/prototypes/generate_prototypes.py +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/prototypes/human_IGH.tsv +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/prototypes/human_IGK.tsv +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/prototypes/human_IGL.tsv +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/prototypes/human_TRA.tsv +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/prototypes/human_TRB.tsv +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/prototypes/human_TRD.tsv +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/prototypes/human_TRG.tsv +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/prototypes/manifest.json +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/prototypes/mouse_TRA.tsv +0 -0
- {mirpy_lib-3.2.0 → mirpy_lib-3.4.0/src}/mir/resources/prototypes/mouse_TRB.tsv +0 -0
|
@@ -10,7 +10,7 @@ tests/assets/vdjdb.slim.txt.gz
|
|
|
10
10
|
tests/assets/real_repertoires/
|
|
11
11
|
tests/assets/srx_repertoires/
|
|
12
12
|
*.airr.tsv
|
|
13
|
-
|
|
13
|
+
examples/assets/
|
|
14
14
|
|
|
15
15
|
## VSCode
|
|
16
16
|
|
|
@@ -184,10 +184,10 @@ cython_debug/
|
|
|
184
184
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
185
185
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
186
186
|
#.idea/
|
|
187
|
-
mir/docs
|
|
187
|
+
src/mir/docs
|
|
188
188
|
.idea
|
|
189
189
|
public
|
|
190
|
-
|
|
190
|
+
examples/assets/derived/
|
|
191
191
|
|
|
192
192
|
# ML run artifacts (checkpoints/metrics) — never committed
|
|
193
193
|
experiments/runs/
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `mirpy-lib` (import `mir`). This project follows semantic versioning; the v3 line is a
|
|
4
|
+
greenfield ML/embedding rewrite (the classical v1.x/v2 toolkit is frozen on branch `legacy-v2`).
|
|
5
|
+
|
|
6
|
+
## 3.4.0 — 2026-07-18
|
|
7
|
+
|
|
8
|
+
Minor: a command-line interface, a uv-based dev setup, and a documentation overhaul. No public
|
|
9
|
+
Python API removed; one optional-dependency group split out.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`mir` command-line interface** (`[project.scripts]`, also `python -m mir.cli`) — the two
|
|
14
|
+
embedding scales without writing Python:
|
|
15
|
+
- `mir embed clonotypes SAMPLE` → a per-clonotype TCREMP embedding table (`e0…`).
|
|
16
|
+
- `mir embed repertoires SAMPLE…` → one repertoire vector `Φ(S)` per sample **per chain** on one
|
|
17
|
+
shared basis (`phi0…`), with optional `--mmd` pairwise-distance output.
|
|
18
|
+
|
|
19
|
+
Inputs are any format `vdjtools.io` reads (AIRR/vdjtools/MiXCR/immunoSEQ/parquet); output is TSV
|
|
20
|
+
or Parquet. See `mir embed <cmd> -h`. Tests in `tests/test_cli.py`.
|
|
21
|
+
- **`mir.repertoire.correct_batch`** — Harmony-like cluster-aware batch correction on a stacked
|
|
22
|
+
sample×feature Φ matrix. Removes the batch offset *per soft cluster* (batch-diversity-penalised),
|
|
23
|
+
so a batch confounded with a biological cluster is corrected without erasing that biology; reduces
|
|
24
|
+
exactly to `mir.cohort.residualize` at `n_clusters=1` / `theta=0` (`prop:batch`).
|
|
25
|
+
- **`[ann]` optional-dependency group** for the approximate-NN density backend (`pynndescent`).
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Development now uses a repo-local `.venv` via [uv](https://docs.astral.sh/uv/)** instead of
|
|
30
|
+
conda. `setup.sh` is rewritten (bash/zsh portable; `--dev-parents`, `--docs`, `--tests`); the
|
|
31
|
+
conda `environment.yml` is removed. Runtime is unchanged — still a pure-Python `py3-none-any` wheel.
|
|
32
|
+
- **`pynndescent` moved from `[bench]` to the new `[ann]` extra.** `[bench]` is now all pure-wheel
|
|
33
|
+
(no numba/llvmlite), so `pip install "mirpy-lib[bench]"` resolves cleanly on any Python. Users of
|
|
34
|
+
`density.neighbor_enrichment(backend="ann")` should install `"mirpy-lib[ann]"`.
|
|
35
|
+
- **`vdjtools>=3.0.0`** (was `>=2.3.0`).
|
|
36
|
+
- Documentation overhaul: a use-case-driven user guide, the two CLI commands documented, an
|
|
37
|
+
examples/notebooks page, `mir.cohort` and `mir.bench.eval` added to the API reference, a logo, and
|
|
38
|
+
the sample-embedding schematic + real depth-robustness figure. Zero-warning Sphinx build.
|
|
39
|
+
- **Repo layout** (no effect on the installed package): adopted the **src-layout** (`mir/` →
|
|
40
|
+
`src/mir/`); renamed `notebooks/` → `examples/`; and moved the working result/plan markdown out of
|
|
41
|
+
the repo root — `THEORY.md` to the manuscript repo, `BENCHMARKS.md` / `REPERTOIRE_{EMBEDDING,LESSONS}.md`
|
|
42
|
+
/ `SQRT_D_MIGRATION.md` / `ROADMAP.md` to `2026-mirpy-analysis/benchmarks/`. Root keeps
|
|
43
|
+
README / CHANGELOG / CLAUDE / SOURCES.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- `DEFAULT_GAP_POSITIONS = (3, 4, -4, -3)` had three independent definitions
|
|
48
|
+
(`distances/junction`, `embedding/tcremp`, `ml/bundle`); now defined once in `distances.junction`
|
|
49
|
+
and imported, so the coordinate constant cannot drift.
|
|
50
|
+
- `cohort.cluster_samples` docstring described itself; `AntigenMetric` and `mir.bench.eval` gained
|
|
51
|
+
the docstrings/module-map entries they were missing.
|
|
52
|
+
|
|
53
|
+
## 3.3.0 — 2026-07-17
|
|
54
|
+
|
|
55
|
+
Minor: one new public parameter, nothing removed or changed.
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **`fit_density_space(chunk_size=)`** — embed and project in batches so the full raw matrix is never
|
|
60
|
+
resident. Peak memory becomes `max(pca_fit_cap, chunk_size) × n_features` instead of
|
|
61
|
+
`len(df) × n_features`: measured **10.60 GB → 1.81 GB** at 450k pooled clonotypes, and flat in `N`
|
|
62
|
+
(vs linear), at no wall-clock cost. This is what makes whole-cohort density arms runnable on a
|
|
63
|
+
laptop — the 4.2M-clonotype pooled arm is ~51 GB raw and ~102 GB once `scaler.transform` upcasts to
|
|
64
|
+
float64. Chunking is bit-exact at the embedding level (`_embed` of a slice == the slice of
|
|
65
|
+
`_embed`); the projected coordinates agree to float noise (~1e-7 relative), since BLAS summation
|
|
66
|
+
order depends on batch shape.
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- `fit_density_space`'s `pca_fit_cap` docstring claimed it "lets whole repertoires be embedded without
|
|
71
|
+
a full-matrix PCA". It caps the **fit**, not the memory — both raw matrices were already
|
|
72
|
+
materialized before the PCA was fitted. Documented, and `chunk_size=` is the actual remedy.
|
|
73
|
+
- **`mir.__version__` was stale** — it read `3.1.1` on the published 3.2.0, because the release bump
|
|
74
|
+
moved `pyproject.toml` but not `mir/__init__.py`, and `publish.yml` only validates *pyproject ==
|
|
75
|
+
tag*. Both now read 3.3.0. (`__version__` is still hand-maintained; deriving it from
|
|
76
|
+
`importlib.metadata` would retire this failure mode for good.)
|
|
77
|
+
- `tests/assets/olga_humanTRB_1000.txt.gz` was a slice of the alphabetically sorted VDJdb TRB dump,
|
|
78
|
+
not OLGA output as its name and `SOURCES.md` claimed — so it was **not** an antigen-naive null (12%
|
|
79
|
+
of rows had a Hamming-1 neighbour vs 0.2% for real OLGA). Regenerated from `olga-generate_sequences`;
|
|
80
|
+
provenance and a byte-reproducible regenerate command recorded in `SOURCES.md`. No test was
|
|
81
|
+
invalidated (they use it only as a generic TRB junction pool), but external calibrations that treated
|
|
82
|
+
it as a synthetic negative control were comparing VDJdb against itself. Not shipped (tests are
|
|
83
|
+
excluded from the sdist); listed here because it invalidates results, not code.
|
|
84
|
+
|
|
85
|
+
## 3.2.0 — 2026-07-17
|
|
86
|
+
|
|
87
|
+
Minor: one new public module, nothing removed or changed.
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
|
|
91
|
+
- **`mir.explain`** (T7) — explainable readouts over any repertoire feature matrix.
|
|
92
|
+
`ChannelSpec` / `ChannelBuilder` / `stack_embeddings` attach the name→column map that `Φ.vector`
|
|
93
|
+
does not carry (`stack_embeddings` is exact: `X[i] == embs[i].vector`, names only, no transform);
|
|
94
|
+
`channel_report` ablates each named channel under a caller-supplied scorer (leave-one-in by
|
|
95
|
+
default; `mode="both"` adds the conditional half that exposes *redundant* channels — high `delta`,
|
|
96
|
+
`delta_out≈0`; optional row-permutation p-values); `channel_drivers` hops from a winning
|
|
97
|
+
**kernel-mean** channel to the clonotypes driving it via `class_witness`, and refuses channels with
|
|
98
|
+
no clonotype pre-image (a Hill number's "drivers" are a category error, not an open question).
|
|
99
|
+
Scorer-agnostic by design — the library never sees the labels and ships no scorers, so a Cox
|
|
100
|
+
C-index and a CV AUC both plug in. **No existing module changed.**
|
|
101
|
+
|
|
102
|
+
## 3.1.1 — 2026-07-14
|
|
103
|
+
|
|
104
|
+
Maintenance re-release of 3.1.0 with no functional changes — 3.1.0 was withdrawn from PyPI, and
|
|
105
|
+
this version restores the package under a fresh, clean version. The API, coordinates, and behaviour
|
|
106
|
+
are identical to 3.1.0 (see below).
|
|
107
|
+
|
|
108
|
+
## 3.1.0 — 2026-07-14
|
|
109
|
+
|
|
110
|
+
The Part-2 feature tier on top of the 3.0 consolidated embedding core: neural codecs, continuous-density
|
|
111
|
+
methods, and the sample-level (repertoire) embedding, all on arda-native coordinates.
|
|
112
|
+
|
|
113
|
+
### Added
|
|
114
|
+
- **`mir.density`** (T6) — graph-free continuous-density TCRNET/ALICE: balloon adaptive-radius enrichment
|
|
115
|
+
(Poisson/binomial + BH q, water-level calibration), abundance-aware weighted mass, backends
|
|
116
|
+
`exact`(BallTree) / `kdtree`(multicore) / `ann`(pynndescent).
|
|
117
|
+
- **`mir.repertoire`** (T7) — sample-level embedding `Φ(S)` = RFF kernel mean ‖ coverage-standardized Hill ‖
|
|
118
|
+
second-moment Fisher; `mmd_distance`/`mmd_matrix` (now with **`unbiased=True`** diagonal-removed MMD²),
|
|
119
|
+
`hla_stratified_mmd`, `class_witness` motif finder.
|
|
120
|
+
- **`mir.ml`** (Part 2, `[ml]` extra) — forward/inverse/pgen/unified neural codecs + `CodecBundle`
|
|
121
|
+
(prototype-hash-verified shipping); learned repertoire track `set_encoder` (Set-Transformer/DeepRC).
|
|
122
|
+
- **Bench** — clustering `method=` (dbscan/hdbscan/optics); `bench.theory` T6 `tcrnet_convergence`,
|
|
123
|
+
`codec_losslessness`.
|
|
124
|
+
- **Repertoire benchmarks** — `experiments/benchmark_repertoire_{aging,depth,cmvhla,hla,yfv,spikein,
|
|
125
|
+
agediverge}.py` and the COVID cohort suite `{covidbatch,covidhla,covidstatus,covidpaired}.py`
|
|
126
|
+
(`airr_covid19`, local-first + HF fallback). Recorded baselines in `BENCHMARKS.md`.
|
|
127
|
+
|
|
128
|
+
### Changed
|
|
129
|
+
- **Coordinate system re-pinned to arda-native** germline + real-repertoire prototypes (versioned; any model
|
|
130
|
+
trained on the old coords must be retrained).
|
|
131
|
+
- **`mir.ml` device selection** now **CUDA → MPS → CPU** (was MPS-only) with a `MIR_DEVICE` env override and
|
|
132
|
+
CUDA seeding — GPU support beyond Apple silicon.
|
|
133
|
+
- Documented all parallelism knobs (README "Performance & parallelism"): `TCREmp(threads=0)` all-core,
|
|
134
|
+
density `backend="kdtree"` multicore-exact, `cluster(n_jobs=…)`, BLAS env.
|
|
135
|
+
|
|
136
|
+
### Fixed
|
|
137
|
+
- **Unbiased repertoire MMD** — the biased V-statistic's `1/n_eff` self-term inflated low-diversity samples and
|
|
138
|
+
faked a divergence signal; `unbiased=True` removes it. Aging-divergence re-evaluated at depth: real but
|
|
139
|
+
diversity-coupled, not an independent axis.
|
|
140
|
+
|
|
141
|
+
### Notes
|
|
142
|
+
- New lessons for the theory appendix in `REPERTOIRE_LESSONS.md`; full findings in `THEORY.md` T7.
|
|
143
|
+
- Pure-Python `py3-none-any` wheel; native code comes from `seqtree`/`vdjtools` wheels.
|
|
144
|
+
|
|
145
|
+
## 3.0.0 — greenfield v3 embedding core (unreleased)
|
|
146
|
+
- Prototype (TCREMP) embedding on `seqtree.gapblock` + baked arda germline distances; vdjtools reuse (no AIRR
|
|
147
|
+
data-model layer of its own); bench harness (VDJdb Table S1); theory scaffold (`THEORY.md` S1–S3, T1–T5).
|
|
@@ -8,9 +8,12 @@ frozen on branch **`legacy-v2`** (`mirpy-lib` 2.x) — do not develop there.
|
|
|
8
8
|
|
|
9
9
|
## Repo split (2026-07-16) — three homes
|
|
10
10
|
This repo is now **the library + its CI/test data only**. Analysis and manuscript live elsewhere:
|
|
11
|
-
- **`~/vcs/code/mirpy`** (here):
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
- **`~/vcs/code/mirpy`** (here): **src-layout** — the library is `src/mir/` (import `mir`); unit +
|
|
12
|
+
fast/slow CI tests (`tests/`, on slim bundled data), runnable `examples/` (marimo), docs. As of the
|
|
13
|
+
3.4.x cleanup the result/plan markdown moved out: **`THEORY.md` → the manuscript repo**;
|
|
14
|
+
**`BENCHMARKS.md`, `REPERTOIRE_{EMBEDDING,LESSONS}.md`, `SQRT_D_MIGRATION.md`, `ROADMAP.md` →
|
|
15
|
+
`2026-mirpy-analysis/benchmarks/`**. Only `README`/`CHANGELOG`/`CLAUDE`/`SOURCES` stay at the root.
|
|
16
|
+
Benchmark *scripts* and the theory *appendix* had already moved out (below).
|
|
14
17
|
- **`~/vcs/projects/2026-mirpy-analysis`**: all **benchmark scripts** (`benchmarks/` — local + aldan3),
|
|
15
18
|
full result docs (BENCHMARKS/THEORY/…), figures, run outputs, dataset catalog. Run the mirpy library
|
|
16
19
|
from here; refresh numbers-of-record here.
|
|
@@ -53,7 +56,7 @@ negative S2) — so prototypes use real reads, not model generation.
|
|
|
53
56
|
mirpy is normally read-only to the sibling repos; the `from_arda` builder + a tandem-D generation
|
|
54
57
|
fix were added to `vdjtools` under the owner's direction (this is that owner's ecosystem).
|
|
55
58
|
|
|
56
|
-
## Layout (`mir/`)
|
|
59
|
+
## Layout (`src/mir/`)
|
|
57
60
|
- `aliases.py`, `alleles.py` — species/locus + allele normalization.
|
|
58
61
|
- `distances/junction.py` — `junction_distance_matrix` (seqtree.gapblock). Coordinate knobs (all
|
|
59
62
|
default to the published space, threaded through `TCREmp`): `metric="squared"` (=`d`, default) |
|
|
@@ -88,6 +91,20 @@ fix were added to `vdjtools` under the owner's direction (this is that owner's e
|
|
|
88
91
|
Opt-in `fit_repertoire_space(n_eigs=r)` swaps the second-moment block's full `D₂(D₂+1)/2` upper-triangle
|
|
89
92
|
for its top-`r` eigenvalues (rotation-invariant spectrum; default `None` = upper-tri, unchanged — but
|
|
90
93
|
lossy for the *directional* HLA imprint, so the full triangle stays the recommended block; `benchmark_repertoire_spectral.py`).
|
|
94
|
+
Also `fit_repertoire_spaces` (one basis **per locus** — the multi-chain fit), `centroid_atypicality`
|
|
95
|
+
(per-sample cosine distance to a group centroid — a Φ-geometry op feeding the digital-donor atypicality channel),
|
|
96
|
+
and `correct_batch` (Harmony-like cluster-aware batch correction on a stacked Φ matrix; reduces to
|
|
97
|
+
`cohort.residualize` at `n_clusters=1`/`theta=0` — `prop:batch`; test in `test_repertoire.py`).
|
|
98
|
+
- `cohort.py` — **the digital donor** (T.7): `fit_donor_embeddings`/`DonorCohort` fuse per-chain identity
|
|
99
|
+
(kernel-mean, cross-sample PCA-reduced) ‖ diversity ‖ coverage across loci through **one `ChannelBuilder`**,
|
|
100
|
+
with an `extra_channels` hook for the analysis's own tissue/clinical blocks; comparability bites twice
|
|
101
|
+
(per-locus `prototype_hash` **and** the stored identity PCA) so `save`/`load` verify every hash and
|
|
102
|
+
`transform` is the only held-out path. Plus `residualize` (batch cookbook), `cluster_samples`
|
|
103
|
+
(MMD→precomputed-metric cluster), `incidence_biomarkers` (subject-incidence Fisher, delegating to
|
|
104
|
+
`vdjtools.biomarker.fisher`). Generalizes the analysis-repo `_tcga_embedding.build_embedding` glue.
|
|
105
|
+
- `bench/eval.py` — the scorers `channel_report` consumes (kept out of `explain.py` so it stays scorer-free):
|
|
106
|
+
`cv_auc` / `held_out_auc` (classification), `cv_cindex` / `km_logrank` (Cox survival, `[bench]`→lifelines),
|
|
107
|
+
`kmer_matrix` (baseline).
|
|
91
108
|
- `explain.py` — **explainable readouts over any feature matrix** (T7). `ChannelSpec`/`ChannelBuilder`/
|
|
92
109
|
`stack_embeddings` attach the name→column map `Φ.vector` does not carry (per-chain blocks merge by
|
|
93
110
|
name); `channel_report(X, spec, scorer, base=, mode="in"|"out"|"both", n_permutations=)` ablates each
|
|
@@ -99,14 +116,22 @@ fix were added to `vdjtools` under the owner's direction (this is that owner's e
|
|
|
99
116
|
has no clonotype pre-image and it raises. Depends one-way on `repertoire.py`; nothing there changed.
|
|
100
117
|
- `ml/` — Part 2 (torch), neural codecs + `set_encoder.py` (learned repertoire track: Set-Transformer/DeepRC
|
|
101
118
|
attention pooling + `SetEncoderBundle`).
|
|
119
|
+
- `cli.py` — the `mir` console script (argparse, stdlib): `embed clonotypes` (TCREmp table) /
|
|
120
|
+
`embed repertoires` (per-locus `fit_repertoire_space`→`sample_embedding` Φ(S), optional `--mmd`).
|
|
121
|
+
Reads via `vdjtools.io.read`; writes TSV/parquet.
|
|
102
122
|
- `resources/` — `prototypes/` (TSVs + manifest), `gene_library/` (region_annotations.txt),
|
|
103
123
|
`germline_dist/` (baked `.npz`, from `build_germline_dist.py`).
|
|
104
124
|
|
|
105
125
|
## Build / test / run
|
|
106
|
-
-
|
|
107
|
-
(
|
|
108
|
-
|
|
109
|
-
|
|
126
|
+
- **Repo-local `.venv` via uv** (Python 3.12; conda retired 2026-07-18). `bash setup.sh`
|
|
127
|
+
(bash/zsh; `--dev-parents` editable-installs `../seqtree ../vdjtools ../vdjmatch`, `--docs`,
|
|
128
|
+
`--tests`), or `uv pip install -e ".[dev,bench]"`. Pure-Python hatchling; no C build for `mir`
|
|
129
|
+
itself. Extras: `[bench] [ann] [annotate] [build] [ml] [docs] [dev] [examples]` (`[ann]` =
|
|
130
|
+
pynndescent, split out of `[bench]` so `[bench]` stays numba-free / clean to resolve).
|
|
131
|
+
- CLI: `mir embed clonotypes SAMPLE` (→ clonotype embedding table) / `mir embed repertoires
|
|
132
|
+
SAMPLE… ` (→ per-sample-per-chain Φ(S)); `mir/cli.py`, `tests/test_cli.py`.
|
|
133
|
+
- Tests: `python -m pytest tests/ -q` (`-m "not integration and not benchmark"` for the fast tier;
|
|
134
|
+
torch/pynndescent tests skip unless `[ml]`/`[ann]` installed). All self-contained on bundled resources.
|
|
110
135
|
- Experiments: `python benchmarks/reproduce_supplementary.py` (theory S1–S3),
|
|
111
136
|
`python benchmarks/benchmark_vdjdb.py` (Table S1). Analyses: `analyze_prototype_counts.py`
|
|
112
137
|
(geometry saturates by K≈100 — T.1/S4), `analyze_pc_decomposition.py` (V/J η² ≈0.44/0.49,
|
|
@@ -126,6 +151,16 @@ fix were added to `vdjtools` under the owner's direction (this is that owner's e
|
|
|
126
151
|
GPU only in `mir.ml`: `pick_device()` = **CUDA → MPS → CPU** auto, override `device=`/`MIR_DEVICE`.
|
|
127
152
|
|
|
128
153
|
## Open loops / next steps
|
|
154
|
+
- **Embedding-tier roadmap** (`2026-mirpy-analysis/benchmarks/ROADMAP.md`, moved out in the 3.4.x
|
|
155
|
+
cleanup) — the "vdjtools at the embedding level" audit +
|
|
156
|
+
plan (three verbs: make / measure / generate-decode). **Phase 0** (robustness + optimization quick wins)
|
|
157
|
+
and **Phase 1** (cohort tier: `bench/eval.py`, `repertoire.{fit_repertoire_spaces,centroid_atypicality}`,
|
|
158
|
+
`cohort.py` digital donor) are **DONE**. Next: **Phase 2** generative loop (`generate.py` `DescriptorDensity`
|
|
159
|
+
+ `evolve`/`sample`, `CodecBundle.from_unified/from_decoder`), then multimodal encoders + embedding
|
|
160
|
+
trajectory. **Analysis-repo follow-up:** refactor `_tcga_embedding.build_embedding` onto
|
|
161
|
+
`cohort.fit_donor_embeddings` (+ `extra_channels` for isotype/composition/atypicality) and re-verify the
|
|
162
|
+
pan-cancer ΔC numbers. NB Phase 0 flipped the density default to `backend="kdtree"` — re-verify any
|
|
163
|
+
recorded balloon-mode baselines (±1 boundary counts).
|
|
129
164
|
- **v3.0 remaining**: 10X paired benchmark; docs (Sphinx theory section + notebooks); CI; publish
|
|
130
165
|
`py3-none-any` wheel; regenerate `generate_prototypes.py` via `vdjtools.model.generate`.
|
|
131
166
|
- **Bench tuning**: raw kneedle eps over-merges; `cluster(eps_factor=0.4)` recovers the paper
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mirpy-lib
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.4.0
|
|
4
4
|
Summary: ML-oriented embeddings for immune receptor repertoires (TCR/BCR)
|
|
5
5
|
Project-URL: Homepage, https://github.com/antigenomics/mirpy
|
|
6
6
|
Project-URL: Repository, https://github.com/antigenomics/mirpy
|
|
@@ -25,7 +25,9 @@ Requires-Dist: scikit-learn
|
|
|
25
25
|
Requires-Dist: scipy
|
|
26
26
|
Requires-Dist: seqtree>=0.3.0
|
|
27
27
|
Requires-Dist: tqdm
|
|
28
|
-
Requires-Dist: vdjtools>=
|
|
28
|
+
Requires-Dist: vdjtools>=3.0.0
|
|
29
|
+
Provides-Extra: ann
|
|
30
|
+
Requires-Dist: pynndescent; extra == 'ann'
|
|
29
31
|
Provides-Extra: annotate
|
|
30
32
|
Requires-Dist: vdjmatch; extra == 'annotate'
|
|
31
33
|
Provides-Extra: bench
|
|
@@ -33,11 +35,10 @@ Requires-Dist: huggingface-hub; extra == 'bench'
|
|
|
33
35
|
Requires-Dist: kneed; extra == 'bench'
|
|
34
36
|
Requires-Dist: lifelines; extra == 'bench'
|
|
35
37
|
Requires-Dist: matplotlib; extra == 'bench'
|
|
36
|
-
Requires-Dist: pynndescent; extra == 'bench'
|
|
37
38
|
Requires-Dist: seaborn; extra == 'bench'
|
|
38
39
|
Provides-Extra: build
|
|
39
40
|
Requires-Dist: biopython; extra == 'build'
|
|
40
|
-
Requires-Dist: vdjtools[model]>=
|
|
41
|
+
Requires-Dist: vdjtools[model]>=3.0.0; extra == 'build'
|
|
41
42
|
Provides-Extra: dev
|
|
42
43
|
Requires-Dist: psutil>=5; extra == 'dev'
|
|
43
44
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
@@ -56,6 +57,16 @@ Provides-Extra: ml
|
|
|
56
57
|
Requires-Dist: torch; extra == 'ml'
|
|
57
58
|
Description-Content-Type: text/markdown
|
|
58
59
|
|
|
60
|
+
<p align="center">
|
|
61
|
+
<picture>
|
|
62
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/mirpy_dark.svg">
|
|
63
|
+
<source media="(prefers-color-scheme: light)" srcset="assets/mirpy_light.svg">
|
|
64
|
+
<!-- Absolute PNG fallback: PyPI strips <picture>/<source> and cannot render a relative or
|
|
65
|
+
raw-served SVG, so the logo must be an absolute-URL raster here. GitHub uses the SVG sources. -->
|
|
66
|
+
<img alt="mirpy" src="https://raw.githubusercontent.com/antigenomics/mirpy/master/assets/mirpy_light.png" width="360">
|
|
67
|
+
</picture>
|
|
68
|
+
</p>
|
|
69
|
+
|
|
59
70
|
<h1 align="center">mirpy — ML embeddings for immune repertoires</h1>
|
|
60
71
|
|
|
61
72
|
[](https://pypi.org/project/mirpy-lib/)
|
|
@@ -65,7 +76,7 @@ Description-Content-Type: text/markdown
|
|
|
65
76
|
**mirpy v3** turns T-/B-cell receptor sequences into fixed-length numeric vectors you can
|
|
66
77
|
cluster, visualize, and feed to ML models. It implements **TCREMP** — embedding each receptor
|
|
67
78
|
by its alignment distances to a fixed set of *prototype* sequences — so that Euclidean distance
|
|
68
|
-
in embedding space approximates pairwise alignment distance (
|
|
79
|
+
in embedding space approximates pairwise alignment distance (Theory T1).
|
|
69
80
|
|
|
70
81
|
> v3 is a slim, embedding-focused rewrite. The classical repertoire toolkit (parsing, overlap,
|
|
71
82
|
> diversity, TCRnet, GLIPH, …) lives on the **`legacy-v2`** branch (`mirpy-lib` 2.x) and in the
|
|
@@ -108,6 +119,24 @@ labels = cluster(pca_denoise(X, n_components=50))
|
|
|
108
119
|
Paired chains concatenate per-chain embeddings via `PairedTCREmp`. Input/output are AIRR polars
|
|
109
120
|
frames keyed by `vdjtools.io.schema` column names.
|
|
110
121
|
|
|
122
|
+
## Command line
|
|
123
|
+
|
|
124
|
+
`pip install mirpy-lib` also installs a `mir` command for the two embedding scales — no Python
|
|
125
|
+
needed. Inputs are any format `vdjtools.io` reads (AIRR TSV, vdjtools, MiXCR, immunoSEQ, parquet).
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# one repertoire -> per-clonotype embedding table (e0…), the input to clustering / ML
|
|
129
|
+
mir embed clonotypes sample.tsv --pca 50 -o clonotypes.parquet
|
|
130
|
+
|
|
131
|
+
# a dataset of repertoires -> one fingerprint Φ(S) per sample, per chain (phi0…), on one
|
|
132
|
+
# shared basis so the rows are mutually comparable; --mmd also writes the pairwise MMD matrix
|
|
133
|
+
mir embed repertoires cohort/*.tsv.gz -o phi.tsv --mmd mmd.tsv
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`mir embed clonotypes -h` / `mir embed repertoires -h` list every flag (species, locus,
|
|
137
|
+
prototype count, weight, Φ blocks, …). Sample id defaults to the filename stem; the locus is
|
|
138
|
+
inferred per file (or restrict with `--locus`).
|
|
139
|
+
|
|
111
140
|
## Recommended presets
|
|
112
141
|
|
|
113
142
|
`TCREmp.from_defaults(species, locus)` uses the per-chain preset when `n_prototypes` is
|
|
@@ -145,9 +174,11 @@ Xr = pca_denoise(X, n_components=p.n_components_recon) # codec reconstruction
|
|
|
145
174
|
| `mir.embedding.tcremp` | `TCREmp` / `PairedTCREmp` — the prototype embedding |
|
|
146
175
|
| `mir.embedding.pca` | PCA denoising of embeddings |
|
|
147
176
|
| `mir.distances` | junction distance (`seqtree.gapblock`; `metric`/`matrix`/`alignment` options) + baked germline distances |
|
|
148
|
-
| `mir.bench` | VDJdb loader, clustering (`cluster(method=…)`: DBSCAN/HDBSCAN/OPTICS) + F1/retention, theory experiments (incl. `codec_losslessness`) |
|
|
177
|
+
| `mir.bench` | VDJdb loader, clustering (`cluster(method=…)`: DBSCAN/HDBSCAN/OPTICS) + F1/retention, theory experiments (incl. `codec_losslessness`), cohort scorers (`bench.eval`: `cv_auc`/`cv_cindex`/`km_logrank`) |
|
|
149
178
|
| `mir.density` | continuous-density TCRNET/ALICE — enrichment (+ clonal-abundance channel, `backend=` exact/kdtree/ann) + noise-filtering (Theory T6) |
|
|
150
|
-
| `mir.repertoire` | sample-level (repertoire) embedding — RFF kernel mean ‖ Hill diversity ‖ second moment; MMD / HLA-stratified distance; motif witness (Theory §T.7) |
|
|
179
|
+
| `mir.repertoire` | sample-level (repertoire) embedding — RFF kernel mean ‖ Hill diversity ‖ second moment; MMD / HLA-stratified distance; motif witness; `centroid_atypicality`, multi-locus `fit_repertoire_spaces` (Theory §T.7) |
|
|
180
|
+
| `mir.explain` | named-channel fusion (`ChannelBuilder`) + scorer-agnostic ablation (`channel_report`/`channel_drivers`) — which part of Φ carries the signal (§T.7) |
|
|
181
|
+
| `mir.cohort` | the **digital donor** — multi-chain `fit_donor_embeddings`/`DonorCohort` (+ `transform`/`save`/`load`) + `residualize` / `cluster_samples` / `incidence_biomarkers` (§T.7) |
|
|
151
182
|
| `mir.ml` | neural codecs (forward/inverse/Pgen/unified) + learned repertoire `set_encoder` (Set-Transformer/DeepRC) — Part 2, experimental; `[ml]` extra |
|
|
152
183
|
|
|
153
184
|
## Background subtraction & clustering (`mir.density`)
|
|
@@ -174,11 +205,12 @@ Use a **biological control** as the background when you have one (e.g. pre- vs p
|
|
|
174
205
|
patient vs healthy) — differential enrichment cancels generic public convergence and isolates the
|
|
175
206
|
antigen-specific response. With no control, `generate_background(locus, n)` samples the vdjtools
|
|
176
207
|
P_gen model (the ALICE regime); the "water level" of a naive repertoire is handled by the
|
|
177
|
-
empirical-null calibration.
|
|
208
|
+
empirical-null calibration. The density benchmarks (YFV, ankylosing-spondylitis B27, TCRNET)
|
|
209
|
+
live in the companion [`2026-mirpy-analysis`](https://github.com/antigenomics) repo.
|
|
178
210
|
|
|
179
211
|
At whole-repertoire scale, pass `neighbor_enrichment(..., backend="kdtree")` (exact scipy cKDTree,
|
|
180
212
|
5–9× faster than the default BallTree) or `backend="ann"` (approximate pynndescent, ~30× faster
|
|
181
|
-
past ~10⁵ clones, trading a small conservative undercount
|
|
213
|
+
past ~10⁵ clones, trading a small conservative undercount; `pip install "mirpy-lib[ann]"`).
|
|
182
214
|
|
|
183
215
|
## Sample-level (repertoire) embedding (`mir.repertoire`)
|
|
184
216
|
|
|
@@ -213,16 +245,24 @@ a batch offset is first-order and cancels, while a batch-orthogonal signal (e.g.
|
|
|
213
245
|
empirical rule of thumb — **diversity for how-even, the embedding for which-clones**: clone-size
|
|
214
246
|
phenotypes (age, CMV) are a diversity summary's turf, while clonotype identity (HLA — strongest in
|
|
215
247
|
TRA and class II) lives in the second moment / witness. A learned co-equal set encoder
|
|
216
|
-
(Set-Transformer / DeepRC) is in `mir.ml.set_encoder` (`[ml]` extra).
|
|
217
|
-
|
|
248
|
+
(Set-Transformer / DeepRC) is in `mir.ml.set_encoder` (`[ml]` extra). Recorded results and theory
|
|
249
|
+
(T7) live in the companion [`2026-mirpy-analysis`](https://github.com/antigenomics) repo
|
|
250
|
+
(`benchmarks/{BENCHMARKS,THEORY}.md`) alongside the benchmark scripts.
|
|
218
251
|
|
|
219
252
|
## Reproduce the paper
|
|
220
253
|
|
|
254
|
+
The self-contained theory notebooks run on bundled data:
|
|
255
|
+
|
|
221
256
|
```bash
|
|
222
|
-
|
|
223
|
-
|
|
257
|
+
pip install "mirpy-lib[examples]"
|
|
258
|
+
marimo edit examples/theory.py # supplementary S1–S3 (distance laws, D↔d, prototype robustness)
|
|
259
|
+
marimo edit examples/quickstart.py # embed + cluster VDJdb antigens
|
|
224
260
|
```
|
|
225
261
|
|
|
262
|
+
The full benchmark suite (VDJdb Table S1, density, repertoire/TCGA) and result docs live in the
|
|
263
|
+
companion analysis repo [`2026-mirpy-analysis`](https://github.com/antigenomics) — this repo is the
|
|
264
|
+
library + CI tests only.
|
|
265
|
+
|
|
226
266
|
Method: Kremlyakova *et al.*, *TCREMP: a bioinformatic pipeline for efficient embedding of
|
|
227
267
|
T-cell receptor sequences*, **J Mol Biol** 437 (2025) 169205.
|
|
228
268
|
|
|
@@ -236,12 +276,14 @@ mirpy is CPU-parallel by default and uses the GPU for the neural codecs. Knobs,
|
|
|
236
276
|
| Density kNN / balloon | `neighbor_enrichment(..., backend=…)` | `"exact"` (BallTree, **1 core**) | `backend="kdtree"` = exact scipy cKDTree, **all cores** (`workers=-1`), 5–9× faster; `backend="ann"` = pynndescent, auto all-core, ~30× at ≥1e5. Prefer `kdtree` for multicore exact. |
|
|
237
277
|
| Clustering | `cluster(..., n_jobs=-1)` | sklearn default (1) | forwarded to DBSCAN/OPTICS/HDBSCAN via `**kwargs`; parallelizes the neighbour search. |
|
|
238
278
|
| BLAS (PCA, RFF, matmul) | `OMP_NUM_THREADS` / `OPENBLAS_NUM_THREADS` env | all cores | numpy/sklearn use the platform BLAS; cap via env if oversubscribed. |
|
|
239
|
-
| Neural codecs (`mir.ml`) | `pick_device()` / `device=` / `MIR_DEVICE` env | **CUDA → MPS → CPU**, auto | every `train_*` / codec / bundle takes `device=`; e.g. `MIR_DEVICE=cuda:1
|
|
279
|
+
| Neural codecs (`mir.ml`) | `pick_device()` / `device=` / `MIR_DEVICE` env | **CUDA → MPS → CPU**, auto | every `train_*` / codec / bundle takes `device=`; e.g. `MIR_DEVICE=cuda:1` pins the second GPU. Torch-free paths (`density`, `repertoire`) never touch the GPU. |
|
|
240
280
|
|
|
241
281
|
Rule of thumb: leave `threads=0` (all cores) for embedding; switch density to `backend="kdtree"`
|
|
242
282
|
for exact multicore or `"ann"` at whole-repertoire scale; the GPU is used only by `mir.ml`.
|
|
243
283
|
|
|
244
284
|
## Development
|
|
245
285
|
|
|
246
|
-
|
|
247
|
-
|
|
286
|
+
Repo-local `.venv` via [uv](https://docs.astral.sh/uv/) (bash/zsh): `bash setup.sh` — add
|
|
287
|
+
`--dev-parents` to editable-install the sibling `seqtree` / `vdjtools` / `vdjmatch` checkouts and
|
|
288
|
+
`--tests` to run the fast suite. Tests: `python -m pytest tests/ -q`. See [`CLAUDE.md`](CLAUDE.md)
|
|
289
|
+
for the architecture and reuse map.
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/mirpy_dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="assets/mirpy_light.svg">
|
|
5
|
+
<!-- Absolute PNG fallback: PyPI strips <picture>/<source> and cannot render a relative or
|
|
6
|
+
raw-served SVG, so the logo must be an absolute-URL raster here. GitHub uses the SVG sources. -->
|
|
7
|
+
<img alt="mirpy" src="https://raw.githubusercontent.com/antigenomics/mirpy/master/assets/mirpy_light.png" width="360">
|
|
8
|
+
</picture>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
1
11
|
<h1 align="center">mirpy — ML embeddings for immune repertoires</h1>
|
|
2
12
|
|
|
3
13
|
[](https://pypi.org/project/mirpy-lib/)
|
|
@@ -7,7 +17,7 @@
|
|
|
7
17
|
**mirpy v3** turns T-/B-cell receptor sequences into fixed-length numeric vectors you can
|
|
8
18
|
cluster, visualize, and feed to ML models. It implements **TCREMP** — embedding each receptor
|
|
9
19
|
by its alignment distances to a fixed set of *prototype* sequences — so that Euclidean distance
|
|
10
|
-
in embedding space approximates pairwise alignment distance (
|
|
20
|
+
in embedding space approximates pairwise alignment distance (Theory T1).
|
|
11
21
|
|
|
12
22
|
> v3 is a slim, embedding-focused rewrite. The classical repertoire toolkit (parsing, overlap,
|
|
13
23
|
> diversity, TCRnet, GLIPH, …) lives on the **`legacy-v2`** branch (`mirpy-lib` 2.x) and in the
|
|
@@ -50,6 +60,24 @@ labels = cluster(pca_denoise(X, n_components=50))
|
|
|
50
60
|
Paired chains concatenate per-chain embeddings via `PairedTCREmp`. Input/output are AIRR polars
|
|
51
61
|
frames keyed by `vdjtools.io.schema` column names.
|
|
52
62
|
|
|
63
|
+
## Command line
|
|
64
|
+
|
|
65
|
+
`pip install mirpy-lib` also installs a `mir` command for the two embedding scales — no Python
|
|
66
|
+
needed. Inputs are any format `vdjtools.io` reads (AIRR TSV, vdjtools, MiXCR, immunoSEQ, parquet).
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# one repertoire -> per-clonotype embedding table (e0…), the input to clustering / ML
|
|
70
|
+
mir embed clonotypes sample.tsv --pca 50 -o clonotypes.parquet
|
|
71
|
+
|
|
72
|
+
# a dataset of repertoires -> one fingerprint Φ(S) per sample, per chain (phi0…), on one
|
|
73
|
+
# shared basis so the rows are mutually comparable; --mmd also writes the pairwise MMD matrix
|
|
74
|
+
mir embed repertoires cohort/*.tsv.gz -o phi.tsv --mmd mmd.tsv
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`mir embed clonotypes -h` / `mir embed repertoires -h` list every flag (species, locus,
|
|
78
|
+
prototype count, weight, Φ blocks, …). Sample id defaults to the filename stem; the locus is
|
|
79
|
+
inferred per file (or restrict with `--locus`).
|
|
80
|
+
|
|
53
81
|
## Recommended presets
|
|
54
82
|
|
|
55
83
|
`TCREmp.from_defaults(species, locus)` uses the per-chain preset when `n_prototypes` is
|
|
@@ -87,9 +115,11 @@ Xr = pca_denoise(X, n_components=p.n_components_recon) # codec reconstruction
|
|
|
87
115
|
| `mir.embedding.tcremp` | `TCREmp` / `PairedTCREmp` — the prototype embedding |
|
|
88
116
|
| `mir.embedding.pca` | PCA denoising of embeddings |
|
|
89
117
|
| `mir.distances` | junction distance (`seqtree.gapblock`; `metric`/`matrix`/`alignment` options) + baked germline distances |
|
|
90
|
-
| `mir.bench` | VDJdb loader, clustering (`cluster(method=…)`: DBSCAN/HDBSCAN/OPTICS) + F1/retention, theory experiments (incl. `codec_losslessness`) |
|
|
118
|
+
| `mir.bench` | VDJdb loader, clustering (`cluster(method=…)`: DBSCAN/HDBSCAN/OPTICS) + F1/retention, theory experiments (incl. `codec_losslessness`), cohort scorers (`bench.eval`: `cv_auc`/`cv_cindex`/`km_logrank`) |
|
|
91
119
|
| `mir.density` | continuous-density TCRNET/ALICE — enrichment (+ clonal-abundance channel, `backend=` exact/kdtree/ann) + noise-filtering (Theory T6) |
|
|
92
|
-
| `mir.repertoire` | sample-level (repertoire) embedding — RFF kernel mean ‖ Hill diversity ‖ second moment; MMD / HLA-stratified distance; motif witness (Theory §T.7) |
|
|
120
|
+
| `mir.repertoire` | sample-level (repertoire) embedding — RFF kernel mean ‖ Hill diversity ‖ second moment; MMD / HLA-stratified distance; motif witness; `centroid_atypicality`, multi-locus `fit_repertoire_spaces` (Theory §T.7) |
|
|
121
|
+
| `mir.explain` | named-channel fusion (`ChannelBuilder`) + scorer-agnostic ablation (`channel_report`/`channel_drivers`) — which part of Φ carries the signal (§T.7) |
|
|
122
|
+
| `mir.cohort` | the **digital donor** — multi-chain `fit_donor_embeddings`/`DonorCohort` (+ `transform`/`save`/`load`) + `residualize` / `cluster_samples` / `incidence_biomarkers` (§T.7) |
|
|
93
123
|
| `mir.ml` | neural codecs (forward/inverse/Pgen/unified) + learned repertoire `set_encoder` (Set-Transformer/DeepRC) — Part 2, experimental; `[ml]` extra |
|
|
94
124
|
|
|
95
125
|
## Background subtraction & clustering (`mir.density`)
|
|
@@ -116,11 +146,12 @@ Use a **biological control** as the background when you have one (e.g. pre- vs p
|
|
|
116
146
|
patient vs healthy) — differential enrichment cancels generic public convergence and isolates the
|
|
117
147
|
antigen-specific response. With no control, `generate_background(locus, n)` samples the vdjtools
|
|
118
148
|
P_gen model (the ALICE regime); the "water level" of a naive repertoire is handled by the
|
|
119
|
-
empirical-null calibration.
|
|
149
|
+
empirical-null calibration. The density benchmarks (YFV, ankylosing-spondylitis B27, TCRNET)
|
|
150
|
+
live in the companion [`2026-mirpy-analysis`](https://github.com/antigenomics) repo.
|
|
120
151
|
|
|
121
152
|
At whole-repertoire scale, pass `neighbor_enrichment(..., backend="kdtree")` (exact scipy cKDTree,
|
|
122
153
|
5–9× faster than the default BallTree) or `backend="ann"` (approximate pynndescent, ~30× faster
|
|
123
|
-
past ~10⁵ clones, trading a small conservative undercount
|
|
154
|
+
past ~10⁵ clones, trading a small conservative undercount; `pip install "mirpy-lib[ann]"`).
|
|
124
155
|
|
|
125
156
|
## Sample-level (repertoire) embedding (`mir.repertoire`)
|
|
126
157
|
|
|
@@ -155,16 +186,24 @@ a batch offset is first-order and cancels, while a batch-orthogonal signal (e.g.
|
|
|
155
186
|
empirical rule of thumb — **diversity for how-even, the embedding for which-clones**: clone-size
|
|
156
187
|
phenotypes (age, CMV) are a diversity summary's turf, while clonotype identity (HLA — strongest in
|
|
157
188
|
TRA and class II) lives in the second moment / witness. A learned co-equal set encoder
|
|
158
|
-
(Set-Transformer / DeepRC) is in `mir.ml.set_encoder` (`[ml]` extra).
|
|
159
|
-
|
|
189
|
+
(Set-Transformer / DeepRC) is in `mir.ml.set_encoder` (`[ml]` extra). Recorded results and theory
|
|
190
|
+
(T7) live in the companion [`2026-mirpy-analysis`](https://github.com/antigenomics) repo
|
|
191
|
+
(`benchmarks/{BENCHMARKS,THEORY}.md`) alongside the benchmark scripts.
|
|
160
192
|
|
|
161
193
|
## Reproduce the paper
|
|
162
194
|
|
|
195
|
+
The self-contained theory notebooks run on bundled data:
|
|
196
|
+
|
|
163
197
|
```bash
|
|
164
|
-
|
|
165
|
-
|
|
198
|
+
pip install "mirpy-lib[examples]"
|
|
199
|
+
marimo edit examples/theory.py # supplementary S1–S3 (distance laws, D↔d, prototype robustness)
|
|
200
|
+
marimo edit examples/quickstart.py # embed + cluster VDJdb antigens
|
|
166
201
|
```
|
|
167
202
|
|
|
203
|
+
The full benchmark suite (VDJdb Table S1, density, repertoire/TCGA) and result docs live in the
|
|
204
|
+
companion analysis repo [`2026-mirpy-analysis`](https://github.com/antigenomics) — this repo is the
|
|
205
|
+
library + CI tests only.
|
|
206
|
+
|
|
168
207
|
Method: Kremlyakova *et al.*, *TCREMP: a bioinformatic pipeline for efficient embedding of
|
|
169
208
|
T-cell receptor sequences*, **J Mol Biol** 437 (2025) 169205.
|
|
170
209
|
|
|
@@ -178,12 +217,14 @@ mirpy is CPU-parallel by default and uses the GPU for the neural codecs. Knobs,
|
|
|
178
217
|
| Density kNN / balloon | `neighbor_enrichment(..., backend=…)` | `"exact"` (BallTree, **1 core**) | `backend="kdtree"` = exact scipy cKDTree, **all cores** (`workers=-1`), 5–9× faster; `backend="ann"` = pynndescent, auto all-core, ~30× at ≥1e5. Prefer `kdtree` for multicore exact. |
|
|
179
218
|
| Clustering | `cluster(..., n_jobs=-1)` | sklearn default (1) | forwarded to DBSCAN/OPTICS/HDBSCAN via `**kwargs`; parallelizes the neighbour search. |
|
|
180
219
|
| BLAS (PCA, RFF, matmul) | `OMP_NUM_THREADS` / `OPENBLAS_NUM_THREADS` env | all cores | numpy/sklearn use the platform BLAS; cap via env if oversubscribed. |
|
|
181
|
-
| Neural codecs (`mir.ml`) | `pick_device()` / `device=` / `MIR_DEVICE` env | **CUDA → MPS → CPU**, auto | every `train_*` / codec / bundle takes `device=`; e.g. `MIR_DEVICE=cuda:1
|
|
220
|
+
| Neural codecs (`mir.ml`) | `pick_device()` / `device=` / `MIR_DEVICE` env | **CUDA → MPS → CPU**, auto | every `train_*` / codec / bundle takes `device=`; e.g. `MIR_DEVICE=cuda:1` pins the second GPU. Torch-free paths (`density`, `repertoire`) never touch the GPU. |
|
|
182
221
|
|
|
183
222
|
Rule of thumb: leave `threads=0` (all cores) for embedding; switch density to `backend="kdtree"`
|
|
184
223
|
for exact multicore or `"ann"` at whole-repertoire scale; the GPU is used only by `mir.ml`.
|
|
185
224
|
|
|
186
225
|
## Development
|
|
187
226
|
|
|
188
|
-
|
|
189
|
-
|
|
227
|
+
Repo-local `.venv` via [uv](https://docs.astral.sh/uv/) (bash/zsh): `bash setup.sh` — add
|
|
228
|
+
`--dev-parents` to editable-install the sibling `seqtree` / `vdjtools` / `vdjmatch` checkouts and
|
|
229
|
+
`--tests` to run the fast suite. Tests: `python -m pytest tests/ -q`. See [`CLAUDE.md`](CLAUDE.md)
|
|
230
|
+
for the architecture and reuse map.
|
|
@@ -17,7 +17,29 @@ Every dataset used by mirpy v3, its origin, and how to regenerate it.
|
|
|
17
17
|
|---|---|---|---|
|
|
18
18
|
| VDJdb slim dump | `tests/assets/vdjdb.slim.txt.gz` | VDJdb (Goncharov et al., *Nat Methods* 2022) export, dotted-column slim format | Antigen labels for the Table S1 benchmark; a specific release — F1 differs from the paper's 2023 summer release |
|
|
19
19
|
| Epitope-specific TRB sets | `tests/assets/{gilgfvftl,llwngpmav}_*.gz` | VDJdb subsets | Single-antigen fixtures |
|
|
20
|
-
| OLGA TRB sample | `tests/assets/olga_humanTRB_1000.txt.gz` |
|
|
20
|
+
| OLGA TRB sample | `tests/assets/olga_humanTRB_1000.txt.gz` | **Computed**, not experimental: 1 000 human TRB rearrangements drawn from the OLGA generative model (Sethna et al., *Bioinformatics* 2019), seed 42. Cols (no header): `junction_nt, junction_aa, v_call, j_call`; kept in **generation order — do not sort** (see note below) | `conda run -n mirpy --no-capture-output olga-generate_sequences --humanTRB -n 1000 -o /tmp/olga.tsv --seed 42` then `awk -F'\t' -v OFS='\t' '{print $1,$2,$3"*01",$4"*01"}' /tmp/olga.tsv \| gzip -n > tests/assets/olga_humanTRB_1000.txt.gz` (the `*01` suffix is appended because OLGA emits gene-level calls but `mir` expects allele-level, matching the bundled prototypes) |
|
|
21
|
+
|
|
22
|
+
> **Provenance correction (2026-07-17).** Until this date `olga_humanTRB_1000.txt.gz` was **not**
|
|
23
|
+
> OLGA output despite its name and this row's claim. It was a `head`-slice of the *alphabetically
|
|
24
|
+
> sorted* VDJdb TRB dump: all 969 unique junctions matched `vdjdb.slim.txt.gz` TRB `cdr3` exactly
|
|
25
|
+
> (100%, vs **0.96%** for genuine OLGA), the `junction_nt` column was a placeholder run of `N`, and
|
|
26
|
+
> the first row (`ATSIRFTDTQYF`) lacked the Cys anchor OLGA always emits. Consequences of the old
|
|
27
|
+
> file, for anyone re-reading results predating this fix:
|
|
28
|
+
> 1. **Not an antigen-naive null.** 12% of its rows had a Hamming-1 neighbour within the file vs
|
|
29
|
+
> **0.2%** for real OLGA (166 vs 4 pairs per 1 000) — VDJdb is *selected for* the antigen-driven
|
|
30
|
+
> convergence such a background is supposed to lack. Any chance-rate / null calibration against it
|
|
31
|
+
> compared VDJdb to itself and is meaningless.
|
|
32
|
+
> 2. **Sorted ⇒ slices are not exchangeable.** Because it was alphabetically sorted, the
|
|
33
|
+
> `_load_olga(n)` / `_load_olga(n, offset=400)` obs-vs-background slice pattern in
|
|
34
|
+
> `tests/test_density.py` drew two systematically different regions of sequence space (the
|
|
35
|
+
> background block was 58% `CAIS`-prefixed and shared only 20 of 54 V genes with obs). Genuine
|
|
36
|
+
> OLGA output is in generation order, which is why the regenerate command above must **not** sort.
|
|
37
|
+
>
|
|
38
|
+
> The `tests/` suite was **not** invalidated: all 58 tests pass identically on both files (they use
|
|
39
|
+
> the asset only as a generic pool of TRB junctions, never as a null), and the Hamming-1 gate in
|
|
40
|
+
> `test_continuous_matches_discrete_hamming1` scores rho 0.727 on the old file vs 0.809 on real OLGA.
|
|
41
|
+
> The damage was confined to external calibrations that treated the file as a synthetic negative
|
|
42
|
+
> control.
|
|
21
43
|
|
|
22
44
|
Bundled model / Pgen data used at runtime lives in **vdjtools** (`vdjtools.model` bundled
|
|
23
45
|
parquet marginals for 7 loci × {olga, learned}) — see the vdjtools `SOURCES.md`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* Figures with dark ink on a transparent background (schematics, plots exported from LaTeX/gnuplot)
|
|
2
|
+
stay legible in dark mode by rendering on a white card. Scoped to `.mir-fig` so the theme-aware
|
|
3
|
+
logo (only-light / only-dark) is untouched. */
|
|
4
|
+
img.mir-fig {
|
|
5
|
+
background: #ffffff;
|
|
6
|
+
padding: 10px 14px;
|
|
7
|
+
border-radius: 8px;
|
|
8
|
+
max-width: 100%;
|
|
9
|
+
}
|
|
Binary file
|
|
Binary file
|