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.
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0}/.gitignore +3 -3
- mirpy_lib-3.6.0/CHANGELOG.md +285 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0}/CLAUDE.md +43 -8
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0}/PKG-INFO +135 -35
- mirpy_lib-3.6.0/README.md +292 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0}/SOURCES.md +11 -9
- mirpy_lib-3.6.0/assets/mirpy_dark.png +0 -0
- mirpy_lib-3.6.0/assets/mirpy_dark.svg +1 -0
- mirpy_lib-3.6.0/assets/mirpy_dark_bg.svg +1519 -0
- mirpy_lib-3.6.0/assets/mirpy_light.png +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0}/assets/mirpy_light.svg +1 -1
- mirpy_lib-3.6.0/assets/mirpy_light_bg.svg +1519 -0
- mirpy_lib-3.6.0/docs/_static/custom.css +9 -0
- mirpy_lib-3.6.0/docs/_static/depth_robustness.png +0 -0
- mirpy_lib-3.6.0/docs/_static/mirpy_dark.png +0 -0
- mirpy_lib-3.6.0/docs/_static/mirpy_dark.svg +1 -0
- mirpy_lib-3.6.0/docs/_static/mirpy_light.png +0 -0
- mirpy_lib-3.3.0/assets/mirpy_dark.svg → mirpy_lib-3.6.0/docs/_static/mirpy_light.svg +1 -1
- mirpy_lib-3.6.0/docs/_static/sample_embedding_diagram.png +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0}/docs/api.rst +51 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0}/docs/conf.py +8 -10
- mirpy_lib-3.6.0/docs/examples.rst +34 -0
- mirpy_lib-3.6.0/docs/index.rst +110 -0
- mirpy_lib-3.6.0/docs/usage.rst +323 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0}/pyproject.toml +22 -15
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0}/requirements.txt +2 -2
- mirpy_lib-3.6.0/setup.sh +107 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/__init__.py +1 -1
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/bench/__init__.py +4 -1
- mirpy_lib-3.6.0/src/mir/bench/eval.py +219 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/bench/metrics.py +11 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/bench/theory.py +3 -2
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/bench/vdjdb.py +3 -1
- mirpy_lib-3.6.0/src/mir/cli.py +249 -0
- mirpy_lib-3.6.0/src/mir/cohort.py +451 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/density.py +88 -24
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/distances/germline.py +21 -3
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/distances/junction.py +18 -7
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/embedding/presets.py +2 -1
- mirpy_lib-3.6.0/src/mir/embedding/prototypes.py +173 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/embedding/tcremp.py +46 -14
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/explain.py +6 -4
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/ml/bundle.py +18 -8
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/ml/set_encoder.py +2 -1
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/repertoire.py +275 -49
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/prototypes/generate_prototypes.py +3 -1
- mirpy_lib-3.3.0/BENCHMARKS.md +0 -264
- mirpy_lib-3.3.0/CHANGELOG.md +0 -100
- mirpy_lib-3.3.0/README.md +0 -189
- mirpy_lib-3.3.0/REPERTOIRE_EMBEDDING.md +0 -186
- mirpy_lib-3.3.0/REPERTOIRE_LESSONS.md +0 -78
- mirpy_lib-3.3.0/SQRT_D_MIGRATION.md +0 -157
- mirpy_lib-3.3.0/THEORY.md +0 -322
- mirpy_lib-3.3.0/assets/mirpy_dark.png +0 -0
- mirpy_lib-3.3.0/assets/mirpy_light.png +0 -0
- mirpy_lib-3.3.0/docs/index.rst +0 -68
- mirpy_lib-3.3.0/docs/requirements.txt +0 -3
- mirpy_lib-3.3.0/docs/usage.rst +0 -154
- mirpy_lib-3.3.0/environment.yml +0 -15
- mirpy_lib-3.3.0/mir/embedding/prototypes.py +0 -108
- mirpy_lib-3.3.0/setup.sh +0 -90
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0}/LICENSE +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0}/docs/Makefile +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/aliases.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/alleles.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/distances/__init__.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/embedding/__init__.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/embedding/pca.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/ml/__init__.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/ml/codec.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/ml/decoder.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/ml/encoder.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/ml/tokenize.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/ml/train.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/py.typed +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/__init__.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/gene_library/README.md +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/gene_library/__init__.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/gene_library/build_gene_library.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/gene_library/build_region_annotations.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/gene_library/imgt_gene_library.txt +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/gene_library/olga_gene_library.txt +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/gene_library/region_annotations.txt +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/build_germline_dist.py +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/human_IGH.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/human_IGK.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/human_IGL.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/human_TRA.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/human_TRB.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/human_TRD.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/human_TRG.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/mouse_IGH.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/mouse_IGK.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/mouse_IGL.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/mouse_TRA.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/mouse_TRB.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/mouse_TRD.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/germline_dist/mouse_TRG.npz +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/prototypes/.gitkeep +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/prototypes/human_IGH.tsv +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/prototypes/human_IGK.tsv +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/prototypes/human_IGL.tsv +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/prototypes/human_TRA.tsv +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/prototypes/human_TRB.tsv +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/prototypes/human_TRD.tsv +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/prototypes/human_TRG.tsv +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/prototypes/manifest.json +0 -0
- {mirpy_lib-3.3.0 → mirpy_lib-3.6.0/src}/mir/resources/prototypes/mouse_TRA.tsv +0 -0
- {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
|
-
|
|
13
|
+
examples/assets/
|
|
14
14
|
|
|
15
15
|
## VSCode
|
|
16
16
|
|
|
@@ -184,10 +184,10 @@ cython_debug/
|
|
|
184
184
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
185
185
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
186
186
|
#.idea/
|
|
187
|
-
mir/docs
|
|
187
|
+
src/mir/docs
|
|
188
188
|
.idea
|
|
189
189
|
public
|
|
190
|
-
|
|
190
|
+
examples/assets/derived/
|
|
191
191
|
|
|
192
192
|
# ML run artifacts (checkpoints/metrics) — never committed
|
|
193
193
|
experiments/runs/
|
|
@@ -0,0 +1,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):
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
- **`~/vcs/code/mirpy`** (here): **src-layout** — the library is `src/mir/` (import `mir`); unit +
|
|
12
|
+
fast/slow CI tests (`tests/`, on slim bundled data), runnable `examples/` (marimo), docs. As of the
|
|
13
|
+
3.4.x cleanup the result/plan markdown moved out: **`THEORY.md` → the manuscript repo**;
|
|
14
|
+
**`BENCHMARKS.md`, `REPERTOIRE_{EMBEDDING,LESSONS}.md`, `SQRT_D_MIGRATION.md`, `ROADMAP.md` →
|
|
15
|
+
`2026-mirpy-analysis/benchmarks/`**. Only `README`/`CHANGELOG`/`CLAUDE`/`SOURCES` stay at the root.
|
|
16
|
+
Benchmark *scripts* and the theory *appendix* had already moved out (below).
|
|
14
17
|
- **`~/vcs/projects/2026-mirpy-analysis`**: all **benchmark scripts** (`benchmarks/` — local + aldan3),
|
|
15
18
|
full result docs (BENCHMARKS/THEORY/…), figures, run outputs, dataset catalog. Run the mirpy library
|
|
16
19
|
from here; refresh numbers-of-record here.
|
|
@@ -53,7 +56,7 @@ negative S2) — so prototypes use real reads, not model generation.
|
|
|
53
56
|
mirpy is normally read-only to the sibling repos; the `from_arda` builder + a tandem-D generation
|
|
54
57
|
fix were added to `vdjtools` under the owner's direction (this is that owner's ecosystem).
|
|
55
58
|
|
|
56
|
-
## Layout (`mir/`)
|
|
59
|
+
## Layout (`src/mir/`)
|
|
57
60
|
- `aliases.py`, `alleles.py` — species/locus + allele normalization.
|
|
58
61
|
- `distances/junction.py` — `junction_distance_matrix` (seqtree.gapblock). Coordinate knobs (all
|
|
59
62
|
default to the published space, threaded through `TCREmp`): `metric="squared"` (=`d`, default) |
|
|
@@ -88,6 +91,20 @@ fix were added to `vdjtools` under the owner's direction (this is that owner's e
|
|
|
88
91
|
Opt-in `fit_repertoire_space(n_eigs=r)` swaps the second-moment block's full `D₂(D₂+1)/2` upper-triangle
|
|
89
92
|
for its top-`r` eigenvalues (rotation-invariant spectrum; default `None` = upper-tri, unchanged — but
|
|
90
93
|
lossy for the *directional* HLA imprint, so the full triangle stays the recommended block; `benchmark_repertoire_spectral.py`).
|
|
94
|
+
Also `fit_repertoire_spaces` (one basis **per locus** — the multi-chain fit), `centroid_atypicality`
|
|
95
|
+
(per-sample cosine distance to a group centroid — a Φ-geometry op feeding the digital-donor atypicality channel),
|
|
96
|
+
and `correct_batch` (Harmony-like cluster-aware batch correction on a stacked Φ matrix; reduces to
|
|
97
|
+
`cohort.residualize` at `n_clusters=1`/`theta=0` — `prop:batch`; test in `test_repertoire.py`).
|
|
98
|
+
- `cohort.py` — **the digital donor** (T.7): `fit_donor_embeddings`/`DonorCohort` fuse per-chain identity
|
|
99
|
+
(kernel-mean, cross-sample PCA-reduced) ‖ diversity ‖ coverage across loci through **one `ChannelBuilder`**,
|
|
100
|
+
with an `extra_channels` hook for the analysis's own tissue/clinical blocks; comparability bites twice
|
|
101
|
+
(per-locus `prototype_hash` **and** the stored identity PCA) so `save`/`load` verify every hash and
|
|
102
|
+
`transform` is the only held-out path. Plus `residualize` (batch cookbook), `cluster_samples`
|
|
103
|
+
(MMD→precomputed-metric cluster), `incidence_biomarkers` (subject-incidence Fisher, delegating to
|
|
104
|
+
`vdjtools.biomarker.fisher`). Generalizes the analysis-repo `_tcga_embedding.build_embedding` glue.
|
|
105
|
+
- `bench/eval.py` — the scorers `channel_report` consumes (kept out of `explain.py` so it stays scorer-free):
|
|
106
|
+
`cv_auc` / `held_out_auc` (classification), `cv_cindex` / `km_logrank` (Cox survival, `[bench]`→lifelines),
|
|
107
|
+
`kmer_matrix` (baseline).
|
|
91
108
|
- `explain.py` — **explainable readouts over any feature matrix** (T7). `ChannelSpec`/`ChannelBuilder`/
|
|
92
109
|
`stack_embeddings` attach the name→column map `Φ.vector` does not carry (per-chain blocks merge by
|
|
93
110
|
name); `channel_report(X, spec, scorer, base=, mode="in"|"out"|"both", n_permutations=)` ablates each
|
|
@@ -99,14 +116,22 @@ fix were added to `vdjtools` under the owner's direction (this is that owner's e
|
|
|
99
116
|
has no clonotype pre-image and it raises. Depends one-way on `repertoire.py`; nothing there changed.
|
|
100
117
|
- `ml/` — Part 2 (torch), neural codecs + `set_encoder.py` (learned repertoire track: Set-Transformer/DeepRC
|
|
101
118
|
attention pooling + `SetEncoderBundle`).
|
|
119
|
+
- `cli.py` — the `mir` console script (argparse, stdlib): `embed clonotypes` (TCREmp table) /
|
|
120
|
+
`embed repertoires` (per-locus `fit_repertoire_space`→`sample_embedding` Φ(S), optional `--mmd`).
|
|
121
|
+
Reads via `vdjtools.io.read`; writes TSV/parquet.
|
|
102
122
|
- `resources/` — `prototypes/` (TSVs + manifest), `gene_library/` (region_annotations.txt),
|
|
103
123
|
`germline_dist/` (baked `.npz`, from `build_germline_dist.py`).
|
|
104
124
|
|
|
105
125
|
## Build / test / run
|
|
106
|
-
-
|
|
107
|
-
(
|
|
108
|
-
|
|
109
|
-
|
|
126
|
+
- **Repo-local `.venv` via uv** (Python 3.12; conda retired 2026-07-18). `bash setup.sh`
|
|
127
|
+
(bash/zsh; `--dev-parents` editable-installs `../seqtree ../vdjtools ../vdjmatch`, `--docs`,
|
|
128
|
+
`--tests`), or `uv pip install -e ".[dev,bench]"`. Pure-Python hatchling; no C build for `mir`
|
|
129
|
+
itself. Extras: `[bench] [ann] [annotate] [build] [ml] [docs] [dev] [examples]` (`[ann]` =
|
|
130
|
+
pynndescent, split out of `[bench]` so `[bench]` stays numba-free / clean to resolve).
|
|
131
|
+
- CLI: `mir embed clonotypes SAMPLE` (→ clonotype embedding table) / `mir embed repertoires
|
|
132
|
+
SAMPLE… ` (→ per-sample-per-chain Φ(S)); `mir/cli.py`, `tests/test_cli.py`.
|
|
133
|
+
- Tests: `python -m pytest tests/ -q` (`-m "not integration and not benchmark"` for the fast tier;
|
|
134
|
+
torch/pynndescent tests skip unless `[ml]`/`[ann]` installed). All self-contained on bundled resources.
|
|
110
135
|
- Experiments: `python benchmarks/reproduce_supplementary.py` (theory S1–S3),
|
|
111
136
|
`python benchmarks/benchmark_vdjdb.py` (Table S1). Analyses: `analyze_prototype_counts.py`
|
|
112
137
|
(geometry saturates by K≈100 — T.1/S4), `analyze_pc_decomposition.py` (V/J η² ≈0.44/0.49,
|
|
@@ -126,6 +151,16 @@ fix were added to `vdjtools` under the owner's direction (this is that owner's e
|
|
|
126
151
|
GPU only in `mir.ml`: `pick_device()` = **CUDA → MPS → CPU** auto, override `device=`/`MIR_DEVICE`.
|
|
127
152
|
|
|
128
153
|
## Open loops / next steps
|
|
154
|
+
- **Embedding-tier roadmap** (`2026-mirpy-analysis/benchmarks/ROADMAP.md`, moved out in the 3.4.x
|
|
155
|
+
cleanup) — the "vdjtools at the embedding level" audit +
|
|
156
|
+
plan (three verbs: make / measure / generate-decode). **Phase 0** (robustness + optimization quick wins)
|
|
157
|
+
and **Phase 1** (cohort tier: `bench/eval.py`, `repertoire.{fit_repertoire_spaces,centroid_atypicality}`,
|
|
158
|
+
`cohort.py` digital donor) are **DONE**. Next: **Phase 2** generative loop (`generate.py` `DescriptorDensity`
|
|
159
|
+
+ `evolve`/`sample`, `CodecBundle.from_unified/from_decoder`), then multimodal encoders + embedding
|
|
160
|
+
trajectory. **Analysis-repo follow-up:** refactor `_tcga_embedding.build_embedding` onto
|
|
161
|
+
`cohort.fit_donor_embeddings` (+ `extra_channels` for isotype/composition/atypicality) and re-verify the
|
|
162
|
+
pan-cancer ΔC numbers. NB Phase 0 flipped the density default to `backend="kdtree"` — re-verify any
|
|
163
|
+
recorded balloon-mode baselines (±1 boundary counts).
|
|
129
164
|
- **v3.0 remaining**: 10X paired benchmark; docs (Sphinx theory section + notebooks); CI; publish
|
|
130
165
|
`py3-none-any` wheel; regenerate `generate_prototypes.py` via `vdjtools.model.generate`.
|
|
131
166
|
- **Bench tuning**: raw kneedle eps over-merges; `cluster(eps_factor=0.4)` recovers the paper
|