mirpy-lib 3.3.0__tar.gz → 3.6.0__tar.gz

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