mirpy-lib 3.2.0__tar.gz → 3.4.0__tar.gz

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