mirpy-lib 3.4.0__tar.gz → 3.7.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.7.0/CHANGELOG.md +317 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/CLAUDE.md +33 -4
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/PKG-INFO +125 -23
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/README.md +124 -18
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/SOURCES.md +11 -9
- mirpy_lib-3.7.0/assets/mirpy_dark.png +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/assets/mirpy_dark.svg +1 -1
- mirpy_lib-3.7.0/assets/mirpy_dark_bg.svg +1519 -0
- mirpy_lib-3.7.0/assets/mirpy_light.png +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/assets/mirpy_light.svg +1 -1
- mirpy_lib-3.7.0/assets/mirpy_light_bg.svg +1519 -0
- mirpy_lib-3.7.0/docs/_static/mirpy_dark.png +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/docs/_static/mirpy_dark.svg +1 -1
- mirpy_lib-3.7.0/docs/_static/mirpy_light.png +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/docs/_static/mirpy_light.svg +1 -1
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/docs/api.rst +58 -5
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/docs/conf.py +4 -8
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/docs/examples.rst +11 -2
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/docs/usage.rst +66 -12
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/pyproject.toml +4 -5
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/requirements.txt +1 -1
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/setup.sh +36 -17
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/__init__.py +1 -1
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/bench/__init__.py +2 -1
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/bench/theory.py +3 -2
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/bench/vdjdb.py +3 -1
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/cli.py +47 -5
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/cohort.py +22 -2
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/density.py +111 -13
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/distances/germline.py +21 -3
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/distances/junction.py +18 -7
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/embedding/presets.py +2 -1
- mirpy_lib-3.7.0/src/mir/embedding/prototypes.py +173 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/embedding/tcremp.py +45 -12
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/explain.py +6 -4
- mirpy_lib-3.7.0/src/mir/generate.py +233 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/ml/bundle.py +16 -7
- mirpy_lib-3.7.0/src/mir/ml/diffusion.py +378 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/ml/set_encoder.py +2 -1
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/repertoire.py +70 -25
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/prototypes/generate_prototypes.py +3 -1
- mirpy_lib-3.7.0/src/mir/track.py +331 -0
- mirpy_lib-3.7.0/src/mir/twin.py +179 -0
- 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.7.0}/.gitignore +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/LICENSE +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/docs/Makefile +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/docs/_static/custom.css +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/docs/_static/depth_robustness.png +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/docs/_static/sample_embedding_diagram.png +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/docs/index.rst +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/aliases.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/alleles.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/bench/eval.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/bench/metrics.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/distances/__init__.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/embedding/__init__.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/embedding/pca.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/ml/__init__.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/ml/codec.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/ml/decoder.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/ml/encoder.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/ml/tokenize.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/ml/train.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/py.typed +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/__init__.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/gene_library/README.md +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/gene_library/__init__.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/gene_library/build_gene_library.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/gene_library/build_region_annotations.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/gene_library/imgt_gene_library.txt +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/gene_library/olga_gene_library.txt +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/gene_library/region_annotations.txt +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/build_germline_dist.py +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/human_IGH.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/human_IGK.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/human_IGL.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/human_TRA.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/human_TRB.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/human_TRD.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/human_TRG.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/mouse_IGH.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/mouse_IGK.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/mouse_IGL.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/mouse_TRA.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/mouse_TRB.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/mouse_TRD.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/germline_dist/mouse_TRG.npz +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/prototypes/.gitkeep +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/prototypes/human_IGH.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/prototypes/human_IGK.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/prototypes/human_IGL.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/prototypes/human_TRA.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/prototypes/human_TRB.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/prototypes/human_TRD.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/prototypes/human_TRG.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/prototypes/manifest.json +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/prototypes/mouse_TRA.tsv +0 -0
- {mirpy_lib-3.4.0 → mirpy_lib-3.7.0}/src/mir/resources/prototypes/mouse_TRB.tsv +0 -0
|
@@ -0,0 +1,317 @@
|
|
|
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.7.0 — 2026-07-30
|
|
7
|
+
|
|
8
|
+
Four new modules: a PhenoPath-style exposure trajectory, the generative loop's mechanical and
|
|
9
|
+
research halves, and the digital twin that glues them to the digital donor — plus a repertoire-level
|
|
10
|
+
exposure channel. No public API removed; a minor bump.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`mir.track.fit_exposure_trajectory`** — a covariate-disentangled latent trajectory over any
|
|
15
|
+
per-sample channel matrix (PhenoPath, Campbell & Yau 2018, *Nat. Commun.* 9:2442,
|
|
16
|
+
doi:10.1038/s41467-018-04696-6, adapted from genes×cells to repertoire-channels×samples): infers a
|
|
17
|
+
shared exposure/progression pseudotime `tau` while separating out which channels respond to it
|
|
18
|
+
differently by a known covariate (`TrajectoryFit.top_interactions`). A simplified closed-form
|
|
19
|
+
alternating fit (per-channel ridge + GLS trajectory update + iteratively-reweighted ARD shrinkage
|
|
20
|
+
on the interaction term), not a literal reimplementation of PhenoPath's CAVI engine. Torch-free.
|
|
21
|
+
- **`mir.generate`** — the generative loop's mechanical half: `DescriptorDensity` (optionally
|
|
22
|
+
class-conditional Gaussian, Ledoit-Wolf shrinkage) over `RepertoireDescriptor` vectors;
|
|
23
|
+
`sample`/`evolve` (perturb one coordinate, propagate the coupled shift via the fitted covariance's
|
|
24
|
+
conditional mean) promote the ad-hoc `benchmark_repertoire_tcga_insilico.py` `np.cov`-slope pattern
|
|
25
|
+
into a reusable library object. Torch-free.
|
|
26
|
+
- **`mir.ml.diffusion`** (needs `[ml]`) — the generative loop's research half: a compact conditional
|
|
27
|
+
DDPM/DDIM generator (classifier-free guidance) over a compact descriptor/code space, sharing
|
|
28
|
+
`DescriptorDensity`'s `sample(n, condition=, seed=)` call shape so either generator drops in
|
|
29
|
+
unchanged. `DiffusionModel.save`/`load` mirrors `CodecBundle`'s shape.
|
|
30
|
+
- **`mir.twin.DonorTwin`/`make_twins`** — the digital twin: glue one donor's `RepertoireDescriptor` +
|
|
31
|
+
an optional `mir.track` trajectory position + covariate into one object; `.perturb()` and
|
|
32
|
+
`.simulate()` accept either generator.
|
|
33
|
+
- **`mir.density.exposure_score`/`exposure_channel`** — aggregate a per-clonotype
|
|
34
|
+
`neighbor_enrichment` result into repertoire-level exposure scalars (breadth, abundance-weighted
|
|
35
|
+
mass fraction, mean log2 fold), ready for `ChannelBuilder`/`DonorCohort extra_channels` — exposure
|
|
36
|
+
detection promoted from clone-level to a first-class cohort channel.
|
|
37
|
+
|
|
38
|
+
## 3.6.0 — 2026-07-30
|
|
39
|
+
|
|
40
|
+
Default-on functional filtering, a new default clone-size weight for repertoire embedding, and a
|
|
41
|
+
clearer crash message — a minor bump since the repertoire-embedding default changes the numeric
|
|
42
|
+
output for callers who didn't pass `weight=` explicitly.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **`mir embed clonotypes` / `mir embed repertoires` drop non-coding clonotypes by default**
|
|
47
|
+
(`--filter-functional`, default on; `--no-filter-functional` to disable) via
|
|
48
|
+
`vdjtools.preprocess.filter_functional` — a stop codon or legacy out-of-frame marker
|
|
49
|
+
(`[*atgc#~_?]`) in `junction_aa` otherwise either crashes (`_`) or silently produces a
|
|
50
|
+
numerically meaningless embedding (`*`). `mir embed repertoires` skips (with a warning) any
|
|
51
|
+
sample/locus left empty after filtering, instead of crashing the whole batch.
|
|
52
|
+
- **`"duplicate_count"` (linear, `g(a)=a`) and `"log2p1"` (`g(a)=log2(1+a)`) clone-size weights**
|
|
53
|
+
for repertoire embedding, alongside the existing `"distinct"` (`g≡1`) and `"log1p"`/`"anscombe"`.
|
|
54
|
+
`"log2p1"` is the **new default** — see Changed.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- **`TCREmp.embed` raises a clear error when `junction_aa` contains `'_'`** (the legacy vdjtools
|
|
59
|
+
out-of-frame marker), naming the value count and pointing at `filter_functional`, instead of
|
|
60
|
+
letting seqtree's opaque `"symbol '_' is not in the alphabet"` propagate uncaught. `'*'` (stop
|
|
61
|
+
codon) is unaffected by this check — it doesn't crash, so filter it via `filter_functional` if
|
|
62
|
+
you don't want it silently embedded.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- **Default clone-size weight for repertoire embedding is now `"log2p1"`** (`g=log2(1+a)`),
|
|
67
|
+
changed from `"log1p"` (natural log). Affects `mir.repertoire.sample_embedding`,
|
|
68
|
+
`RepertoireSpace.sample_cloud`, `sample_descriptor`, `class_witness`, `mir.explain.channel_drivers`,
|
|
69
|
+
and `mir embed repertoires --weight`. This changes the numeric values of `Φ(S)`'s mean/second
|
|
70
|
+
blocks (and any MMD computed from them) for callers who relied on the implicit default — pass
|
|
71
|
+
`weight="log1p"` to reproduce the old default exactly.
|
|
72
|
+
|
|
73
|
+
## 3.5.0 — 2026-07-28
|
|
74
|
+
|
|
75
|
+
A documentation-accuracy pass plus the fixes found by the accompanying code audit. No public API
|
|
76
|
+
removed; two new keyword arguments, one runtime dependency dropped, and three paths that used to
|
|
77
|
+
fail silently now fail loudly — hence a minor bump rather than a patch.
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- **Prototype replicates — `load_prototypes(..., replicate=r)`, `n_replicates()`, and
|
|
82
|
+
`replicate=` on `TCREmp`/`PairedTCREmp.from_defaults` and both `mir embed` commands.** Each
|
|
83
|
+
bundled chain ships 10 000 real receptors whose row order is already a uniform shuffle, so a
|
|
84
|
+
disjoint block of `n` rows is an independent draw from the same pool: `replicate=r` returns block
|
|
85
|
+
`r`, giving `10000 // n` replicates (**10** at the common `n=1000`) with no new data and no RNG.
|
|
86
|
+
This answers "is my result an artefact of *which* prototypes I drew?" — a question a nested
|
|
87
|
+
`n_prototypes` sweep cannot answer, since those draws are prefixes of one another.
|
|
88
|
+
|
|
89
|
+
`replicate=0` is unchanged and remains *the* prototype set behind every preset, bundled codec and
|
|
90
|
+
published number. `prototype_hash` now covers the replicate index, so `CodecBundle`,
|
|
91
|
+
`RepertoireSpace`, `DonorCohort` and `SetEncoderBundle` refuse to mix draws (artifacts written
|
|
92
|
+
before this release read as draw 0). README and the user guide state the default, the provenance
|
|
93
|
+
(real repertoires, `seed=42`), and that cross-replicate embeddings are incomparable.
|
|
94
|
+
- **`generate_background(..., species=, source="arda")`** — the P_gen background can now be drawn
|
|
95
|
+
in the **arda IMGT allele namespace** (the same frame as the prototypes and baked germline
|
|
96
|
+
distances, so generated V/J calls resolve exactly instead of taking the allele cascade) and for
|
|
97
|
+
**mouse**. Both need a `vdjtools` shipping the bundled `arda` model set: those 9 models were
|
|
98
|
+
already in the wheel but unreachable, fixed upstream in `vdjtools.model.load_bundled`. The human
|
|
99
|
+
`learned`/`olga` path is unchanged and still works on older vdjtools; asking for arda/mouse
|
|
100
|
+
without it raises a message naming the requirement.
|
|
101
|
+
- **`neighbor_enrichment(..., k_max=, seed=)`** — forwarded to the `backend="ann"` engine, which
|
|
102
|
+
already warned "raise `k_max`" for a saturated neighbour ball but gave no way to do it.
|
|
103
|
+
|
|
104
|
+
### Fixed
|
|
105
|
+
|
|
106
|
+
- **Germline distances silently maxed out for 42 human and 71 mouse alleles**, including
|
|
107
|
+
**`IGHV3-23*01` and `IGHV1-69*01` — the two most-used human IGHV genes.** arda emits an
|
|
108
|
+
*ambiguity group* (`"IGHV1-69*01,IGHV1-69D*01"`) as one row when the alleles share an identical
|
|
109
|
+
germline region, and the allele index registered only the joined string, so a query naming a
|
|
110
|
+
member matched nothing and took the max-distance fallback — quietly, since a fallback is also the
|
|
111
|
+
legitimate answer for a genuinely unknown gene. Members are now indexed to their group's row (a
|
|
112
|
+
standalone row still wins). Across every bundled locus, 333 of 457 group-member names resolved to
|
|
113
|
+
the fallback before; now none do.
|
|
114
|
+
- **`mir embed repertoires --blocks diversity` crashed** with `ValueError: zero-dimensional arrays
|
|
115
|
+
cannot be concatenated`: `SampleEmbedding.vector` assumed the kernel-mean block was always
|
|
116
|
+
present. A mean-less Φ is now a valid vector, and `mmd_distance` / `mmd_matrix` say *why* MMD
|
|
117
|
+
needs the mean block instead of raising a `NoneType` `TypeError`.
|
|
118
|
+
|
|
119
|
+
### Changed
|
|
120
|
+
|
|
121
|
+
- **`GermlineDistances.matrix` resolves each *distinct* allele once** instead of once per row, then
|
|
122
|
+
gathers a small `(n_distinct, K)` table and expands it. A repertoire has ~10⁵–10⁶ rows but ~10²
|
|
123
|
+
distinct alleles, so this is the dominant cost of `TCREmp.embed`: measured **2.32 s → 1.04 s** for
|
|
124
|
+
200k clonotypes × 1000 prototypes (the germline block itself 0.765 s → 0.072 s), bit-identical
|
|
125
|
+
output, nulls still take the allele fallback.
|
|
126
|
+
- **`RandomFourierFeatures.transform` computes in place** after the matmul — peak memory for a deep
|
|
127
|
+
sample drops ~3× (measured 9.2 → 3.2 GiB at n=200k, D=2048), output identical.
|
|
128
|
+
- **`RepertoireSpace.save` / `DonorCohort.save` refuse a model built with a custom `matrix=` or
|
|
129
|
+
`alignment=`.** Neither knob is recorded in `meta`, so `load` rebuilt the default
|
|
130
|
+
gapblock/BLOSUM62 space — a *different* coordinate system that still passed the prototype-hash
|
|
131
|
+
check. Failing at save time keeps the comparability invariant honest.
|
|
132
|
+
- **`TCREmp.embed` rejects null `junction_aa`** with a message naming the column and the row count,
|
|
133
|
+
instead of an opaque `TypeError: object of type 'NoneType' has no len()` from inside seqtree.
|
|
134
|
+
- **`fit_donor_embeddings` warns when a locus has too few donors to fit the identity PCA** — that
|
|
135
|
+
block was silently emitted as all-NaN, imputed to a constant, information-free channel.
|
|
136
|
+
- **`alignment="sw"` raises a directed `ImportError`** ("pip install biopython") instead of a bare
|
|
137
|
+
`ModuleNotFoundError`; `python -m mir.embedding.tcremp` now skips the SW leg when BioPython is
|
|
138
|
+
absent, so the self-check passes in the default `[dev,bench]` environment.
|
|
139
|
+
- **Determinism**: `bench.theory._mutate` no longer "mutates" a residue to itself (5.3% of `k=1`
|
|
140
|
+
substitutions were no-ops, and it disagreed with `density._mutate1`); `load_vdjdb` and
|
|
141
|
+
`generate_prototypes` no longer depend on `.unique()`'s arbitrary row order.
|
|
142
|
+
- **Single-sourced the version**: `pyproject.toml` takes it from `src/mir/__init__.py`
|
|
143
|
+
(`[tool.hatch.version]`), `docs/conf.py` imports `mir.__version__`, and `publish.yml` validates
|
|
144
|
+
the release tag against the built wheel — one copy instead of three.
|
|
145
|
+
- **Corrected the documented gap-block ↔ Smith-Waterman relationship.** `mir.distances.junction`
|
|
146
|
+
claimed the two "agree for equal-length CDR3s and rank-correlate ≥0.99 on gapped pairs". Measured
|
|
147
|
+
over all 44 850 pairs of 300 bundled human-TRB prototypes: close pairs do agree exactly, but SW is
|
|
148
|
+
a *local* alignment, so distant pairs diverge (equal-length outliers 181 vs 77) and the overall
|
|
149
|
+
rank correlation is ρ≈0.78. The docstring now says that, and that the two are different coordinate
|
|
150
|
+
systems rather than interchangeable scales.
|
|
151
|
+
- **Tests**: the survival-scorer test is no longer marked `integration` (lifelines ships in
|
|
152
|
+
`[bench]`, which CI installs), lifting `mir.bench.eval` from 23% to 64% coverage in CI;
|
|
153
|
+
`test_set_encoder.py` skips instead of failing on a torch-free install, so the documented
|
|
154
|
+
`pytest tests/` is green again; the VDJdb loader test no longer depends on a gitignored fixture it
|
|
155
|
+
could never find in CI (`mir.bench.vdjdb` 44% → 100%); and `TCREmp`'s public coordinate knobs
|
|
156
|
+
(`metric="sqrt"`, custom `matrix=`, `alignment="sw"`, null rejection) have real tests instead of
|
|
157
|
+
assertions buried in a `__main__` block. Fast-tier coverage 64% → 66%.
|
|
158
|
+
- **CI gained an `optional-tiers` job** (`[dev,bench,ml]` + BioPython, `-m "not benchmark"`): all of
|
|
159
|
+
`mir.ml` — ~20% of the library, including both prototype-hash comparability guards — previously
|
|
160
|
+
ran in no CI configuration, because the only job installs `[dev,bench]` and every torch test skips.
|
|
161
|
+
- Docs corrections: the density `backend=` default is `"kdtree"` (all-core exact), not `"exact"`;
|
|
162
|
+
the `cv_cindex` snippets in the user guide and in `mir.explain` now match the real
|
|
163
|
+
`(durations, events, *, base, block)` signature (the old form scored every channel `NaN`);
|
|
164
|
+
`CodecBundle.load` is documented as *not* verifying (its `forward_encoder` does); `encoder.code`
|
|
165
|
+
vs `encoder.encode`; the `mir embed repertoires` flag list is complete; `SOURCES.md` records the
|
|
166
|
+
prototypes as `arda-real` (experimental) with `src/`-prefixed regenerate commands; the examples'
|
|
167
|
+
run lines point at `examples/`, not the old `notebooks/`.
|
|
168
|
+
|
|
169
|
+
### Removed
|
|
170
|
+
|
|
171
|
+
- **`tqdm`** from the runtime dependencies — nothing in the package imports it.
|
|
172
|
+
- **`nbsphinx` / `nbsphinx-link` / `ipython`** from the `[docs]` extra, and the unreferenced
|
|
173
|
+
`docs/requirements.txt`: the docs have contained no notebooks since the examples became marimo
|
|
174
|
+
scripts. The Sphinx build stays zero-warning under `-W`.
|
|
175
|
+
|
|
176
|
+
## 3.4.0 — 2026-07-18
|
|
177
|
+
|
|
178
|
+
Minor: a command-line interface, a uv-based dev setup, and a documentation overhaul. No public
|
|
179
|
+
Python API removed; one optional-dependency group split out.
|
|
180
|
+
|
|
181
|
+
### Added
|
|
182
|
+
|
|
183
|
+
- **`mir` command-line interface** (`[project.scripts]`, also `python -m mir.cli`) — the two
|
|
184
|
+
embedding scales without writing Python:
|
|
185
|
+
- `mir embed clonotypes SAMPLE` → a per-clonotype TCREMP embedding table (`e0…`).
|
|
186
|
+
- `mir embed repertoires SAMPLE…` → one repertoire vector `Φ(S)` per sample **per chain** on one
|
|
187
|
+
shared basis (`phi0…`), with optional `--mmd` pairwise-distance output.
|
|
188
|
+
|
|
189
|
+
Inputs are any format `vdjtools.io` reads (AIRR/vdjtools/MiXCR/immunoSEQ/parquet); output is TSV
|
|
190
|
+
or Parquet. See `mir embed <cmd> -h`. Tests in `tests/test_cli.py`.
|
|
191
|
+
- **`mir.repertoire.correct_batch`** — Harmony-like cluster-aware batch correction on a stacked
|
|
192
|
+
sample×feature Φ matrix. Removes the batch offset *per soft cluster* (batch-diversity-penalised),
|
|
193
|
+
so a batch confounded with a biological cluster is corrected without erasing that biology; reduces
|
|
194
|
+
exactly to `mir.cohort.residualize` at `n_clusters=1` / `theta=0` (`prop:batch`).
|
|
195
|
+
- **`[ann]` optional-dependency group** for the approximate-NN density backend (`pynndescent`).
|
|
196
|
+
|
|
197
|
+
### Changed
|
|
198
|
+
|
|
199
|
+
- **Development now uses a repo-local `.venv` via [uv](https://docs.astral.sh/uv/)** instead of
|
|
200
|
+
conda. `setup.sh` is rewritten (bash/zsh portable; `--dev-parents`, `--docs`, `--tests`); the
|
|
201
|
+
conda `environment.yml` is removed. Runtime is unchanged — still a pure-Python `py3-none-any` wheel.
|
|
202
|
+
- **`pynndescent` moved from `[bench]` to the new `[ann]` extra.** `[bench]` is now all pure-wheel
|
|
203
|
+
(no numba/llvmlite), so `pip install "mirpy-lib[bench]"` resolves cleanly on any Python. Users of
|
|
204
|
+
`density.neighbor_enrichment(backend="ann")` should install `"mirpy-lib[ann]"`.
|
|
205
|
+
- **`vdjtools>=3.0.0`** (was `>=2.3.0`).
|
|
206
|
+
- Documentation overhaul: a use-case-driven user guide, the two CLI commands documented, an
|
|
207
|
+
examples/notebooks page, `mir.cohort` and `mir.bench.eval` added to the API reference, a logo, and
|
|
208
|
+
the sample-embedding schematic + real depth-robustness figure. Zero-warning Sphinx build.
|
|
209
|
+
- **Repo layout** (no effect on the installed package): adopted the **src-layout** (`mir/` →
|
|
210
|
+
`src/mir/`); renamed `notebooks/` → `examples/`; and moved the working result/plan markdown out of
|
|
211
|
+
the repo root — `THEORY.md` to the manuscript repo, `BENCHMARKS.md` / `REPERTOIRE_{EMBEDDING,LESSONS}.md`
|
|
212
|
+
/ `SQRT_D_MIGRATION.md` / `ROADMAP.md` to `2026-mirpy-analysis/benchmarks/`. Root keeps
|
|
213
|
+
README / CHANGELOG / CLAUDE / SOURCES.
|
|
214
|
+
|
|
215
|
+
### Fixed
|
|
216
|
+
|
|
217
|
+
- `DEFAULT_GAP_POSITIONS = (3, 4, -4, -3)` had three independent definitions
|
|
218
|
+
(`distances/junction`, `embedding/tcremp`, `ml/bundle`); now defined once in `distances.junction`
|
|
219
|
+
and imported, so the coordinate constant cannot drift.
|
|
220
|
+
- `cohort.cluster_samples` docstring described itself; `AntigenMetric` and `mir.bench.eval` gained
|
|
221
|
+
the docstrings/module-map entries they were missing.
|
|
222
|
+
|
|
223
|
+
## 3.3.0 — 2026-07-17
|
|
224
|
+
|
|
225
|
+
Minor: one new public parameter, nothing removed or changed.
|
|
226
|
+
|
|
227
|
+
### Added
|
|
228
|
+
|
|
229
|
+
- **`fit_density_space(chunk_size=)`** — embed and project in batches so the full raw matrix is never
|
|
230
|
+
resident. Peak memory becomes `max(pca_fit_cap, chunk_size) × n_features` instead of
|
|
231
|
+
`len(df) × n_features`: measured **10.60 GB → 1.81 GB** at 450k pooled clonotypes, and flat in `N`
|
|
232
|
+
(vs linear), at no wall-clock cost. This is what makes whole-cohort density arms runnable on a
|
|
233
|
+
laptop — the 4.2M-clonotype pooled arm is ~51 GB raw and ~102 GB once `scaler.transform` upcasts to
|
|
234
|
+
float64. Chunking is bit-exact at the embedding level (`_embed` of a slice == the slice of
|
|
235
|
+
`_embed`); the projected coordinates agree to float noise (~1e-7 relative), since BLAS summation
|
|
236
|
+
order depends on batch shape.
|
|
237
|
+
|
|
238
|
+
### Fixed
|
|
239
|
+
|
|
240
|
+
- `fit_density_space`'s `pca_fit_cap` docstring claimed it "lets whole repertoires be embedded without
|
|
241
|
+
a full-matrix PCA". It caps the **fit**, not the memory — both raw matrices were already
|
|
242
|
+
materialized before the PCA was fitted. Documented, and `chunk_size=` is the actual remedy.
|
|
243
|
+
- **`mir.__version__` was stale** — it read `3.1.1` on the published 3.2.0, because the release bump
|
|
244
|
+
moved `pyproject.toml` but not `mir/__init__.py`, and `publish.yml` only validates *pyproject ==
|
|
245
|
+
tag*. Both now read 3.3.0. (`__version__` is still hand-maintained; deriving it from
|
|
246
|
+
`importlib.metadata` would retire this failure mode for good.)
|
|
247
|
+
- `tests/assets/olga_humanTRB_1000.txt.gz` was a slice of the alphabetically sorted VDJdb TRB dump,
|
|
248
|
+
not OLGA output as its name and `SOURCES.md` claimed — so it was **not** an antigen-naive null (12%
|
|
249
|
+
of rows had a Hamming-1 neighbour vs 0.2% for real OLGA). Regenerated from `olga-generate_sequences`;
|
|
250
|
+
provenance and a byte-reproducible regenerate command recorded in `SOURCES.md`. No test was
|
|
251
|
+
invalidated (they use it only as a generic TRB junction pool), but external calibrations that treated
|
|
252
|
+
it as a synthetic negative control were comparing VDJdb against itself. Not shipped (tests are
|
|
253
|
+
excluded from the sdist); listed here because it invalidates results, not code.
|
|
254
|
+
|
|
255
|
+
## 3.2.0 — 2026-07-17
|
|
256
|
+
|
|
257
|
+
Minor: one new public module, nothing removed or changed.
|
|
258
|
+
|
|
259
|
+
### Added
|
|
260
|
+
|
|
261
|
+
- **`mir.explain`** (T7) — explainable readouts over any repertoire feature matrix.
|
|
262
|
+
`ChannelSpec` / `ChannelBuilder` / `stack_embeddings` attach the name→column map that `Φ.vector`
|
|
263
|
+
does not carry (`stack_embeddings` is exact: `X[i] == embs[i].vector`, names only, no transform);
|
|
264
|
+
`channel_report` ablates each named channel under a caller-supplied scorer (leave-one-in by
|
|
265
|
+
default; `mode="both"` adds the conditional half that exposes *redundant* channels — high `delta`,
|
|
266
|
+
`delta_out≈0`; optional row-permutation p-values); `channel_drivers` hops from a winning
|
|
267
|
+
**kernel-mean** channel to the clonotypes driving it via `class_witness`, and refuses channels with
|
|
268
|
+
no clonotype pre-image (a Hill number's "drivers" are a category error, not an open question).
|
|
269
|
+
Scorer-agnostic by design — the library never sees the labels and ships no scorers, so a Cox
|
|
270
|
+
C-index and a CV AUC both plug in. **No existing module changed.**
|
|
271
|
+
|
|
272
|
+
## 3.1.1 — 2026-07-14
|
|
273
|
+
|
|
274
|
+
Maintenance re-release of 3.1.0 with no functional changes — 3.1.0 was withdrawn from PyPI, and
|
|
275
|
+
this version restores the package under a fresh, clean version. The API, coordinates, and behaviour
|
|
276
|
+
are identical to 3.1.0 (see below).
|
|
277
|
+
|
|
278
|
+
## 3.1.0 — 2026-07-14
|
|
279
|
+
|
|
280
|
+
The Part-2 feature tier on top of the 3.0 consolidated embedding core: neural codecs, continuous-density
|
|
281
|
+
methods, and the sample-level (repertoire) embedding, all on arda-native coordinates.
|
|
282
|
+
|
|
283
|
+
### Added
|
|
284
|
+
- **`mir.density`** (T6) — graph-free continuous-density TCRNET/ALICE: balloon adaptive-radius enrichment
|
|
285
|
+
(Poisson/binomial + BH q, water-level calibration), abundance-aware weighted mass, backends
|
|
286
|
+
`exact`(BallTree) / `kdtree`(multicore) / `ann`(pynndescent).
|
|
287
|
+
- **`mir.repertoire`** (T7) — sample-level embedding `Φ(S)` = RFF kernel mean ‖ coverage-standardized Hill ‖
|
|
288
|
+
second-moment Fisher; `mmd_distance`/`mmd_matrix` (now with **`unbiased=True`** diagonal-removed MMD²),
|
|
289
|
+
`hla_stratified_mmd`, `class_witness` motif finder.
|
|
290
|
+
- **`mir.ml`** (Part 2, `[ml]` extra) — forward/inverse/pgen/unified neural codecs + `CodecBundle`
|
|
291
|
+
(prototype-hash-verified shipping); learned repertoire track `set_encoder` (Set-Transformer/DeepRC).
|
|
292
|
+
- **Bench** — clustering `method=` (dbscan/hdbscan/optics); `bench.theory` T6 `tcrnet_convergence`,
|
|
293
|
+
`codec_losslessness`.
|
|
294
|
+
- **Repertoire benchmarks** — `experiments/benchmark_repertoire_{aging,depth,cmvhla,hla,yfv,spikein,
|
|
295
|
+
agediverge}.py` and the COVID cohort suite `{covidbatch,covidhla,covidstatus,covidpaired}.py`
|
|
296
|
+
(`airr_covid19`, local-first + HF fallback). Recorded baselines in `BENCHMARKS.md`.
|
|
297
|
+
|
|
298
|
+
### Changed
|
|
299
|
+
- **Coordinate system re-pinned to arda-native** germline + real-repertoire prototypes (versioned; any model
|
|
300
|
+
trained on the old coords must be retrained).
|
|
301
|
+
- **`mir.ml` device selection** now **CUDA → MPS → CPU** (was MPS-only) with a `MIR_DEVICE` env override and
|
|
302
|
+
CUDA seeding — GPU support beyond Apple silicon.
|
|
303
|
+
- Documented all parallelism knobs (README "Performance & parallelism"): `TCREmp(threads=0)` all-core,
|
|
304
|
+
density `backend="kdtree"` multicore-exact, `cluster(n_jobs=…)`, BLAS env.
|
|
305
|
+
|
|
306
|
+
### Fixed
|
|
307
|
+
- **Unbiased repertoire MMD** — the biased V-statistic's `1/n_eff` self-term inflated low-diversity samples and
|
|
308
|
+
faked a divergence signal; `unbiased=True` removes it. Aging-divergence re-evaluated at depth: real but
|
|
309
|
+
diversity-coupled, not an independent axis.
|
|
310
|
+
|
|
311
|
+
### Notes
|
|
312
|
+
- New lessons for the theory appendix in `REPERTOIRE_LESSONS.md`; full findings in `THEORY.md` T7.
|
|
313
|
+
- Pure-Python `py3-none-any` wheel; native code comes from `seqtree`/`vdjtools` wheels.
|
|
314
|
+
|
|
315
|
+
## 3.0.0 — greenfield v3 embedding core (unreleased)
|
|
316
|
+
- Prototype (TCREMP) embedding on `seqtree.gapblock` + baked arda germline distances; vdjtools reuse (no AIRR
|
|
317
|
+
data-model layer of its own); bench harness (VDJdb Table S1); theory scaffold (`THEORY.md` S1–S3, T1–T5).
|
|
@@ -114,8 +114,28 @@ fix were added to `vdjtools` under the owner's direction (this is that owner's e
|
|
|
114
114
|
high `delta` + `delta_out≈0` is the **redundancy** signature. `channel_drivers` hops channel→clonotypes
|
|
115
115
|
via `class_witness`, but **only** for a channel declared `attributable` (a kernel mean); a Hill number
|
|
116
116
|
has no clonotype pre-image and it raises. Depends one-way on `repertoire.py`; nothing there changed.
|
|
117
|
+
- `track.py` — **exposure trajectory** (repertoire-level exposure detection, complementing
|
|
118
|
+
`density.py`'s clone-level TCRNET/ALICE): `fit_exposure_trajectory(X, covariates, ...)`, a
|
|
119
|
+
PhenoPath-style (Campbell & Yau 2018) covariate-disentangled latent factor model —
|
|
120
|
+
`Y[n,g] = c_g + alpha_g.x_n + (kappa_g + gamma_g.x_n)*tau_n + eps` — fit by alternating closed-form
|
|
121
|
+
per-channel ridge regression and a GLS trajectory update (a simplified, non-CAVI approximation to
|
|
122
|
+
PhenoPath's inference; ARD-style iteratively-reweighted shrinkage on `gamma`, the covariate x
|
|
123
|
+
trajectory interaction). `TrajectoryFit.top_interactions()` ranks channels. Fills the
|
|
124
|
+
analysis-repo ROADMAP's Phase 5 (embedding trajectory). Torch-free.
|
|
125
|
+
- `generate.py` — the **generative loop, mechanical half** (ROADMAP Phase 2): `DescriptorDensity`
|
|
126
|
+
(optionally class-conditional Gaussian, Ledoit-Wolf shrinkage) over `RepertoireDescriptor` vectors;
|
|
127
|
+
`sample` draws new synthetic donor states, `evolve` perturbs one coordinate and propagates the
|
|
128
|
+
coupled shift via the fitted covariance's conditional mean — promotes the ad-hoc
|
|
129
|
+
`benchmark_repertoire_tcga_insilico.py` `np.cov`-slope pattern into a reusable library object.
|
|
130
|
+
Torch-free.
|
|
131
|
+
- `twin.py` — the **digital twin**: `DonorTwin`/`make_twins` glue one donor's `RepertoireDescriptor`
|
|
132
|
+
+ an optional `track.py` trajectory position + covariate into one object; `.perturb()` (via
|
|
133
|
+
`generate.evolve`) and `.simulate()` (via `generate.DescriptorDensity` or `ml.diffusion.DiffusionModel`
|
|
134
|
+
— both share the `sample(n, condition=, seed=)` shape, so either drops in). Torch-free itself.
|
|
117
135
|
- `ml/` — Part 2 (torch), neural codecs + `set_encoder.py` (learned repertoire track: Set-Transformer/DeepRC
|
|
118
|
-
attention pooling + `SetEncoderBundle`).
|
|
136
|
+
attention pooling + `SetEncoderBundle`) + `diffusion.py` (**generative loop, research half**: compact
|
|
137
|
+
conditional DDPM/DDIM with classifier-free guidance over a compact descriptor/code space, standardized
|
|
138
|
+
internally with `x0`-clipping for DDIM stability; `DiffusionModel.save`/`load` mirrors `CodecBundle`).
|
|
119
139
|
- `cli.py` — the `mir` console script (argparse, stdlib): `embed clonotypes` (TCREmp table) /
|
|
120
140
|
`embed repertoires` (per-locus `fit_repertoire_space`→`sample_embedding` Φ(S), optional `--mmd`).
|
|
121
141
|
Reads via `vdjtools.io.read`; writes TSV/parquet.
|
|
@@ -155,9 +175,18 @@ fix were added to `vdjtools` under the owner's direction (this is that owner's e
|
|
|
155
175
|
cleanup) — the "vdjtools at the embedding level" audit +
|
|
156
176
|
plan (three verbs: make / measure / generate-decode). **Phase 0** (robustness + optimization quick wins)
|
|
157
177
|
and **Phase 1** (cohort tier: `bench/eval.py`, `repertoire.{fit_repertoire_spaces,centroid_atypicality}`,
|
|
158
|
-
`cohort.py` digital donor) are **DONE**.
|
|
159
|
-
|
|
160
|
-
|
|
178
|
+
`cohort.py` digital donor) are **DONE**. **Phase 2 (generative loop, mechanical half) — DONE**:
|
|
179
|
+
`generate.py` `DescriptorDensity` (`sample`/`evolve`) ships, promoting the ad-hoc
|
|
180
|
+
`benchmark_repertoire_tcga_insilico.py` `np.cov`-slope pattern; `CodecBundle.from_unified/from_decoder`
|
|
181
|
+
is still open. **Phase 5 (embedding trajectory) — partially addressed**: `track.py`
|
|
182
|
+
`fit_exposure_trajectory` (a PhenoPath-style covariate-disentangled latent trajectory, Campbell &
|
|
183
|
+
Yau 2018) fills this rather than the originally-sketched `track.repertoire_trajectory`
|
|
184
|
+
Φ-velocity idea — a different, arguably more principled approach to the same phase; `clonotype_flux`
|
|
185
|
+
(differential enrichment) is still open. Also new: `mir.ml.diffusion` (generative loop, **research
|
|
186
|
+
half** — conditional DDPM/DDIM, complementing the linear `DescriptorDensity`) and `twin.py`
|
|
187
|
+
(`DonorTwin`/`make_twins` — glues descriptor + trajectory + generator into one perturb/simulate
|
|
188
|
+
object). Next: Phase 3 (embedding inversion / generation), Phase 4 (multimodal encoders).
|
|
189
|
+
**Analysis-repo follow-up:** refactor `_tcga_embedding.build_embedding` onto
|
|
161
190
|
`cohort.fit_donor_embeddings` (+ `extra_channels` for isotype/composition/atypicality) and re-verify the
|
|
162
191
|
pan-cancer ΔC numbers. NB Phase 0 flipped the density default to `backend="kdtree"` — re-verify any
|
|
163
192
|
recorded balloon-mode baselines (±1 boundary counts).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mirpy-lib
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.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 |
|
|
@@ -179,7 +228,10 @@ Xr = pca_denoise(X, n_components=p.n_components_recon) # codec reconstruction
|
|
|
179
228
|
| `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
229
|
| `mir.explain` | named-channel fusion (`ChannelBuilder`) + scorer-agnostic ablation (`channel_report`/`channel_drivers`) — which part of Φ carries the signal (§T.7) |
|
|
181
230
|
| `mir.cohort` | the **digital donor** — multi-chain `fit_donor_embeddings`/`DonorCohort` (+ `transform`/`save`/`load`) + `residualize` / `cluster_samples` / `incidence_biomarkers` (§T.7) |
|
|
182
|
-
| `mir.
|
|
231
|
+
| `mir.track` | **exposure trajectory** — PhenoPath-style covariate-disentangled latent progression axis (`fit_exposure_trajectory`) over any channel matrix; repertoire-level exposure detection |
|
|
232
|
+
| `mir.generate` | the **generative loop** (mechanical half) — `DescriptorDensity`: sample new synthetic donor states / `evolve` one along a coordinate, over `RepertoireDescriptor` |
|
|
233
|
+
| `mir.twin` | the **digital twin** — `DonorTwin`/`make_twins`: perturb or resample one donor's state through a `mir.generate`/`mir.ml.diffusion` generator |
|
|
234
|
+
| `mir.ml` | neural codecs (forward/inverse/Pgen/unified) + learned repertoire `set_encoder` (Set-Transformer/DeepRC) + conditional **diffusion generator** (`mir.ml.diffusion`, the generative loop's research half) — Part 2, experimental; `[ml]` extra |
|
|
183
235
|
|
|
184
236
|
## Background subtraction & clustering (`mir.density`)
|
|
185
237
|
|
|
@@ -203,24 +255,32 @@ labels, mask = denoise_and_cluster(obs_emb, res) # noise-filter +
|
|
|
203
255
|
|
|
204
256
|
Use a **biological control** as the background when you have one (e.g. pre- vs post-vaccination,
|
|
205
257
|
patient vs healthy) — differential enrichment cancels generic public convergence and isolates the
|
|
206
|
-
antigen-specific response.
|
|
207
|
-
|
|
208
|
-
|
|
258
|
+
antigen-specific response. No control of your own? Pooled healthy-donor repertoires are one fetch
|
|
259
|
+
away (HF `isalgo/airr_control`, read with `vdjtools.io.read` — see [`SOURCES.md`](SOURCES.md)).
|
|
260
|
+
Failing that, `generate_background(locus, n)` samples the vdjtools P_gen model (the ALICE regime);
|
|
261
|
+
the "water level" of a naive repertoire is handled by the empirical-null calibration. Pass
|
|
262
|
+
`source="arda"` there when your data is arda-annotated (same allele namespace as the prototypes),
|
|
263
|
+
and `species="mouse"` for mouse — both need a vdjtools shipping the bundled `arda` model set. The density benchmarks (YFV, ankylosing-spondylitis B27, TCRNET)
|
|
209
264
|
live in the companion [`2026-mirpy-analysis`](https://github.com/antigenomics) repo.
|
|
210
265
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
266
|
+
The default backend is `"kdtree"` (exact scipy cKDTree, all cores). At whole-repertoire scale pass
|
|
267
|
+
`backend="ann"` (approximate pynndescent, ~30× faster past ~10⁵ clones, trading a small conservative
|
|
268
|
+
undercount; `pip install "mirpy-lib[ann]"`).
|
|
214
269
|
|
|
215
270
|
## Sample-level (repertoire) embedding (`mir.repertoire`)
|
|
216
271
|
|
|
217
272
|
One fixed vector `Φ(S)` per **repertoire** — an order-invariant multiset of clonotypes with clone
|
|
218
273
|
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
|
-
|
|
274
|
+
empirical measure `ρ_S = Σ_σ w_σ δ_{φ(σ)}` in three blocks: an RFF **kernel mean** (depth-robust,
|
|
275
|
+
codebook-free — no `K`, no clustering), a coverage-standardized **Hill diversity** profile, and a
|
|
276
|
+
**second-moment** Fisher vector carrying clonotype co-occurrence (HLA-linked public structure).
|
|
277
|
+
Repertoire distance is the **MMD** `‖Φ₁(S) − Φ₁(S')‖`.
|
|
278
|
+
|
|
279
|
+
The per-clonotype weights `w_σ = g(a_σ)/Σ_τ g(a_τ)` come from a clone-size transform `g` (`weight=`
|
|
280
|
+
on `sample_embedding`/`fit_repertoire_space`/`mir embed repertoires --weight`): `"log2p1"` —
|
|
281
|
+
`g=log2(1+a)` — is the **default**, concave so one hyperexpanded clone can't dominate;
|
|
282
|
+
`"duplicate_count"` weights linearly by clone size (`g=a`); `"distinct"` ignores size entirely
|
|
283
|
+
(`g≡1`, presence only). `"log1p"` (natural log) and `"anscombe"` remain available.
|
|
224
284
|
|
|
225
285
|
```python
|
|
226
286
|
from mir.repertoire import fit_repertoire_space, sample_embedding, mmd_matrix, class_witness
|
|
@@ -249,6 +309,47 @@ TRA and class II) lives in the second moment / witness. A learned co-equal set e
|
|
|
249
309
|
(T7) live in the companion [`2026-mirpy-analysis`](https://github.com/antigenomics) repo
|
|
250
310
|
(`benchmarks/{BENCHMARKS,THEORY}.md`) alongside the benchmark scripts.
|
|
251
311
|
|
|
312
|
+
## Exposure trajectory, generative loop, digital twin
|
|
313
|
+
|
|
314
|
+
A repertoire cohort often has a **known covariate** (HLA, batch, vaccine arm) but an **unknown or
|
|
315
|
+
noisy progression axis** (days since exposure, response severity). `mir.track.fit_exposure_trajectory`
|
|
316
|
+
recovers that latent trajectory `tau` while disentangling it from the covariate — a PhenoPath-style
|
|
317
|
+
model (Campbell & Yau 2018, *Nat. Commun.*
|
|
318
|
+
[10.1038/s41467-018-04696-6](https://doi.org/10.1038/s41467-018-04696-6), adapted from genes×cells to
|
|
319
|
+
repertoire-channels×samples) over *any* per-sample channel matrix (a stacked `Φ`, a `ChannelBuilder`
|
|
320
|
+
build, or a raw embedding block):
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
from mir.explain import stack_embeddings
|
|
324
|
+
from mir.track import fit_exposure_trajectory
|
|
325
|
+
|
|
326
|
+
X, spec = stack_embeddings(embs) # channels: mean ‖ diversity ‖ second
|
|
327
|
+
fit = fit_exposure_trajectory(X, hla_indicator, channel_names=spec.names)
|
|
328
|
+
fit.tau # inferred exposure/progression pseudotime, one per sample
|
|
329
|
+
fit.top_interactions(5) # which channels respond to progression differently by covariate
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Complementing that, `mir.generate.DescriptorDensity` fits a (optionally class-conditional) density
|
|
333
|
+
over `RepertoireDescriptor` vectors — `sample` draws brand-new synthetic donor states, `evolve`
|
|
334
|
+
perturbs one donor along a coordinate ("what if hotter") and propagates the coupled shift through
|
|
335
|
+
every other coordinate via the fitted covariance's conditional mean. `mir.ml.diffusion` (`[ml]`
|
|
336
|
+
extra) is the non-linear alternative — a compact conditional DDPM/DDIM generator with
|
|
337
|
+
classifier-free guidance, sharing the same `sample(n, condition=…)` call shape so it drops in
|
|
338
|
+
unchanged. `mir.twin.DonorTwin` glues a donor's descriptor + trajectory position + covariate into
|
|
339
|
+
one object you perturb or resample through, instead of threading the three APIs together by hand:
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
from mir.generate import fit_descriptor_density
|
|
343
|
+
from mir.twin import make_twins
|
|
344
|
+
|
|
345
|
+
density = fit_descriptor_density(descriptors, labels=tumor_type)
|
|
346
|
+
twins = make_twins(descriptors, conditions=tumor_type, donor_ids=sample_ids)
|
|
347
|
+
hotter = twins[0].perturb(density, coordinate="infiltration", delta=2.0)
|
|
348
|
+
synthetic = twins[0].simulate(density, n=20) # 20 new synthetic peers of donor 0
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
See `examples/trajectory_and_twin.py` for a runnable end-to-end demo.
|
|
352
|
+
|
|
252
353
|
## Reproduce the paper
|
|
253
354
|
|
|
254
355
|
The self-contained theory notebooks run on bundled data:
|
|
@@ -256,7 +357,7 @@ The self-contained theory notebooks run on bundled data:
|
|
|
256
357
|
```bash
|
|
257
358
|
pip install "mirpy-lib[examples]"
|
|
258
359
|
marimo edit examples/theory.py # supplementary S1–S3 (distance laws, D↔d, prototype robustness)
|
|
259
|
-
marimo edit examples/quickstart.py # embed + cluster VDJdb
|
|
360
|
+
marimo edit examples/quickstart.py # embed + cluster (epitope colours need a local VDJdb dump)
|
|
260
361
|
```
|
|
261
362
|
|
|
262
363
|
The full benchmark suite (VDJdb Table S1, density, repertoire/TCGA) and result docs live in the
|
|
@@ -273,13 +374,14 @@ mirpy is CPU-parallel by default and uses the GPU for the neural codecs. Knobs,
|
|
|
273
374
|
| Stage | Knob | Default | Notes |
|
|
274
375
|
|---|---|---|---|
|
|
275
376
|
| 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=…)` | `"
|
|
377
|
+
| 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
378
|
| Clustering | `cluster(..., n_jobs=-1)` | sklearn default (1) | forwarded to DBSCAN/OPTICS/HDBSCAN via `**kwargs`; parallelizes the neighbour search. |
|
|
278
379
|
| 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
380
|
| 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
381
|
|
|
281
|
-
Rule of thumb: leave `threads=0` (all cores) for embedding;
|
|
282
|
-
|
|
382
|
+
Rule of thumb: leave `threads=0` (all cores) for embedding; leave density on the default
|
|
383
|
+
`backend="kdtree"` (exact, multicore) and switch to `"ann"` only at whole-repertoire scale; the GPU
|
|
384
|
+
is used only by `mir.ml`.
|
|
283
385
|
|
|
284
386
|
## Development
|
|
285
387
|
|