33Notable changes to vdjtools v2. Releases before 3.0.0 are recorded in the git tags
44(` v2.5.0 ` … ` v2.9.0 ` ) and their commit history.
55
6+ ## 3.4.0 — 2026-08-12
7+
8+ Follows 3.3.0's model workshop with the defects that workshop then found, and retrains every
9+ bundled model.
10+
11+ ### Added
12+
13+ - ** Live EM progress, checkpointing and exact resume.** ` infer ` /` infer_native ` take
14+ ` progress=callable(iter, loglik, rel_change, n_scoreable) ` — ` infer.print_progress() ` is a
15+ ready-made one — so a long fit is visibly converging rather than merely running; the relative
16+ change it reports is exactly what is compared against ` tol ` . They also take
17+ ` checkpoint=DIR ` /` checkpoint_every=N ` , saving the model after each iteration (written to a
18+ sibling directory and swapped in, so a kill mid-write leaves the previous checkpoint loadable),
19+ and ` infer.resume(DIR, seqs) ` continues from one. ** Resuming is exact** : 3 iterations plus a
20+ resumed 4 give the same log-likelihood * and* the same tables as an uninterrupted 7, and the
21+ training log spans every attempt. Exposed as
22+ ` vdjtools model learn -v --checkpoint DIR --resume DIR ` ; ` vdjtools model build -v ` additionally
23+ stops swallowing arda's mapping output. This matters because IGH's EM enumerates ~ 1,225 D pairs
24+ per read against TRB's 9 — 12 minutes per iteration on a 112-core node, and more than 110 minutes
25+ without finishing one on a laptop.
26+
27+ ### Changed
28+
29+ - ** All seven bundled ` learned ` models retrained** on the full non-functional read corpus (every
30+ available read; out-of-frame * and* stop-codon, since both escaped selection). Every one converged
31+ on tolerance with a step-by-step monotone log-likelihood, and ` check_model ` reports zero errors:
32+
33+ | locus | clonotypes | iters | log-likelihood |
34+ | ---| ---| ---| ---|
35+ | TRA | 34,238 | 10 | −21.52 → −20.01 |
36+ | TRB | 122,703 | 11 | −37.01 → −33.76 |
37+ | TRG | 14,305 | 8 | −21.90 → −20.33 |
38+ | TRD | 10,915 | 7 | −40.94 → −35.79 |
39+ | IGH | 141,607 | 9 | −57.16 → −49.88 |
40+ | IGK | 256,347 | 6 | −16.18 → −14.28 |
41+ | IGL | 23,469 | 12 | −6.63 → −6.00 |
42+
43+ Each now ships its training log, so a model states what it was fitted on. IGH was trained on a
44+ 112-core cluster node; the rest fit comfortably on a laptop.
45+ - ** Ambiguous junction bases are substituted, not dropped.** They previously crashed EM with a bare
46+ ` KeyError ` from inside the native encoder. Both training entry points now substitute ` A ` by
47+ default and warn with the count (` ambiguous=None ` drops instead) — it affects ~ 0.01% of these
48+ reads, so dropping cost sample size for nothing. It is a substitution, not a marginalization.
49+
50+ ### Fixed
51+
52+ - ** ` collapse_alleles ` could give a gene a germline it could not use — in the default path.**
53+ ` load_bundled(..., collapse=True) ` picks one representative allele per gene and averages the
54+ other alleles' conditionals onto it. The representative was chosen by usage alone, but IMGT ships
55+ some alleles with a ** truncated** CDR3-region germline: human ` IGKV3-20*02 ` is 11 nt against
56+ ` *01 ` 's 30 and carried the higher learned usage, so it became the gene's germline — relabelled
57+ ` *01 ` , which was doubly misleading — and 25% of the gene's own averaged deletion distribution
58+ landed on trims the 11-nt germline cannot reach. The Pgen DP never visits those, so that quarter
59+ of the probability vanished from every Pgen through IGKV3-20 instead of being redistributed.
60+ The representative is now chosen by ** germline length first, usage second** (alleles of a gene are
61+ near-identical through the CDR3 region, so a large length gap means an incomplete database entry),
62+ and the collapsed deletion conditionals are ** projected onto the representative's reachable
63+ support and renormalized** . Every bundled model is now clean at ` collapse=True ` .
64+ - ** A failed ` arda ` run reported nothing but an exit code.** ` annotate_reads ` passed
65+ ` capture_output=True ` and let ` CalledProcessError ` propagate, so arda's own message was swallowed
66+ — precisely how the CLI rename in 3.3.0 stayed invisible. It now raises with arda's stderr and the
67+ installed ` arda-mapper ` version.
68+ - ` reference.read_fasta ` wraps arda's FASTA parser with gzip support; arda's opens with plain
69+ ` open() ` , so a ` .gz ` reached it as mojibake and died on the first byte.
70+
71+ ### Documented
72+
73+ - ** Known quirks of the OLGA models** (` docs/model.rst ` ). The bundled ` olga ` set is a bit-faithful
74+ import, and that includes its defects. Verified against OLGA's raw ` model_marginals.txt ` with
75+ OLGA's own parser: deletion mass on unreachable trims is ** OLGA's** , with identical fractions to
76+ 4 dp (` IGHV4-30-4*01 ` 100% — its Pgen is identically zero in OLGA too — ` IGKJ4*02 ` 80.9%,
77+ ` TRAV20*03 ` 54.7%), and our Pgen matches olga-pip exactly (ratio 1.000000) on every sequence OLGA
78+ will score. Correcting it would break the exact-OLGA-Pgen invariant, so ` check_model ` reports it
79+ at ` warn ` rather than ` error ` for an OLGA-sourced model. Also covers the empty-germline ORF genes,
80+ protocol-specific V/J usage, and OLGA's refusal of out-of-frame input.
81+
682## 3.3.0 — 2026-08-12
783
884The recombination model becomes a workshop: buildable on your own reference, checkable, comparable,
@@ -61,19 +137,6 @@ scoreable and extendable. See the new [user guide](https://docs.isalgo.dev/vdjto
61137- ** Table export/import** : ` marginals_frame ` / ` set_marginals ` , and ` save_model(..., fmt="tsv") `
62138 with format auto-detection on load, so a hand-edited TSV directory is a first-class model input.
63139
64- - ** Live EM progress, checkpointing and resume.** ` infer ` /` infer_native ` take
65- ` progress=callable(iter, loglik, rel_change, n_scoreable) ` — ` infer.print_progress() ` is a
66- ready-made one — so a long fit is visibly converging rather than merely running; the relative
67- change it reports is the exact quantity compared against ` tol ` . They also take
68- ` checkpoint=DIR ` /` checkpoint_every=N ` , saving the model after each iteration (swapped into place,
69- so a kill mid-write leaves the previous checkpoint loadable), and ` infer.resume(DIR, seqs) `
70- continues from one. ** Resuming is exact** — 3 iterations plus a resumed 4 give the same
71- log-likelihood * and* the same tables as an uninterrupted 7 — and the training log spans every
72- attempt. Exposed as ` vdjtools model learn -v --checkpoint DIR --resume DIR ` , and
73- ` vdjtools model build -v ` additionally stops swallowing arda's mapping output.
74- This matters because IGH's EM enumerates ~ 1,225 D pairs per read against TRB's 9: 14 minutes per
75- iteration on a 112-core node, and more than 110 minutes for a single iteration on a laptop.
76-
77140### Changed
78141
79142- ** ` pgen_nt ` now releases the GIL** , the one Pgen binding that still held it after the Phase-13
@@ -87,22 +150,6 @@ scoreable and extendable. See the new [user guide](https://docs.isalgo.dev/vdjto
87150
88151### Fixed
89152
90- - ** ` collapse_alleles ` could give a gene a germline it could not use — in the default path.**
91- ` load_bundled(..., collapse=True) ` picks one representative allele per gene and averages the
92- other alleles' conditionals onto it. The representative was chosen by usage alone, but IMGT ships
93- some alleles with a ** truncated** CDR3-region germline: human ` IGKV3-20*02 ` is 11 nt against
94- ` *01 ` 's 30 and carried the higher learned usage, so it became the gene's germline — relabelled
95- ` *01 ` , which was doubly misleading — and 25% of the gene's own averaged deletion distribution
96- landed on trims the 11-nt germline cannot reach. The Pgen DP never visits those, so that quarter
97- of the probability vanished from every Pgen through IGKV3-20 instead of being redistributed.
98- The representative is now chosen by ** germline length first, usage second** (alleles of a gene are
99- near-identical through the CDR3 region, so a large length gap means an incomplete database entry),
100- and the collapsed deletion conditionals are ** projected onto the representative's reachable
101- support and renormalized** . Every bundled model is now clean at ` collapse=True ` .
102- - ** A failed ` arda ` run reported nothing but an exit code.** ` annotate_reads ` passed
103- ` capture_output=True ` and let ` CalledProcessError ` propagate, so arda's own message was swallowed
104- — which is precisely how the CLI rename above stayed invisible. It now raises with arda's stderr
105- and the installed ` arda-mapper ` version.
106153- ** ` annotate_reads ` was calling an arda CLI that no longer exists.** It shelled out to
107154 ` arda rnaseq map -o … ` , but arda 2.19 turned ` rnaseq ` into the full map→assemble→correct preset
108155 with no stage positional and ` -p/--out-prefix ` in place of ` -o ` , so every real invocation exited
0 commit comments