mirpy-lib 3.4.0__tar.gz → 3.6.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.6.0/CHANGELOG.md +285 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/PKG-INFO +80 -22
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/README.md +79 -17
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/SOURCES.md +11 -9
- mirpy_lib-3.6.0/assets/mirpy_dark.png +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/assets/mirpy_dark.svg +1 -1
- mirpy_lib-3.6.0/assets/mirpy_dark_bg.svg +1519 -0
- mirpy_lib-3.6.0/assets/mirpy_light.png +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/assets/mirpy_light.svg +1 -1
- mirpy_lib-3.6.0/assets/mirpy_light_bg.svg +1519 -0
- mirpy_lib-3.6.0/docs/_static/mirpy_dark.png +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/docs/_static/mirpy_dark.svg +1 -1
- mirpy_lib-3.6.0/docs/_static/mirpy_light.png +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/docs/_static/mirpy_light.svg +1 -1
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/docs/api.rst +12 -5
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/docs/conf.py +4 -8
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/docs/examples.rst +4 -1
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/docs/usage.rst +66 -12
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/pyproject.toml +4 -5
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/requirements.txt +1 -1
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/setup.sh +36 -17
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/__init__.py +1 -1
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/bench/__init__.py +2 -1
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/bench/theory.py +3 -2
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/bench/vdjdb.py +3 -1
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/cli.py +47 -5
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/cohort.py +22 -2
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/density.py +48 -13
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/distances/germline.py +21 -3
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/distances/junction.py +18 -7
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/embedding/presets.py +2 -1
- mirpy_lib-3.6.0/src/mir/embedding/prototypes.py +173 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/embedding/tcremp.py +45 -12
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/explain.py +6 -4
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/ml/bundle.py +16 -7
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/ml/set_encoder.py +2 -1
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/repertoire.py +70 -25
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/prototypes/generate_prototypes.py +3 -1
- mirpy_lib-3.4.0/CHANGELOG.md +0 -147
- mirpy_lib-3.4.0/assets/mirpy_dark.png +0 -0
- mirpy_lib-3.4.0/assets/mirpy_light.png +0 -0
- mirpy_lib-3.4.0/docs/_static/mirpy_dark.png +0 -0
- mirpy_lib-3.4.0/docs/_static/mirpy_light.png +0 -0
- mirpy_lib-3.4.0/docs/requirements.txt +0 -3
- mirpy_lib-3.4.0/src/mir/embedding/prototypes.py +0 -109
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/.gitignore +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/CLAUDE.md +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/LICENSE +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/docs/Makefile +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/docs/_static/custom.css +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/docs/_static/depth_robustness.png +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/docs/_static/sample_embedding_diagram.png +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/docs/index.rst +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/aliases.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/alleles.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/bench/eval.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/bench/metrics.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/distances/__init__.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/embedding/__init__.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/embedding/pca.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/ml/__init__.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/ml/codec.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/ml/decoder.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/ml/encoder.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/ml/tokenize.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/ml/train.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/py.typed +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/__init__.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/gene_library/README.md +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/gene_library/__init__.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/gene_library/build_gene_library.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/gene_library/build_region_annotations.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/gene_library/imgt_gene_library.txt +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/gene_library/olga_gene_library.txt +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/gene_library/region_annotations.txt +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/build_germline_dist.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/human_IGH.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/human_IGK.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/human_IGL.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/human_TRA.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/human_TRB.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/human_TRD.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/human_TRG.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/mouse_IGH.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/mouse_IGK.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/mouse_IGL.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/mouse_TRA.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/mouse_TRB.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/mouse_TRD.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/germline_dist/mouse_TRG.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/prototypes/.gitkeep +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/prototypes/human_IGH.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/prototypes/human_IGK.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/prototypes/human_IGL.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/prototypes/human_TRA.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/prototypes/human_TRB.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/prototypes/human_TRD.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/prototypes/human_TRG.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/prototypes/manifest.json +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/prototypes/mouse_TRA.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.6.0}/src/mir/resources/prototypes/mouse_TRB.tsv +0 -0
|
@@ -0,0 +1,285 @@
|
|
|
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.6.0 — 2026-07-30
|
|
7
|
+
|
|
8
|
+
Default-on functional filtering, a new default clone-size weight for repertoire embedding, and a
|
|
9
|
+
clearer crash message — a minor bump since the repertoire-embedding default changes the numeric
|
|
10
|
+
output for callers who didn't pass `weight=` explicitly.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`mir embed clonotypes` / `mir embed repertoires` drop non-coding clonotypes by default**
|
|
15
|
+
(`--filter-functional`, default on; `--no-filter-functional` to disable) via
|
|
16
|
+
`vdjtools.preprocess.filter_functional` — a stop codon or legacy out-of-frame marker
|
|
17
|
+
(`[*atgc#~_?]`) in `junction_aa` otherwise either crashes (`_`) or silently produces a
|
|
18
|
+
numerically meaningless embedding (`*`). `mir embed repertoires` skips (with a warning) any
|
|
19
|
+
sample/locus left empty after filtering, instead of crashing the whole batch.
|
|
20
|
+
- **`"duplicate_count"` (linear, `g(a)=a`) and `"log2p1"` (`g(a)=log2(1+a)`) clone-size weights**
|
|
21
|
+
for repertoire embedding, alongside the existing `"distinct"` (`g≡1`) and `"log1p"`/`"anscombe"`.
|
|
22
|
+
`"log2p1"` is the **new default** — see Changed.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **`TCREmp.embed` raises a clear error when `junction_aa` contains `'_'`** (the legacy vdjtools
|
|
27
|
+
out-of-frame marker), naming the value count and pointing at `filter_functional`, instead of
|
|
28
|
+
letting seqtree's opaque `"symbol '_' is not in the alphabet"` propagate uncaught. `'*'` (stop
|
|
29
|
+
codon) is unaffected by this check — it doesn't crash, so filter it via `filter_functional` if
|
|
30
|
+
you don't want it silently embedded.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **Default clone-size weight for repertoire embedding is now `"log2p1"`** (`g=log2(1+a)`),
|
|
35
|
+
changed from `"log1p"` (natural log). Affects `mir.repertoire.sample_embedding`,
|
|
36
|
+
`RepertoireSpace.sample_cloud`, `sample_descriptor`, `class_witness`, `mir.explain.channel_drivers`,
|
|
37
|
+
and `mir embed repertoires --weight`. This changes the numeric values of `Φ(S)`'s mean/second
|
|
38
|
+
blocks (and any MMD computed from them) for callers who relied on the implicit default — pass
|
|
39
|
+
`weight="log1p"` to reproduce the old default exactly.
|
|
40
|
+
|
|
41
|
+
## 3.5.0 — 2026-07-28
|
|
42
|
+
|
|
43
|
+
A documentation-accuracy pass plus the fixes found by the accompanying code audit. No public API
|
|
44
|
+
removed; two new keyword arguments, one runtime dependency dropped, and three paths that used to
|
|
45
|
+
fail silently now fail loudly — hence a minor bump rather than a patch.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- **Prototype replicates — `load_prototypes(..., replicate=r)`, `n_replicates()`, and
|
|
50
|
+
`replicate=` on `TCREmp`/`PairedTCREmp.from_defaults` and both `mir embed` commands.** Each
|
|
51
|
+
bundled chain ships 10 000 real receptors whose row order is already a uniform shuffle, so a
|
|
52
|
+
disjoint block of `n` rows is an independent draw from the same pool: `replicate=r` returns block
|
|
53
|
+
`r`, giving `10000 // n` replicates (**10** at the common `n=1000`) with no new data and no RNG.
|
|
54
|
+
This answers "is my result an artefact of *which* prototypes I drew?" — a question a nested
|
|
55
|
+
`n_prototypes` sweep cannot answer, since those draws are prefixes of one another.
|
|
56
|
+
|
|
57
|
+
`replicate=0` is unchanged and remains *the* prototype set behind every preset, bundled codec and
|
|
58
|
+
published number. `prototype_hash` now covers the replicate index, so `CodecBundle`,
|
|
59
|
+
`RepertoireSpace`, `DonorCohort` and `SetEncoderBundle` refuse to mix draws (artifacts written
|
|
60
|
+
before this release read as draw 0). README and the user guide state the default, the provenance
|
|
61
|
+
(real repertoires, `seed=42`), and that cross-replicate embeddings are incomparable.
|
|
62
|
+
- **`generate_background(..., species=, source="arda")`** — the P_gen background can now be drawn
|
|
63
|
+
in the **arda IMGT allele namespace** (the same frame as the prototypes and baked germline
|
|
64
|
+
distances, so generated V/J calls resolve exactly instead of taking the allele cascade) and for
|
|
65
|
+
**mouse**. Both need a `vdjtools` shipping the bundled `arda` model set: those 9 models were
|
|
66
|
+
already in the wheel but unreachable, fixed upstream in `vdjtools.model.load_bundled`. The human
|
|
67
|
+
`learned`/`olga` path is unchanged and still works on older vdjtools; asking for arda/mouse
|
|
68
|
+
without it raises a message naming the requirement.
|
|
69
|
+
- **`neighbor_enrichment(..., k_max=, seed=)`** — forwarded to the `backend="ann"` engine, which
|
|
70
|
+
already warned "raise `k_max`" for a saturated neighbour ball but gave no way to do it.
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- **Germline distances silently maxed out for 42 human and 71 mouse alleles**, including
|
|
75
|
+
**`IGHV3-23*01` and `IGHV1-69*01` — the two most-used human IGHV genes.** arda emits an
|
|
76
|
+
*ambiguity group* (`"IGHV1-69*01,IGHV1-69D*01"`) as one row when the alleles share an identical
|
|
77
|
+
germline region, and the allele index registered only the joined string, so a query naming a
|
|
78
|
+
member matched nothing and took the max-distance fallback — quietly, since a fallback is also the
|
|
79
|
+
legitimate answer for a genuinely unknown gene. Members are now indexed to their group's row (a
|
|
80
|
+
standalone row still wins). Across every bundled locus, 333 of 457 group-member names resolved to
|
|
81
|
+
the fallback before; now none do.
|
|
82
|
+
- **`mir embed repertoires --blocks diversity` crashed** with `ValueError: zero-dimensional arrays
|
|
83
|
+
cannot be concatenated`: `SampleEmbedding.vector` assumed the kernel-mean block was always
|
|
84
|
+
present. A mean-less Φ is now a valid vector, and `mmd_distance` / `mmd_matrix` say *why* MMD
|
|
85
|
+
needs the mean block instead of raising a `NoneType` `TypeError`.
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- **`GermlineDistances.matrix` resolves each *distinct* allele once** instead of once per row, then
|
|
90
|
+
gathers a small `(n_distinct, K)` table and expands it. A repertoire has ~10⁵–10⁶ rows but ~10²
|
|
91
|
+
distinct alleles, so this is the dominant cost of `TCREmp.embed`: measured **2.32 s → 1.04 s** for
|
|
92
|
+
200k clonotypes × 1000 prototypes (the germline block itself 0.765 s → 0.072 s), bit-identical
|
|
93
|
+
output, nulls still take the allele fallback.
|
|
94
|
+
- **`RandomFourierFeatures.transform` computes in place** after the matmul — peak memory for a deep
|
|
95
|
+
sample drops ~3× (measured 9.2 → 3.2 GiB at n=200k, D=2048), output identical.
|
|
96
|
+
- **`RepertoireSpace.save` / `DonorCohort.save` refuse a model built with a custom `matrix=` or
|
|
97
|
+
`alignment=`.** Neither knob is recorded in `meta`, so `load` rebuilt the default
|
|
98
|
+
gapblock/BLOSUM62 space — a *different* coordinate system that still passed the prototype-hash
|
|
99
|
+
check. Failing at save time keeps the comparability invariant honest.
|
|
100
|
+
- **`TCREmp.embed` rejects null `junction_aa`** with a message naming the column and the row count,
|
|
101
|
+
instead of an opaque `TypeError: object of type 'NoneType' has no len()` from inside seqtree.
|
|
102
|
+
- **`fit_donor_embeddings` warns when a locus has too few donors to fit the identity PCA** — that
|
|
103
|
+
block was silently emitted as all-NaN, imputed to a constant, information-free channel.
|
|
104
|
+
- **`alignment="sw"` raises a directed `ImportError`** ("pip install biopython") instead of a bare
|
|
105
|
+
`ModuleNotFoundError`; `python -m mir.embedding.tcremp` now skips the SW leg when BioPython is
|
|
106
|
+
absent, so the self-check passes in the default `[dev,bench]` environment.
|
|
107
|
+
- **Determinism**: `bench.theory._mutate` no longer "mutates" a residue to itself (5.3% of `k=1`
|
|
108
|
+
substitutions were no-ops, and it disagreed with `density._mutate1`); `load_vdjdb` and
|
|
109
|
+
`generate_prototypes` no longer depend on `.unique()`'s arbitrary row order.
|
|
110
|
+
- **Single-sourced the version**: `pyproject.toml` takes it from `src/mir/__init__.py`
|
|
111
|
+
(`[tool.hatch.version]`), `docs/conf.py` imports `mir.__version__`, and `publish.yml` validates
|
|
112
|
+
the release tag against the built wheel — one copy instead of three.
|
|
113
|
+
- **Corrected the documented gap-block ↔ Smith-Waterman relationship.** `mir.distances.junction`
|
|
114
|
+
claimed the two "agree for equal-length CDR3s and rank-correlate ≥0.99 on gapped pairs". Measured
|
|
115
|
+
over all 44 850 pairs of 300 bundled human-TRB prototypes: close pairs do agree exactly, but SW is
|
|
116
|
+
a *local* alignment, so distant pairs diverge (equal-length outliers 181 vs 77) and the overall
|
|
117
|
+
rank correlation is ρ≈0.78. The docstring now says that, and that the two are different coordinate
|
|
118
|
+
systems rather than interchangeable scales.
|
|
119
|
+
- **Tests**: the survival-scorer test is no longer marked `integration` (lifelines ships in
|
|
120
|
+
`[bench]`, which CI installs), lifting `mir.bench.eval` from 23% to 64% coverage in CI;
|
|
121
|
+
`test_set_encoder.py` skips instead of failing on a torch-free install, so the documented
|
|
122
|
+
`pytest tests/` is green again; the VDJdb loader test no longer depends on a gitignored fixture it
|
|
123
|
+
could never find in CI (`mir.bench.vdjdb` 44% → 100%); and `TCREmp`'s public coordinate knobs
|
|
124
|
+
(`metric="sqrt"`, custom `matrix=`, `alignment="sw"`, null rejection) have real tests instead of
|
|
125
|
+
assertions buried in a `__main__` block. Fast-tier coverage 64% → 66%.
|
|
126
|
+
- **CI gained an `optional-tiers` job** (`[dev,bench,ml]` + BioPython, `-m "not benchmark"`): all of
|
|
127
|
+
`mir.ml` — ~20% of the library, including both prototype-hash comparability guards — previously
|
|
128
|
+
ran in no CI configuration, because the only job installs `[dev,bench]` and every torch test skips.
|
|
129
|
+
- Docs corrections: the density `backend=` default is `"kdtree"` (all-core exact), not `"exact"`;
|
|
130
|
+
the `cv_cindex` snippets in the user guide and in `mir.explain` now match the real
|
|
131
|
+
`(durations, events, *, base, block)` signature (the old form scored every channel `NaN`);
|
|
132
|
+
`CodecBundle.load` is documented as *not* verifying (its `forward_encoder` does); `encoder.code`
|
|
133
|
+
vs `encoder.encode`; the `mir embed repertoires` flag list is complete; `SOURCES.md` records the
|
|
134
|
+
prototypes as `arda-real` (experimental) with `src/`-prefixed regenerate commands; the examples'
|
|
135
|
+
run lines point at `examples/`, not the old `notebooks/`.
|
|
136
|
+
|
|
137
|
+
### Removed
|
|
138
|
+
|
|
139
|
+
- **`tqdm`** from the runtime dependencies — nothing in the package imports it.
|
|
140
|
+
- **`nbsphinx` / `nbsphinx-link` / `ipython`** from the `[docs]` extra, and the unreferenced
|
|
141
|
+
`docs/requirements.txt`: the docs have contained no notebooks since the examples became marimo
|
|
142
|
+
scripts. The Sphinx build stays zero-warning under `-W`.
|
|
143
|
+
|
|
144
|
+
## 3.4.0 — 2026-07-18
|
|
145
|
+
|
|
146
|
+
Minor: a command-line interface, a uv-based dev setup, and a documentation overhaul. No public
|
|
147
|
+
Python API removed; one optional-dependency group split out.
|
|
148
|
+
|
|
149
|
+
### Added
|
|
150
|
+
|
|
151
|
+
- **`mir` command-line interface** (`[project.scripts]`, also `python -m mir.cli`) — the two
|
|
152
|
+
embedding scales without writing Python:
|
|
153
|
+
- `mir embed clonotypes SAMPLE` → a per-clonotype TCREMP embedding table (`e0…`).
|
|
154
|
+
- `mir embed repertoires SAMPLE…` → one repertoire vector `Φ(S)` per sample **per chain** on one
|
|
155
|
+
shared basis (`phi0…`), with optional `--mmd` pairwise-distance output.
|
|
156
|
+
|
|
157
|
+
Inputs are any format `vdjtools.io` reads (AIRR/vdjtools/MiXCR/immunoSEQ/parquet); output is TSV
|
|
158
|
+
or Parquet. See `mir embed <cmd> -h`. Tests in `tests/test_cli.py`.
|
|
159
|
+
- **`mir.repertoire.correct_batch`** — Harmony-like cluster-aware batch correction on a stacked
|
|
160
|
+
sample×feature Φ matrix. Removes the batch offset *per soft cluster* (batch-diversity-penalised),
|
|
161
|
+
so a batch confounded with a biological cluster is corrected without erasing that biology; reduces
|
|
162
|
+
exactly to `mir.cohort.residualize` at `n_clusters=1` / `theta=0` (`prop:batch`).
|
|
163
|
+
- **`[ann]` optional-dependency group** for the approximate-NN density backend (`pynndescent`).
|
|
164
|
+
|
|
165
|
+
### Changed
|
|
166
|
+
|
|
167
|
+
- **Development now uses a repo-local `.venv` via [uv](https://docs.astral.sh/uv/)** instead of
|
|
168
|
+
conda. `setup.sh` is rewritten (bash/zsh portable; `--dev-parents`, `--docs`, `--tests`); the
|
|
169
|
+
conda `environment.yml` is removed. Runtime is unchanged — still a pure-Python `py3-none-any` wheel.
|
|
170
|
+
- **`pynndescent` moved from `[bench]` to the new `[ann]` extra.** `[bench]` is now all pure-wheel
|
|
171
|
+
(no numba/llvmlite), so `pip install "mirpy-lib[bench]"` resolves cleanly on any Python. Users of
|
|
172
|
+
`density.neighbor_enrichment(backend="ann")` should install `"mirpy-lib[ann]"`.
|
|
173
|
+
- **`vdjtools>=3.0.0`** (was `>=2.3.0`).
|
|
174
|
+
- Documentation overhaul: a use-case-driven user guide, the two CLI commands documented, an
|
|
175
|
+
examples/notebooks page, `mir.cohort` and `mir.bench.eval` added to the API reference, a logo, and
|
|
176
|
+
the sample-embedding schematic + real depth-robustness figure. Zero-warning Sphinx build.
|
|
177
|
+
- **Repo layout** (no effect on the installed package): adopted the **src-layout** (`mir/` →
|
|
178
|
+
`src/mir/`); renamed `notebooks/` → `examples/`; and moved the working result/plan markdown out of
|
|
179
|
+
the repo root — `THEORY.md` to the manuscript repo, `BENCHMARKS.md` / `REPERTOIRE_{EMBEDDING,LESSONS}.md`
|
|
180
|
+
/ `SQRT_D_MIGRATION.md` / `ROADMAP.md` to `2026-mirpy-analysis/benchmarks/`. Root keeps
|
|
181
|
+
README / CHANGELOG / CLAUDE / SOURCES.
|
|
182
|
+
|
|
183
|
+
### Fixed
|
|
184
|
+
|
|
185
|
+
- `DEFAULT_GAP_POSITIONS = (3, 4, -4, -3)` had three independent definitions
|
|
186
|
+
(`distances/junction`, `embedding/tcremp`, `ml/bundle`); now defined once in `distances.junction`
|
|
187
|
+
and imported, so the coordinate constant cannot drift.
|
|
188
|
+
- `cohort.cluster_samples` docstring described itself; `AntigenMetric` and `mir.bench.eval` gained
|
|
189
|
+
the docstrings/module-map entries they were missing.
|
|
190
|
+
|
|
191
|
+
## 3.3.0 — 2026-07-17
|
|
192
|
+
|
|
193
|
+
Minor: one new public parameter, nothing removed or changed.
|
|
194
|
+
|
|
195
|
+
### Added
|
|
196
|
+
|
|
197
|
+
- **`fit_density_space(chunk_size=)`** — embed and project in batches so the full raw matrix is never
|
|
198
|
+
resident. Peak memory becomes `max(pca_fit_cap, chunk_size) × n_features` instead of
|
|
199
|
+
`len(df) × n_features`: measured **10.60 GB → 1.81 GB** at 450k pooled clonotypes, and flat in `N`
|
|
200
|
+
(vs linear), at no wall-clock cost. This is what makes whole-cohort density arms runnable on a
|
|
201
|
+
laptop — the 4.2M-clonotype pooled arm is ~51 GB raw and ~102 GB once `scaler.transform` upcasts to
|
|
202
|
+
float64. Chunking is bit-exact at the embedding level (`_embed` of a slice == the slice of
|
|
203
|
+
`_embed`); the projected coordinates agree to float noise (~1e-7 relative), since BLAS summation
|
|
204
|
+
order depends on batch shape.
|
|
205
|
+
|
|
206
|
+
### Fixed
|
|
207
|
+
|
|
208
|
+
- `fit_density_space`'s `pca_fit_cap` docstring claimed it "lets whole repertoires be embedded without
|
|
209
|
+
a full-matrix PCA". It caps the **fit**, not the memory — both raw matrices were already
|
|
210
|
+
materialized before the PCA was fitted. Documented, and `chunk_size=` is the actual remedy.
|
|
211
|
+
- **`mir.__version__` was stale** — it read `3.1.1` on the published 3.2.0, because the release bump
|
|
212
|
+
moved `pyproject.toml` but not `mir/__init__.py`, and `publish.yml` only validates *pyproject ==
|
|
213
|
+
tag*. Both now read 3.3.0. (`__version__` is still hand-maintained; deriving it from
|
|
214
|
+
`importlib.metadata` would retire this failure mode for good.)
|
|
215
|
+
- `tests/assets/olga_humanTRB_1000.txt.gz` was a slice of the alphabetically sorted VDJdb TRB dump,
|
|
216
|
+
not OLGA output as its name and `SOURCES.md` claimed — so it was **not** an antigen-naive null (12%
|
|
217
|
+
of rows had a Hamming-1 neighbour vs 0.2% for real OLGA). Regenerated from `olga-generate_sequences`;
|
|
218
|
+
provenance and a byte-reproducible regenerate command recorded in `SOURCES.md`. No test was
|
|
219
|
+
invalidated (they use it only as a generic TRB junction pool), but external calibrations that treated
|
|
220
|
+
it as a synthetic negative control were comparing VDJdb against itself. Not shipped (tests are
|
|
221
|
+
excluded from the sdist); listed here because it invalidates results, not code.
|
|
222
|
+
|
|
223
|
+
## 3.2.0 — 2026-07-17
|
|
224
|
+
|
|
225
|
+
Minor: one new public module, nothing removed or changed.
|
|
226
|
+
|
|
227
|
+
### Added
|
|
228
|
+
|
|
229
|
+
- **`mir.explain`** (T7) — explainable readouts over any repertoire feature matrix.
|
|
230
|
+
`ChannelSpec` / `ChannelBuilder` / `stack_embeddings` attach the name→column map that `Φ.vector`
|
|
231
|
+
does not carry (`stack_embeddings` is exact: `X[i] == embs[i].vector`, names only, no transform);
|
|
232
|
+
`channel_report` ablates each named channel under a caller-supplied scorer (leave-one-in by
|
|
233
|
+
default; `mode="both"` adds the conditional half that exposes *redundant* channels — high `delta`,
|
|
234
|
+
`delta_out≈0`; optional row-permutation p-values); `channel_drivers` hops from a winning
|
|
235
|
+
**kernel-mean** channel to the clonotypes driving it via `class_witness`, and refuses channels with
|
|
236
|
+
no clonotype pre-image (a Hill number's "drivers" are a category error, not an open question).
|
|
237
|
+
Scorer-agnostic by design — the library never sees the labels and ships no scorers, so a Cox
|
|
238
|
+
C-index and a CV AUC both plug in. **No existing module changed.**
|
|
239
|
+
|
|
240
|
+
## 3.1.1 — 2026-07-14
|
|
241
|
+
|
|
242
|
+
Maintenance re-release of 3.1.0 with no functional changes — 3.1.0 was withdrawn from PyPI, and
|
|
243
|
+
this version restores the package under a fresh, clean version. The API, coordinates, and behaviour
|
|
244
|
+
are identical to 3.1.0 (see below).
|
|
245
|
+
|
|
246
|
+
## 3.1.0 — 2026-07-14
|
|
247
|
+
|
|
248
|
+
The Part-2 feature tier on top of the 3.0 consolidated embedding core: neural codecs, continuous-density
|
|
249
|
+
methods, and the sample-level (repertoire) embedding, all on arda-native coordinates.
|
|
250
|
+
|
|
251
|
+
### Added
|
|
252
|
+
- **`mir.density`** (T6) — graph-free continuous-density TCRNET/ALICE: balloon adaptive-radius enrichment
|
|
253
|
+
(Poisson/binomial + BH q, water-level calibration), abundance-aware weighted mass, backends
|
|
254
|
+
`exact`(BallTree) / `kdtree`(multicore) / `ann`(pynndescent).
|
|
255
|
+
- **`mir.repertoire`** (T7) — sample-level embedding `Φ(S)` = RFF kernel mean ‖ coverage-standardized Hill ‖
|
|
256
|
+
second-moment Fisher; `mmd_distance`/`mmd_matrix` (now with **`unbiased=True`** diagonal-removed MMD²),
|
|
257
|
+
`hla_stratified_mmd`, `class_witness` motif finder.
|
|
258
|
+
- **`mir.ml`** (Part 2, `[ml]` extra) — forward/inverse/pgen/unified neural codecs + `CodecBundle`
|
|
259
|
+
(prototype-hash-verified shipping); learned repertoire track `set_encoder` (Set-Transformer/DeepRC).
|
|
260
|
+
- **Bench** — clustering `method=` (dbscan/hdbscan/optics); `bench.theory` T6 `tcrnet_convergence`,
|
|
261
|
+
`codec_losslessness`.
|
|
262
|
+
- **Repertoire benchmarks** — `experiments/benchmark_repertoire_{aging,depth,cmvhla,hla,yfv,spikein,
|
|
263
|
+
agediverge}.py` and the COVID cohort suite `{covidbatch,covidhla,covidstatus,covidpaired}.py`
|
|
264
|
+
(`airr_covid19`, local-first + HF fallback). Recorded baselines in `BENCHMARKS.md`.
|
|
265
|
+
|
|
266
|
+
### Changed
|
|
267
|
+
- **Coordinate system re-pinned to arda-native** germline + real-repertoire prototypes (versioned; any model
|
|
268
|
+
trained on the old coords must be retrained).
|
|
269
|
+
- **`mir.ml` device selection** now **CUDA → MPS → CPU** (was MPS-only) with a `MIR_DEVICE` env override and
|
|
270
|
+
CUDA seeding — GPU support beyond Apple silicon.
|
|
271
|
+
- Documented all parallelism knobs (README "Performance & parallelism"): `TCREmp(threads=0)` all-core,
|
|
272
|
+
density `backend="kdtree"` multicore-exact, `cluster(n_jobs=…)`, BLAS env.
|
|
273
|
+
|
|
274
|
+
### Fixed
|
|
275
|
+
- **Unbiased repertoire MMD** — the biased V-statistic's `1/n_eff` self-term inflated low-diversity samples and
|
|
276
|
+
faked a divergence signal; `unbiased=True` removes it. Aging-divergence re-evaluated at depth: real but
|
|
277
|
+
diversity-coupled, not an independent axis.
|
|
278
|
+
|
|
279
|
+
### Notes
|
|
280
|
+
- New lessons for the theory appendix in `REPERTOIRE_LESSONS.md`; full findings in `THEORY.md` T7.
|
|
281
|
+
- Pure-Python `py3-none-any` wheel; native code comes from `seqtree`/`vdjtools` wheels.
|
|
282
|
+
|
|
283
|
+
## 3.0.0 — greenfield v3 embedding core (unreleased)
|
|
284
|
+
- Prototype (TCREMP) embedding on `seqtree.gapblock` + baked arda germline distances; vdjtools reuse (no AIRR
|
|
285
|
+
data-model layer of its own); bench harness (VDJdb Table S1); theory scaffold (`THEORY.md` S1–S3, T1–T5).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mirpy-lib
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.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
|
|
@@ -24,7 +24,6 @@ Requires-Dist: polars
|
|
|
24
24
|
Requires-Dist: scikit-learn
|
|
25
25
|
Requires-Dist: scipy
|
|
26
26
|
Requires-Dist: seqtree>=0.3.0
|
|
27
|
-
Requires-Dist: tqdm
|
|
28
27
|
Requires-Dist: vdjtools>=3.0.0
|
|
29
28
|
Provides-Extra: ann
|
|
30
29
|
Requires-Dist: pynndescent; extra == 'ann'
|
|
@@ -44,9 +43,6 @@ Requires-Dist: psutil>=5; extra == 'dev'
|
|
|
44
43
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
45
44
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
46
45
|
Provides-Extra: docs
|
|
47
|
-
Requires-Dist: ipython<10,>=8; extra == 'docs'
|
|
48
|
-
Requires-Dist: nbsphinx-link<2,>=1.3; extra == 'docs'
|
|
49
|
-
Requires-Dist: nbsphinx<1,>=0.9; extra == 'docs'
|
|
50
46
|
Requires-Dist: pydata-sphinx-theme<1,>=0.15; extra == 'docs'
|
|
51
47
|
Requires-Dist: sphinx<9,>=7.2; extra == 'docs'
|
|
52
48
|
Provides-Extra: examples
|
|
@@ -108,12 +104,13 @@ df = pl.DataFrame({
|
|
|
108
104
|
X = model.embed(df) # (2, 9000) float32 — 3 distances × 3000 prototypes
|
|
109
105
|
```
|
|
110
106
|
|
|
111
|
-
Downstream (cluster antigen-specific TCRs
|
|
107
|
+
Downstream (cluster antigen-specific TCRs — needs `[bench]` for the kneedle `eps`, and a real
|
|
108
|
+
set of clonotypes rather than the two above):
|
|
112
109
|
|
|
113
110
|
```python
|
|
114
111
|
from mir.embedding.pca import pca_denoise
|
|
115
112
|
from mir.bench.metrics import cluster, cluster_metrics
|
|
116
|
-
labels = cluster(pca_denoise(
|
|
113
|
+
labels = cluster(pca_denoise(model.embed(vdjdb_df), n_components=50))
|
|
117
114
|
```
|
|
118
115
|
|
|
119
116
|
Paired chains concatenate per-chain embeddings via `PairedTCREmp`. Input/output are AIRR polars
|
|
@@ -137,6 +134,11 @@ mir embed repertoires cohort/*.tsv.gz -o phi.tsv --mmd mmd.tsv
|
|
|
137
134
|
prototype count, weight, Φ blocks, …). Sample id defaults to the filename stem; the locus is
|
|
138
135
|
inferred per file (or restrict with `--locus`).
|
|
139
136
|
|
|
137
|
+
Both commands drop non-coding clonotypes (stop codon / legacy out-of-frame markers in
|
|
138
|
+
`junction_aa`) before embedding by default — pass `--no-filter-functional` to skip this. Without
|
|
139
|
+
it, a stop codon silently produces a numerically meaningless embedding and an out-of-frame `_`
|
|
140
|
+
marker crashes the run outright (neither is a valid amino acid).
|
|
141
|
+
|
|
140
142
|
## Recommended presets
|
|
141
143
|
|
|
142
144
|
`TCREmp.from_defaults(species, locus)` uses the per-chain preset when `n_prototypes` is
|
|
@@ -167,6 +169,53 @@ Xc = pca_denoise(X, n_components=p.n_components) # clustering
|
|
|
167
169
|
Xr = pca_denoise(X, n_components=p.n_components_recon) # codec reconstruction
|
|
168
170
|
```
|
|
169
171
|
|
|
172
|
+
## Prototypes — which receptors, and how much do they matter?
|
|
173
|
+
|
|
174
|
+
Every embedding is *distances to prototypes*, so the prototype set **is** the coordinate system.
|
|
175
|
+
mirpy ships one per chain, and you get them without downloading anything:
|
|
176
|
+
|
|
177
|
+
| | |
|
|
178
|
+
|---|---|
|
|
179
|
+
| What | **10 000 real receptors** per chain — a uniform random sample (fixed `seed=42`) of unique, productive, germline-resolvable clonotypes from arda-annotated real repertoires |
|
|
180
|
+
| Why real | Model-generated junctions have degenerate lengths and embed measurably worse (negative self-prototype distance correlation); real repertoires give a tight, well-behaved manifold |
|
|
181
|
+
| The default | `replicate=0` — the first `n` rows. This is *the* set: every preset, bundled codec, and published number uses it. Don't change it unless you're deliberately testing sensitivity |
|
|
182
|
+
| Chains | human TRA/TRB/TRG/TRD/IGH/IGK/IGL, mouse TRA/TRB (`list_available_prototypes()`) |
|
|
183
|
+
|
|
184
|
+
**Is my result an artefact of which prototypes I drew?** Take a replicate. The file order is itself a
|
|
185
|
+
uniform shuffle, so each disjoint block of `n` rows is an independent draw from the same pool —
|
|
186
|
+
`n_replicates()` of them, **10 at `n=1000`**, 5 at `n=2000`:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from mir.embedding.prototypes import n_replicates
|
|
190
|
+
from mir.embedding.tcremp import TCREmp
|
|
191
|
+
|
|
192
|
+
scores = [my_metric(TCREmp.from_defaults("human", "TRB", 1000, replicate=r).embed(df))
|
|
193
|
+
for r in range(n_replicates("human", "TRB", 1000))] # 10 draws; spread = sensitivity
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Same from the shell: `mir embed clonotypes sample.tsv --n-prototypes 1000 --replicate 3`.
|
|
197
|
+
|
|
198
|
+
**How much *does* it matter?** Usually very little, and you can check for yourself —
|
|
199
|
+
`bench.theory.prototype_source_correlation(queries, protos_a, protos_b)` correlates the pairwise
|
|
200
|
+
junction-distance geometry under two prototype sets. Two independent draws, 400 held-out human-TRB
|
|
201
|
+
queries:
|
|
202
|
+
|
|
203
|
+
| prototypes `n` | 100 | 250 | 500 | 1000 (default) | 2000 |
|
|
204
|
+
|---|--:|--:|--:|--:|--:|
|
|
205
|
+
| R between two draws | 0.922 | 0.971 | 0.990 | 0.993 | **0.997** |
|
|
206
|
+
|
|
207
|
+
So the geometry is essentially draw-independent from `n≈500` up: at the default counts, *which*
|
|
208
|
+
prototypes you drew is not what your result rests on. Below `n≈250` it starts to be.
|
|
209
|
+
|
|
210
|
+
> Each replicate is a **different coordinate system**. Distances *within* one are comparable;
|
|
211
|
+
> distances *across* two are not. The prototype hash covers the replicate index, so codecs,
|
|
212
|
+
> `RepertoireSpace` and `DonorCohort` all refuse to mix them — compare summary statistics across
|
|
213
|
+
> replicates (AUC, F1, cluster counts), never raw embeddings. Sweeping `n_prototypes` instead is a
|
|
214
|
+
> *nested* comparison (draw `r=0` at `n=500` is a prefix of `n=1000`), which answers "how many do I
|
|
215
|
+
> need", not "does it matter which".
|
|
216
|
+
|
|
217
|
+
Provenance and the regenerate command are in [`SOURCES.md`](SOURCES.md).
|
|
218
|
+
|
|
170
219
|
## What's inside
|
|
171
220
|
|
|
172
221
|
| Module | Purpose |
|
|
@@ -203,24 +252,32 @@ labels, mask = denoise_and_cluster(obs_emb, res) # noise-filter +
|
|
|
203
252
|
|
|
204
253
|
Use a **biological control** as the background when you have one (e.g. pre- vs post-vaccination,
|
|
205
254
|
patient vs healthy) — differential enrichment cancels generic public convergence and isolates the
|
|
206
|
-
antigen-specific response.
|
|
207
|
-
|
|
208
|
-
|
|
255
|
+
antigen-specific response. No control of your own? Pooled healthy-donor repertoires are one fetch
|
|
256
|
+
away (HF `isalgo/airr_control`, read with `vdjtools.io.read` — see [`SOURCES.md`](SOURCES.md)).
|
|
257
|
+
Failing that, `generate_background(locus, n)` samples the vdjtools P_gen model (the ALICE regime);
|
|
258
|
+
the "water level" of a naive repertoire is handled by the empirical-null calibration. Pass
|
|
259
|
+
`source="arda"` there when your data is arda-annotated (same allele namespace as the prototypes),
|
|
260
|
+
and `species="mouse"` for mouse — both need a vdjtools shipping the bundled `arda` model set. The density benchmarks (YFV, ankylosing-spondylitis B27, TCRNET)
|
|
209
261
|
live in the companion [`2026-mirpy-analysis`](https://github.com/antigenomics) repo.
|
|
210
262
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
263
|
+
The default backend is `"kdtree"` (exact scipy cKDTree, all cores). At whole-repertoire scale pass
|
|
264
|
+
`backend="ann"` (approximate pynndescent, ~30× faster past ~10⁵ clones, trading a small conservative
|
|
265
|
+
undercount; `pip install "mirpy-lib[ann]"`).
|
|
214
266
|
|
|
215
267
|
## Sample-level (repertoire) embedding (`mir.repertoire`)
|
|
216
268
|
|
|
217
269
|
One fixed vector `Φ(S)` per **repertoire** — an order-invariant multiset of clonotypes with clone
|
|
218
270
|
sizes — depth-robust into the low-coverage bulk-RNA-seq regime (Theory §T.7). `Φ(S)` sketches the
|
|
219
|
-
empirical measure `ρ_S = Σ_σ w_σ δ_{φ(σ)}`
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
271
|
+
empirical measure `ρ_S = Σ_σ w_σ δ_{φ(σ)}` in three blocks: an RFF **kernel mean** (depth-robust,
|
|
272
|
+
codebook-free — no `K`, no clustering), a coverage-standardized **Hill diversity** profile, and a
|
|
273
|
+
**second-moment** Fisher vector carrying clonotype co-occurrence (HLA-linked public structure).
|
|
274
|
+
Repertoire distance is the **MMD** `‖Φ₁(S) − Φ₁(S')‖`.
|
|
275
|
+
|
|
276
|
+
The per-clonotype weights `w_σ = g(a_σ)/Σ_τ g(a_τ)` come from a clone-size transform `g` (`weight=`
|
|
277
|
+
on `sample_embedding`/`fit_repertoire_space`/`mir embed repertoires --weight`): `"log2p1"` —
|
|
278
|
+
`g=log2(1+a)` — is the **default**, concave so one hyperexpanded clone can't dominate;
|
|
279
|
+
`"duplicate_count"` weights linearly by clone size (`g=a`); `"distinct"` ignores size entirely
|
|
280
|
+
(`g≡1`, presence only). `"log1p"` (natural log) and `"anscombe"` remain available.
|
|
224
281
|
|
|
225
282
|
```python
|
|
226
283
|
from mir.repertoire import fit_repertoire_space, sample_embedding, mmd_matrix, class_witness
|
|
@@ -256,7 +313,7 @@ The self-contained theory notebooks run on bundled data:
|
|
|
256
313
|
```bash
|
|
257
314
|
pip install "mirpy-lib[examples]"
|
|
258
315
|
marimo edit examples/theory.py # supplementary S1–S3 (distance laws, D↔d, prototype robustness)
|
|
259
|
-
marimo edit examples/quickstart.py # embed + cluster VDJdb
|
|
316
|
+
marimo edit examples/quickstart.py # embed + cluster (epitope colours need a local VDJdb dump)
|
|
260
317
|
```
|
|
261
318
|
|
|
262
319
|
The full benchmark suite (VDJdb Table S1, density, repertoire/TCGA) and result docs live in the
|
|
@@ -273,13 +330,14 @@ mirpy is CPU-parallel by default and uses the GPU for the neural codecs. Knobs,
|
|
|
273
330
|
| Stage | Knob | Default | Notes |
|
|
274
331
|
|---|---|---|---|
|
|
275
332
|
| Embedding (junction distance) | `TCREmp(..., threads=N)` | `0` = **all cores** | The C++ `seqtree.gapblock` scorer; releases the GIL, ~530 M pairs/s @16 cores. `threads=1` for a serial run. |
|
|
276
|
-
| Density kNN / balloon | `neighbor_enrichment(..., backend=…)` | `"
|
|
333
|
+
| Density kNN / balloon | `neighbor_enrichment(..., backend=…)` | `"kdtree"` (scipy cKDTree, **all cores**) | Exact and multithreaded (`workers=-1`), 5–9× faster than the BallTree baseline. `backend="ann"` = pynndescent, auto all-core, ~30× at ≥1e5; `backend="exact"` = the 1-core BallTree baseline, for reproducing older runs. |
|
|
277
334
|
| Clustering | `cluster(..., n_jobs=-1)` | sklearn default (1) | forwarded to DBSCAN/OPTICS/HDBSCAN via `**kwargs`; parallelizes the neighbour search. |
|
|
278
335
|
| BLAS (PCA, RFF, matmul) | `OMP_NUM_THREADS` / `OPENBLAS_NUM_THREADS` env | all cores | numpy/sklearn use the platform BLAS; cap via env if oversubscribed. |
|
|
279
336
|
| 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. |
|
|
280
337
|
|
|
281
|
-
Rule of thumb: leave `threads=0` (all cores) for embedding;
|
|
282
|
-
|
|
338
|
+
Rule of thumb: leave `threads=0` (all cores) for embedding; leave density on the default
|
|
339
|
+
`backend="kdtree"` (exact, multicore) and switch to `"ann"` only at whole-repertoire scale; the GPU
|
|
340
|
+
is used only by `mir.ml`.
|
|
283
341
|
|
|
284
342
|
## Development
|
|
285
343
|
|
|
@@ -49,12 +49,13 @@ df = pl.DataFrame({
|
|
|
49
49
|
X = model.embed(df) # (2, 9000) float32 — 3 distances × 3000 prototypes
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
Downstream (cluster antigen-specific TCRs
|
|
52
|
+
Downstream (cluster antigen-specific TCRs — needs `[bench]` for the kneedle `eps`, and a real
|
|
53
|
+
set of clonotypes rather than the two above):
|
|
53
54
|
|
|
54
55
|
```python
|
|
55
56
|
from mir.embedding.pca import pca_denoise
|
|
56
57
|
from mir.bench.metrics import cluster, cluster_metrics
|
|
57
|
-
labels = cluster(pca_denoise(
|
|
58
|
+
labels = cluster(pca_denoise(model.embed(vdjdb_df), n_components=50))
|
|
58
59
|
```
|
|
59
60
|
|
|
60
61
|
Paired chains concatenate per-chain embeddings via `PairedTCREmp`. Input/output are AIRR polars
|
|
@@ -78,6 +79,11 @@ mir embed repertoires cohort/*.tsv.gz -o phi.tsv --mmd mmd.tsv
|
|
|
78
79
|
prototype count, weight, Φ blocks, …). Sample id defaults to the filename stem; the locus is
|
|
79
80
|
inferred per file (or restrict with `--locus`).
|
|
80
81
|
|
|
82
|
+
Both commands drop non-coding clonotypes (stop codon / legacy out-of-frame markers in
|
|
83
|
+
`junction_aa`) before embedding by default — pass `--no-filter-functional` to skip this. Without
|
|
84
|
+
it, a stop codon silently produces a numerically meaningless embedding and an out-of-frame `_`
|
|
85
|
+
marker crashes the run outright (neither is a valid amino acid).
|
|
86
|
+
|
|
81
87
|
## Recommended presets
|
|
82
88
|
|
|
83
89
|
`TCREmp.from_defaults(species, locus)` uses the per-chain preset when `n_prototypes` is
|
|
@@ -108,6 +114,53 @@ Xc = pca_denoise(X, n_components=p.n_components) # clustering
|
|
|
108
114
|
Xr = pca_denoise(X, n_components=p.n_components_recon) # codec reconstruction
|
|
109
115
|
```
|
|
110
116
|
|
|
117
|
+
## Prototypes — which receptors, and how much do they matter?
|
|
118
|
+
|
|
119
|
+
Every embedding is *distances to prototypes*, so the prototype set **is** the coordinate system.
|
|
120
|
+
mirpy ships one per chain, and you get them without downloading anything:
|
|
121
|
+
|
|
122
|
+
| | |
|
|
123
|
+
|---|---|
|
|
124
|
+
| What | **10 000 real receptors** per chain — a uniform random sample (fixed `seed=42`) of unique, productive, germline-resolvable clonotypes from arda-annotated real repertoires |
|
|
125
|
+
| Why real | Model-generated junctions have degenerate lengths and embed measurably worse (negative self-prototype distance correlation); real repertoires give a tight, well-behaved manifold |
|
|
126
|
+
| The default | `replicate=0` — the first `n` rows. This is *the* set: every preset, bundled codec, and published number uses it. Don't change it unless you're deliberately testing sensitivity |
|
|
127
|
+
| Chains | human TRA/TRB/TRG/TRD/IGH/IGK/IGL, mouse TRA/TRB (`list_available_prototypes()`) |
|
|
128
|
+
|
|
129
|
+
**Is my result an artefact of which prototypes I drew?** Take a replicate. The file order is itself a
|
|
130
|
+
uniform shuffle, so each disjoint block of `n` rows is an independent draw from the same pool —
|
|
131
|
+
`n_replicates()` of them, **10 at `n=1000`**, 5 at `n=2000`:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from mir.embedding.prototypes import n_replicates
|
|
135
|
+
from mir.embedding.tcremp import TCREmp
|
|
136
|
+
|
|
137
|
+
scores = [my_metric(TCREmp.from_defaults("human", "TRB", 1000, replicate=r).embed(df))
|
|
138
|
+
for r in range(n_replicates("human", "TRB", 1000))] # 10 draws; spread = sensitivity
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Same from the shell: `mir embed clonotypes sample.tsv --n-prototypes 1000 --replicate 3`.
|
|
142
|
+
|
|
143
|
+
**How much *does* it matter?** Usually very little, and you can check for yourself —
|
|
144
|
+
`bench.theory.prototype_source_correlation(queries, protos_a, protos_b)` correlates the pairwise
|
|
145
|
+
junction-distance geometry under two prototype sets. Two independent draws, 400 held-out human-TRB
|
|
146
|
+
queries:
|
|
147
|
+
|
|
148
|
+
| prototypes `n` | 100 | 250 | 500 | 1000 (default) | 2000 |
|
|
149
|
+
|---|--:|--:|--:|--:|--:|
|
|
150
|
+
| R between two draws | 0.922 | 0.971 | 0.990 | 0.993 | **0.997** |
|
|
151
|
+
|
|
152
|
+
So the geometry is essentially draw-independent from `n≈500` up: at the default counts, *which*
|
|
153
|
+
prototypes you drew is not what your result rests on. Below `n≈250` it starts to be.
|
|
154
|
+
|
|
155
|
+
> Each replicate is a **different coordinate system**. Distances *within* one are comparable;
|
|
156
|
+
> distances *across* two are not. The prototype hash covers the replicate index, so codecs,
|
|
157
|
+
> `RepertoireSpace` and `DonorCohort` all refuse to mix them — compare summary statistics across
|
|
158
|
+
> replicates (AUC, F1, cluster counts), never raw embeddings. Sweeping `n_prototypes` instead is a
|
|
159
|
+
> *nested* comparison (draw `r=0` at `n=500` is a prefix of `n=1000`), which answers "how many do I
|
|
160
|
+
> need", not "does it matter which".
|
|
161
|
+
|
|
162
|
+
Provenance and the regenerate command are in [`SOURCES.md`](SOURCES.md).
|
|
163
|
+
|
|
111
164
|
## What's inside
|
|
112
165
|
|
|
113
166
|
| Module | Purpose |
|
|
@@ -144,24 +197,32 @@ labels, mask = denoise_and_cluster(obs_emb, res) # noise-filter +
|
|
|
144
197
|
|
|
145
198
|
Use a **biological control** as the background when you have one (e.g. pre- vs post-vaccination,
|
|
146
199
|
patient vs healthy) — differential enrichment cancels generic public convergence and isolates the
|
|
147
|
-
antigen-specific response.
|
|
148
|
-
|
|
149
|
-
|
|
200
|
+
antigen-specific response. No control of your own? Pooled healthy-donor repertoires are one fetch
|
|
201
|
+
away (HF `isalgo/airr_control`, read with `vdjtools.io.read` — see [`SOURCES.md`](SOURCES.md)).
|
|
202
|
+
Failing that, `generate_background(locus, n)` samples the vdjtools P_gen model (the ALICE regime);
|
|
203
|
+
the "water level" of a naive repertoire is handled by the empirical-null calibration. Pass
|
|
204
|
+
`source="arda"` there when your data is arda-annotated (same allele namespace as the prototypes),
|
|
205
|
+
and `species="mouse"` for mouse — both need a vdjtools shipping the bundled `arda` model set. The density benchmarks (YFV, ankylosing-spondylitis B27, TCRNET)
|
|
150
206
|
live in the companion [`2026-mirpy-analysis`](https://github.com/antigenomics) repo.
|
|
151
207
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
208
|
+
The default backend is `"kdtree"` (exact scipy cKDTree, all cores). At whole-repertoire scale pass
|
|
209
|
+
`backend="ann"` (approximate pynndescent, ~30× faster past ~10⁵ clones, trading a small conservative
|
|
210
|
+
undercount; `pip install "mirpy-lib[ann]"`).
|
|
155
211
|
|
|
156
212
|
## Sample-level (repertoire) embedding (`mir.repertoire`)
|
|
157
213
|
|
|
158
214
|
One fixed vector `Φ(S)` per **repertoire** — an order-invariant multiset of clonotypes with clone
|
|
159
215
|
sizes — depth-robust into the low-coverage bulk-RNA-seq regime (Theory §T.7). `Φ(S)` sketches the
|
|
160
|
-
empirical measure `ρ_S = Σ_σ w_σ δ_{φ(σ)}`
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
216
|
+
empirical measure `ρ_S = Σ_σ w_σ δ_{φ(σ)}` in three blocks: an RFF **kernel mean** (depth-robust,
|
|
217
|
+
codebook-free — no `K`, no clustering), a coverage-standardized **Hill diversity** profile, and a
|
|
218
|
+
**second-moment** Fisher vector carrying clonotype co-occurrence (HLA-linked public structure).
|
|
219
|
+
Repertoire distance is the **MMD** `‖Φ₁(S) − Φ₁(S')‖`.
|
|
220
|
+
|
|
221
|
+
The per-clonotype weights `w_σ = g(a_σ)/Σ_τ g(a_τ)` come from a clone-size transform `g` (`weight=`
|
|
222
|
+
on `sample_embedding`/`fit_repertoire_space`/`mir embed repertoires --weight`): `"log2p1"` —
|
|
223
|
+
`g=log2(1+a)` — is the **default**, concave so one hyperexpanded clone can't dominate;
|
|
224
|
+
`"duplicate_count"` weights linearly by clone size (`g=a`); `"distinct"` ignores size entirely
|
|
225
|
+
(`g≡1`, presence only). `"log1p"` (natural log) and `"anscombe"` remain available.
|
|
165
226
|
|
|
166
227
|
```python
|
|
167
228
|
from mir.repertoire import fit_repertoire_space, sample_embedding, mmd_matrix, class_witness
|
|
@@ -197,7 +258,7 @@ The self-contained theory notebooks run on bundled data:
|
|
|
197
258
|
```bash
|
|
198
259
|
pip install "mirpy-lib[examples]"
|
|
199
260
|
marimo edit examples/theory.py # supplementary S1–S3 (distance laws, D↔d, prototype robustness)
|
|
200
|
-
marimo edit examples/quickstart.py # embed + cluster VDJdb
|
|
261
|
+
marimo edit examples/quickstart.py # embed + cluster (epitope colours need a local VDJdb dump)
|
|
201
262
|
```
|
|
202
263
|
|
|
203
264
|
The full benchmark suite (VDJdb Table S1, density, repertoire/TCGA) and result docs live in the
|
|
@@ -214,13 +275,14 @@ mirpy is CPU-parallel by default and uses the GPU for the neural codecs. Knobs,
|
|
|
214
275
|
| Stage | Knob | Default | Notes |
|
|
215
276
|
|---|---|---|---|
|
|
216
277
|
| Embedding (junction distance) | `TCREmp(..., threads=N)` | `0` = **all cores** | The C++ `seqtree.gapblock` scorer; releases the GIL, ~530 M pairs/s @16 cores. `threads=1` for a serial run. |
|
|
217
|
-
| Density kNN / balloon | `neighbor_enrichment(..., backend=…)` | `"
|
|
278
|
+
| Density kNN / balloon | `neighbor_enrichment(..., backend=…)` | `"kdtree"` (scipy cKDTree, **all cores**) | Exact and multithreaded (`workers=-1`), 5–9× faster than the BallTree baseline. `backend="ann"` = pynndescent, auto all-core, ~30× at ≥1e5; `backend="exact"` = the 1-core BallTree baseline, for reproducing older runs. |
|
|
218
279
|
| Clustering | `cluster(..., n_jobs=-1)` | sklearn default (1) | forwarded to DBSCAN/OPTICS/HDBSCAN via `**kwargs`; parallelizes the neighbour search. |
|
|
219
280
|
| BLAS (PCA, RFF, matmul) | `OMP_NUM_THREADS` / `OPENBLAS_NUM_THREADS` env | all cores | numpy/sklearn use the platform BLAS; cap via env if oversubscribed. |
|
|
220
281
|
| 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. |
|
|
221
282
|
|
|
222
|
-
Rule of thumb: leave `threads=0` (all cores) for embedding;
|
|
223
|
-
|
|
283
|
+
Rule of thumb: leave `threads=0` (all cores) for embedding; leave density on the default
|
|
284
|
+
`backend="kdtree"` (exact, multicore) and switch to `"ann"` only at whole-repertoire scale; the GPU
|
|
285
|
+
is used only by `mir.ml`.
|
|
224
286
|
|
|
225
287
|
## Development
|
|
226
288
|
|