seqtree 0.2.0__tar.gz → 0.3.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.
- {seqtree-0.2.0 → seqtree-0.3.0}/.gitignore +5 -0
- seqtree-0.3.0/CHANGELOG.md +190 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/CMakeLists.txt +1 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/PKG-INFO +54 -7
- {seqtree-0.2.0 → seqtree-0.3.0}/README.md +52 -5
- {seqtree-0.2.0 → seqtree-0.3.0}/ROADMAP.md +11 -5
- seqtree-0.3.0/SOURCES.md +97 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/appendix/evalue.pdf +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/appendix/evalue.tex +206 -85
- {seqtree-0.2.0 → seqtree-0.3.0}/appendix/refs.bib +3 -1
- seqtree-0.3.0/examples/01_gapped_search.py +118 -0
- seqtree-0.3.0/examples/02_sequence_dendrogram.py +144 -0
- seqtree-0.3.0/examples/03_indel_positions.py +169 -0
- seqtree-0.3.0/examples/04_island_profile.py +151 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/include/seqtree/seqtree.hpp +18 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/include/seqtree/types.hpp +16 -7
- {seqtree-0.2.0 → seqtree-0.3.0}/pyproject.toml +3 -3
- seqtree-0.3.0/python/seqtree/__init__.py +69 -0
- seqtree-0.3.0/python/seqtree/control.py +185 -0
- seqtree-0.3.0/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
- seqtree-0.3.0/python/seqtree/evalue.py +178 -0
- seqtree-0.3.0/python/seqtree/gapblock.py +673 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/python/seqtree/layout.py +12 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/python/seqtree/pmhc.py +2 -0
- seqtree-0.3.0/python/seqtree/seeds.py +149 -0
- seqtree-0.3.0/skills/seqtree/SKILL.md +218 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/src/_bindings.cpp +83 -5
- {seqtree-0.2.0 → seqtree-0.3.0}/src/engine_seqtm.cpp +2 -4
- {seqtree-0.2.0 → seqtree-0.3.0}/src/engines.hpp +1 -1
- seqtree-0.3.0/src/gapblock.cpp +135 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/src/index.cpp +45 -24
- {seqtree-0.2.0 → seqtree-0.3.0}/src/searcher.cpp +17 -7
- {seqtree-0.2.0 → seqtree-0.3.0}/src/substitution_matrix.cpp +15 -0
- seqtree-0.2.0/python/seqtree/__init__.py +0 -44
- seqtree-0.2.0/python/seqtree/control.py +0 -96
- seqtree-0.2.0/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
- seqtree-0.2.0/python/seqtree/evalue.py +0 -79
- {seqtree-0.2.0 → seqtree-0.3.0}/.gitattributes +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/LICENSE +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/appendix/.gitignore +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/appendix/.latexmkrc +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/appendix/Makefile +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/appendix/epitope_detection.pdf +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/appendix/evalue_matrix.pdf +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/appendix/mhc1_rocpr.pdf +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/appendix/mhc2_rocpr.pdf +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/include/seqtree/kmer_index.hpp +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/python/seqtree/pmhc_evalue.py +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/python/seqtree/py.typed +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/src/blosum62.inc +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/src/codec.cpp +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/src/engine_seqtrie.cpp +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/src/kmer_index.cpp +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/src/pam100.inc +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/src/pam250.inc +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/src/positional_matrix.cpp +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/src/structural.inc +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/src/trie.cpp +0 -0
- {seqtree-0.2.0 → seqtree-0.3.0}/src/trie.hpp +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `seqtree`. Dates are release dates; the project is pre-1.0, so a **minor**
|
|
4
|
+
bump may carry breaking changes.
|
|
5
|
+
|
|
6
|
+
## [0.3.0] — 2026-07-10
|
|
7
|
+
|
|
8
|
+
Gap-block alignment, calibrated cutoffs, seed significance — the removal of several engine paths
|
|
9
|
+
that returned confident wrong answers, and a corrected background control that changes every
|
|
10
|
+
E-value.
|
|
11
|
+
|
|
12
|
+
### Breaking
|
|
13
|
+
|
|
14
|
+
- **The bundled control is a different set of sequences.** It was the *abundance head* of the
|
|
15
|
+
upstream repertoire — the 250,000 most expanded clonotypes — because both `gen_control.py` and
|
|
16
|
+
`_download` took the first `size` unique rows of a count-descending table. `appendix/evalue.tex`
|
|
17
|
+
(`ass:indep`) assumes the control's unique clonotypes are i.i.d. from `P₀`. Measured against a
|
|
18
|
+
uniform sample of the same size, the head is **25.8× more self-similar** (P(Hamming≤2 | equal
|
|
19
|
+
length) 3.11×10⁻³ vs 1.20×10⁻⁴) and carries **3.1× the ball mass** at a BLOSUM62 budget of 40
|
|
20
|
+
(mean n_C 110.1 vs 35.5). Both are now uniform reservoir samples over unique **productive**
|
|
21
|
+
clonotypes, seeded and shuffled so any prefix is itself a valid sub-sample.
|
|
22
|
+
|
|
23
|
+
**Every E-value moves.** Delete `~/.cache/seqtree/control_*.sqtree` after upgrading. Numbers
|
|
24
|
+
derived from the control are corrected throughout this file, `seeds.py`, `SKILL.md` and the
|
|
25
|
+
appendix.
|
|
26
|
+
- **Controls are filtered to productive clonotypes.** VDJtools marks out-of-frame rearrangements
|
|
27
|
+
with `_` and in-frame stops with `*`; 13.7% of the mouse TRB table is out of frame. `_` cannot be
|
|
28
|
+
repaired at the amino-acid level — VDJtools collapses a *run* of untranslatable positions into one
|
|
29
|
+
character, so the residue count is already gone — and out-of-frame junctions escape thymic
|
|
30
|
+
selection, making them an estimator of `P_gen`, which `lem:hierarchy` says is not `P₀`.
|
|
31
|
+
`load_control("mouse_trb_aa")` previously raised on `_`; it now yields 694,241 clonotypes.
|
|
32
|
+
- **`engine="auto"` now always resolves to `seqtm`.** It previously routed matrix-plus-indel
|
|
33
|
+
searches to `seqtrie`, whose budget defaults to `INT_MAX/4`, so
|
|
34
|
+
`SearchParams(max_subs=1, max_ins=1, matrix="BLOSUM62")` silently returned **every reference in
|
|
35
|
+
the index**. `seqtrie` ignores per-type edit caps and can never honour them; `auto` will not pick
|
|
36
|
+
it. Passing a matrix to `seqtrie` without an explicit `max_penalty` now raises.
|
|
37
|
+
- **`Mode::Local` / `mode="local"` deleted.** It was a no-op: the field was stored and never read,
|
|
38
|
+
with zero call sites across `seqtree`, `vdjmatch` and `mhcmatch`.
|
|
39
|
+
- **`GapPrior` takes `(block_start, block_length, longer_length)`** instead of
|
|
40
|
+
`(block_start, shorter_length)`. `central_prior`'s output is bit-identical
|
|
41
|
+
(`|2i − (m−d)| == |2i + d − m|`), and a frozen table pins that.
|
|
42
|
+
- **`gap_extend` is now honoured.** `Index.align` is a real Gotoh affine alignment; previously
|
|
43
|
+
`gap_extend=1` and `gap_extend=99` produced identical scores and ops.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- `pairwise_batch` silently inverted `n_ins`/`n_dels` when transposing, but only when
|
|
48
|
+
`len(a) >= len(b)` — a size-dependent inversion.
|
|
49
|
+
- `Index.align` did not validate the query alphabet (unlike `search_into`), and accepted negative
|
|
50
|
+
gap costs.
|
|
51
|
+
- `Limits::max_hits` was set and never read; in `mode="all"` it truncated an **unsorted** list.
|
|
52
|
+
- Stale docstring on `SubstitutionMatrix.from_similarity` (claimed `max(s_aa, s_bb) − s_ab`; the
|
|
53
|
+
code is the Gram form `s_aa + s_bb − 2·s_ab`).
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- **`SubstitutionMatrix.scale()`** — the median mismatch penalty (BLOSUM62 → 14). Gap costs must
|
|
58
|
+
live on the matrix's scale; the old `gap_open=1` default made gaps ~14× cheaper than
|
|
59
|
+
substitutions, so `align()` would gap an equal-length pair rather than substitute. Use
|
|
60
|
+
`gap_open = 2 * matrix.scale()`.
|
|
61
|
+
- **`seqtree.gapblock`** — single-contiguous-gap-block alignment for anchored junction loops.
|
|
62
|
+
`gapblock_score` is the exact `O(min(m,n))` optimum (0 mismatches in 55,727 pairs against
|
|
63
|
+
brute-force layout enumeration). `GapBlockIndex` reuses the existing Hamming engine over
|
|
64
|
+
deletion variants; no new C++.
|
|
65
|
+
- **Gap priors** — `central_prior(lam)`, `profile_prior(lam, w)`, `frame_prior(lam, c)` and
|
|
66
|
+
`embed_in_frame(seq, width, c)`. A sequence score alone cannot place the block: a hard central
|
|
67
|
+
pin agrees with the flat, score-only choice on only 10.6% of pairs.
|
|
68
|
+
- **`gapblock.score_matrix` and `ScoreMatrix`** — the dense `n × K` counterpart of
|
|
69
|
+
`GapBlockIndex.search`, for prototype-distance embeddings, where nothing can be pruned because
|
|
70
|
+
the distance to every reference *is* the output. C++, GIL released, one thread per core. On an
|
|
71
|
+
M3 against 3,000 prototypes: **51.3 M pairs/s** single-threaded, **532.7 M** on 16 cores, versus
|
|
72
|
+
0.41 M for pure-Python `gapblock_score` — while evaluating all `L+1` block positions, not a
|
|
73
|
+
fixed shortlist. The prior is flattened once into an `[m][d][i]` cube, so the kernel never
|
|
74
|
+
re-enters Python. `ScoreMatrix` carries the CPython buffer protocol: `numpy.asarray` wraps it
|
|
75
|
+
without copying, and seqtree keeps its zero runtime dependencies.
|
|
76
|
+
- **`gapblock.positions_prior(starts)`** — restrict the block to a fixed set of starts, negative
|
|
77
|
+
values counting from the end, reproducing the `gap_positions=(3, 4, -4, -3)` convention that
|
|
78
|
+
other junction aligners hardcode. Shipped for interoperability, not as a recommendation: at a
|
|
79
|
+
matched false-positive rate on human TRB, candidate starts reach precision 0.156 against 0.414
|
|
80
|
+
for a single hard-pinned centre.
|
|
81
|
+
- **`gapblock.IslandProfile`** — a per-island position weight matrix whose column penalty is
|
|
82
|
+
measured against the column's own consensus, `pen(j, a) = round(lam·log(p_max_j / p_j(a)))`. A
|
|
83
|
+
textbook log-odds score is signed and therefore not a ball; this one is `>= 0`, zero on the
|
|
84
|
+
consensus, and flows through `thetas_from_scores` unchanged. The frame column defaults to the
|
|
85
|
+
entropy-optimal one, which is modal at `c = 6` on real islands — where crystal structures put
|
|
86
|
+
the block.
|
|
87
|
+
|
|
88
|
+
Whether it beats scoring against every member **depends entirely on the cutoff**, which moves
|
|
89
|
+
with `N`: the E-value's `k = floor(e_target·M/N)` is how many control neighbours the cutoff may
|
|
90
|
+
admit, so the FPR is `k/M`. Over 108 calibrated VDJdb islands of ≥10 members (human TRB, three
|
|
91
|
+
held-out splits, paired bootstrap over islands, 250k control negatives):
|
|
92
|
+
|
|
93
|
+
| regime | FPR | min-over-members | `IslandProfile` | difference [95% CI] |
|
|
94
|
+
|---|---|---|---|---|
|
|
95
|
+
| loose reference | 1% | **99.5%** | 99.1% | −0.40 [−1.09, +0.14] |
|
|
96
|
+
| per-epitope islands (`N`= group, median 88) | 0.0568% | 88.3% | **89.3%** | +0.93 [−0.80, +2.79] |
|
|
97
|
+
| repertoire annotation (`N`≈20k) | 0.0012% | 37.6% | **48.5%** | +10.90 [+7.69, +14.21] |
|
|
98
|
+
|
|
99
|
+
So: **no significant difference while building the islands**, a large one when using them to
|
|
100
|
+
annotate a repertoire (on islands ≥50 members, 9.8% vs 22.6%). At `N≈20k` and `e_target=0.05`,
|
|
101
|
+
`k=0` and `thetas_from_scores` returns `-1` — the rule of three certifies no `E` below
|
|
102
|
+
`3N/M = 0.236`, and that is the cutoff the third row uses.
|
|
103
|
+
|
|
104
|
+
It does **not** generalise: same-epitope junctions in a different island are recovered by
|
|
105
|
+
neither representation. Nor is it a compression — 1,176 B against 182 B of member strings,
|
|
106
|
+
break-even at 84 members.
|
|
107
|
+
- **`threshold_for_evalue` / `thetas_from_scores`** — invert `Ê = (N/M)·n_C` into the score cutoff
|
|
108
|
+
that achieves a target E, **per query**. Exact rather than a root-find, because scores are
|
|
109
|
+
integers. Returns `-1` where `e_target < 3N/M`, i.e. where the control is too small to certify
|
|
110
|
+
the bar.
|
|
111
|
+
- **`seqtree.seeds`** — `core_kmers` and `SeedIndex` give control-calibrated E-values for shared
|
|
112
|
+
core k-mers. A shared rare central k-mer is ~4× enriched among co-specific pairs, but covers only
|
|
113
|
+
~0.5% of them: seeds buy precision, not recall.
|
|
114
|
+
- **`bench/bench_gapblock.py`** — the gap-freedom ladder, from a hard central pin through priors to
|
|
115
|
+
unrestricted affine.
|
|
116
|
+
|
|
117
|
+
### Measured
|
|
118
|
+
|
|
119
|
+
Numbers that constrain the API, all reproducible from `bench/` and the downstream repos:
|
|
120
|
+
|
|
121
|
+
- **One gap block is enough.** Against a model-independent structural oracle (iterative
|
|
122
|
+
superposition + unrestricted affine DP) over 3,049 crystal junction pairs from 199 unique
|
|
123
|
+
sequences, the true correspondence is a single contiguous block in **95.2–100%** of cases for
|
|
124
|
+
every `d = 1..4`. Forcing one block costs no median CA-RMSD.
|
|
125
|
+
- **The restriction is free where it applies, and protective where it does not.** At
|
|
126
|
+
`gap_open = 2*scale`, gap-block equals unrestricted affine on **98.8%** of related pairs (one
|
|
127
|
+
indel + 0–2 substitutions). On *unrelated* pairs affine undercuts it by a median of **106**
|
|
128
|
+
penalty units — affine inventing an alignment that does not exist. Extra gap freedom buys
|
|
129
|
+
manufactured similarity.
|
|
130
|
+
- **A fixed score cutoff is not a calibrated cutoff.** Building islands on human TRB by union-find
|
|
131
|
+
at `gapblock_score ≤ 60`, **31.7%** of size-matched *random control* junctions land in a component
|
|
132
|
+
of ≥5 — structure invented by the threshold. Per-query E-value edges at `E* = 0.05` cut that to
|
|
133
|
+
**0.000** while raising the real signal: 2.334 edges per node against 0.021 for the control, which
|
|
134
|
+
forms 19,248 singletons, 223 pairs, three components of size 3–4, and nothing larger. The control
|
|
135
|
+
arm's realised edge rate lands on `E*`, which is the check that the calibration is honest.
|
|
136
|
+
- **Mouse replicates it.** Against the mouse TRB control (694,241 productive clonotypes), 5 epitopes
|
|
137
|
+
and 1,692 TCRs give 5.856 calibrated edges per node against 0.019 for the control, which again
|
|
138
|
+
forms nothing larger than a pair. At a fixed θ=40 the control still lands 18.3% of its nodes in
|
|
139
|
+
components of ≥5.
|
|
140
|
+
- **Constraining the block is what buys precision.** Compared at a *matched* false-positive rate —
|
|
141
|
+
each rung given the cutoff at which its own ball admits `E*` chance neighbours, since a freer rung
|
|
142
|
+
finds lower scores and a fixed budget would reward it for that — retrieval precision on the
|
|
143
|
+
length-different fraction of VDJdb human TRB same-epitope pairs (2,000 queries, `E* = 0.1`):
|
|
144
|
+
|
|
145
|
+
| rung | layouts | precision |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| fixed centre | 1 | **0.414** |
|
|
148
|
+
| central prior λ=21 | ~1–2 effective | 0.336 |
|
|
149
|
+
| flat (score alone) | L+1 | 0.176 |
|
|
150
|
+
| candidates (after 3–4, before last 3–4, centre) | 5 | 0.156 |
|
|
151
|
+
|
|
152
|
+
Trying several plausible positions and keeping the best score is worse than not trying: the score
|
|
153
|
+
picks the structurally correct layout about a tenth of the time, so each extra candidate is mostly
|
|
154
|
+
an opportunity to be wrong. Mouse replicates the ordering at `E* = 1.0` but its length-different
|
|
155
|
+
stratum holds only 24–79 true positives, too few to separate the rungs.
|
|
156
|
+
- **Performance.** 91% of `GapBlockIndex.search` time is the query-deletion-variant branch, 9% the
|
|
157
|
+
9.8M-entry auxiliary indices. Netting the prior out of each variant's budget cuts that branch
|
|
158
|
+
from ~15 sub-searches to 2.5. Variant dedup (7–10% of variants before pruning, fewer after) and
|
|
159
|
+
length-bucketing are **not** built. At budget 40 over 250k references, `d_max=2` gap-block search
|
|
160
|
+
costs 2,562 µs/query — less than the plain Hamming ball at the same budget (3,051 µs/query).
|
|
161
|
+
|
|
162
|
+
### Docs
|
|
163
|
+
|
|
164
|
+
- `skills/seqtree/SKILL.md` — public API surface, invariants, and the gotchas that have bitten.
|
|
165
|
+
- `docs/gapblock.rst` — a worked guide: why one gap block, how to choose its position, why a
|
|
166
|
+
placement rule is a column frame, and why a fixed score cutoff is not a calibrated one.
|
|
167
|
+
- README corrected: `seqtrie` is a full-width DP that ignores per-type caps, not a banded one, and
|
|
168
|
+
`auto` does not choose between engines.
|
|
169
|
+
- `appendix/evalue.tex` gains §"The score model: one gap block, placed by a prior" (the appendix
|
|
170
|
+
derived a theory of balls without ever saying what the score was) and a remark inverting the
|
|
171
|
+
E-value into a per-query cutoff. The pMHC section is compacted from ~110 lines of prose to ~50,
|
|
172
|
+
deferring to the `mhcmatch` appendix, which specialises this one rather than repeating it. Its
|
|
173
|
+
empirical tables stay: they are this repo's own `bench/bench_mhc_guess.py` output.
|
|
174
|
+
- Test coverage: `gapblock.py`, `evalue.py` and `seeds.py` at **100%**; package total 88%. A new
|
|
175
|
+
`tests/python/test_doc_coverage.py` fails the build if a public symbol is undocumented, missing
|
|
176
|
+
from `__all__`, or unreachable from any docs page.
|
|
177
|
+
|
|
178
|
+
## [0.2.0]
|
|
179
|
+
|
|
180
|
+
- `structural` substitution matrix: Miyazawa–Jernigan interaction-strength similarity.
|
|
181
|
+
- Built-in matrix list: `identity`, `BLOSUM62`, `PAM250`, `PAM100`, `structural` (dropped PAM50).
|
|
182
|
+
|
|
183
|
+
## [0.1.0]
|
|
184
|
+
|
|
185
|
+
- `SubstitutionMatrix.penalty(a, b)` exposed to Python.
|
|
186
|
+
|
|
187
|
+
## [0.0.3]
|
|
188
|
+
|
|
189
|
+
- Reproducible table→plot benchmark pipeline with oracle + perf regression.
|
|
190
|
+
- pMHC non-binder E-value filter; class-II promiscuity notes.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: seqtree
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Fast fuzzy search over biological sequences (C++ core, Python bindings)
|
|
5
5
|
Keywords: sequence-search,fuzzy-matching,CDR3,immunology,bioinformatics,trie
|
|
6
6
|
Author-Email: ISALGO laboratory <mikhail.shugay@gmail.com>
|
|
@@ -17,9 +17,9 @@ Project-URL: Documentation, https://antigenomics.github.io/seqtree/
|
|
|
17
17
|
Requires-Python: >=3.10
|
|
18
18
|
Provides-Extra: test
|
|
19
19
|
Requires-Dist: pytest>=7.4; extra == "test"
|
|
20
|
+
Requires-Dist: pytest-cov>=4.1; extra == "test"
|
|
20
21
|
Provides-Extra: bench
|
|
21
22
|
Requires-Dist: huggingface_hub; extra == "bench"
|
|
22
|
-
Requires-Dist: pandas; extra == "bench"
|
|
23
23
|
Requires-Dist: psutil; extra == "bench"
|
|
24
24
|
Provides-Extra: docs
|
|
25
25
|
Requires-Dist: sphinx; extra == "docs"
|
|
@@ -48,11 +48,12 @@ Two search engines over one trie:
|
|
|
48
48
|
- **`seqtm`** — branch-and-bound enumeration. Exact per-type edit caps
|
|
49
49
|
(`max_subs` / `max_ins` / `max_dels`) and a fast Hamming-only path. Best for
|
|
50
50
|
small edit distances (UMI collapse, error correction, CDR3/epitope matching).
|
|
51
|
-
- **`seqtrie`** —
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
- **`seqtrie`** — full-width edit-distance DP carried down the trie. Honours the
|
|
52
|
+
`max_penalty` score budget only; it **ignores the per-type edit caps**. Use it
|
|
53
|
+
when the budget is the whole specification.
|
|
54
54
|
|
|
55
|
-
`engine="auto"` picks
|
|
55
|
+
`engine="auto"` always picks `seqtm`, because it is the only engine that enforces
|
|
56
|
+
the caps you asked for. Results are payload-agnostic:
|
|
56
57
|
`(ref_id, score, n_subs, n_ins, n_dels)`. Downstream libraries map `ref_id` back
|
|
57
58
|
to their own payloads (V gene, MHC, counts) and filter.
|
|
58
59
|
|
|
@@ -69,6 +70,23 @@ Beyond search, seqtree ships:
|
|
|
69
70
|
- **E-values / significance** — calibrate hit counts against a background control repertoire
|
|
70
71
|
(`load_control` + `evalues`), the TCRNET approach on a finite-sample footing. See the
|
|
71
72
|
[E-value guide](https://antigenomics.github.io/seqtree/evalue.html).
|
|
73
|
+
- **Calibrated cutoffs** — `threshold_for_evalue` inverts the E-value into the score cutoff that
|
|
74
|
+
achieves it, **per query**. A fixed cutoff is not a calibrated one: a control repertoire is
|
|
75
|
+
dense near germline and sparse among rare junctions, so the same threshold buys a common query
|
|
76
|
+
far more chance neighbours than a rare one.
|
|
77
|
+
- **Gap-block alignment** — `gapblock` restricts alignment to one contiguous indel, which is the
|
|
78
|
+
right model for a V(D)J junction and, measured against unrestricted affine alignment, is
|
|
79
|
+
exactly optimal on **98.8%** of genuinely related pairs at a calibrated `gap_open`. A gap
|
|
80
|
+
prior (`central_prior`, `profile_prior`, `frame_prior`) chooses where the block goes — a
|
|
81
|
+
sequence score alone cannot. `score_matrix` scores a whole query set against a whole reference
|
|
82
|
+
set in one GIL-released C++ call (**532 M pairs/s** on 16 cores; `numpy.asarray` wraps the
|
|
83
|
+
result with no copy), the shape a prototype-distance embedding needs.
|
|
84
|
+
- **Island profiles** — `IslandProfile.fit` builds a position weight matrix over a set of
|
|
85
|
+
frame-aligned junctions (an *island*) and scores a query column by column against the island
|
|
86
|
+
consensus, as a non-negative penalty that flows through `threshold_for_evalue` unchanged. At a
|
|
87
|
+
repertoire-scale cutoff it recovers **48.5%** of held-out members against **37.6%** for
|
|
88
|
+
min-over-members; at a loose cutoff the two are indistinguishable, so it earns its keep only
|
|
89
|
+
where the cutoff is strict.
|
|
72
90
|
|
|
73
91
|
## Install
|
|
74
92
|
|
|
@@ -85,7 +103,7 @@ needs a C++17 compiler and CMake (pulled in automatically by the build).
|
|
|
85
103
|
```fish
|
|
86
104
|
bash setup.sh # repo-local .venv + editable install
|
|
87
105
|
bash setup.sh --tests # + pytest
|
|
88
|
-
bash setup.sh --bench # + benchmark deps (huggingface_hub,
|
|
106
|
+
bash setup.sh --bench # + benchmark deps (huggingface_hub, psutil)
|
|
89
107
|
```
|
|
90
108
|
|
|
91
109
|
## Quickstart
|
|
@@ -119,6 +137,33 @@ target = seqtree.Index.build(vdjdb_cdr3s, alphabet="aa")
|
|
|
119
137
|
for q, r in zip(queries, seqtree.evalues(target, control, queries, p)):
|
|
120
138
|
if r["p_enrichment"] < 1e-3:
|
|
121
139
|
print(q, r["E"], r["n_target"], r["n_control"])
|
|
140
|
+
|
|
141
|
+
# ...and the cutoff that achieves a target E, per query (-1 = unreachable at this control size)
|
|
142
|
+
ceiling = seqtree.SearchParams(max_subs=14, max_penalty=50, matrix="BLOSUM62", engine="seqtm")
|
|
143
|
+
thetas = seqtree.threshold_for_evalue(target, control, queries, ceiling, e_target=0.05)
|
|
144
|
+
|
|
145
|
+
# one contiguous gap block, placed by a prior rather than by the score alone
|
|
146
|
+
from seqtree.gapblock import GapBlockIndex, central_prior, embed_in_frame
|
|
147
|
+
|
|
148
|
+
gbi = GapBlockIndex(cdr3s, "aa", d_max=2)
|
|
149
|
+
mat = seqtree.SubstitutionMatrix.blosum62()
|
|
150
|
+
for ref_id, score, block_len, block_pos in gbi.search(
|
|
151
|
+
"CASSLGQAYEQYF", 40, mat, gap_open=2 * mat.scale(),
|
|
152
|
+
gap_prior=central_prior(int(1.5 * mat.scale()))):
|
|
153
|
+
...
|
|
154
|
+
|
|
155
|
+
# a fixed frame column makes gap placement transitive -- and a column index, hence a PWM, possible
|
|
156
|
+
embed_in_frame("CASSGQAYEQYF", width=14, c=4) # 'CASS--GQAYEQYF'
|
|
157
|
+
|
|
158
|
+
# a whole query set vs a whole reference set, in one GIL-released C++ call
|
|
159
|
+
from seqtree.gapblock import score_matrix, IslandProfile
|
|
160
|
+
sm = score_matrix(clonotypes, prototypes, mat, gap_open=2 * mat.scale(), threads=0)
|
|
161
|
+
import numpy as np
|
|
162
|
+
distances = np.asarray(sm) # (len(clonotypes), len(prototypes)) int32, zero-copy
|
|
163
|
+
|
|
164
|
+
# a position weight matrix over an island, still a non-negative penalty (feeds threshold_for_evalue)
|
|
165
|
+
profile = IslandProfile.fit(island_members)
|
|
166
|
+
profile.score("CASSLGQAYEQYF") # 0 on the consensus, > 0 for deviations
|
|
122
167
|
```
|
|
123
168
|
|
|
124
169
|
## Tests
|
|
@@ -138,6 +183,8 @@ python bench/bench.py # recall vs ground truth (real VDJdb data)
|
|
|
138
183
|
python bench/bench_evalue.py # true E-value benchmark (target vs background control)
|
|
139
184
|
python bench/bench_evalue_matrix.py # significance across reference/control/query/scope grid
|
|
140
185
|
python bench/bench_epitope.py # epitope detection-complexity (GIL vs NLV)
|
|
186
|
+
python bench/bench_gapblock.py # the gap-freedom ladder: fixed centre → prior → flat → affine
|
|
187
|
+
python bench/bench_score_matrix.py # dense batch gap-block throughput (µs/pair, M pairs/s, RSS)
|
|
141
188
|
```
|
|
142
189
|
|
|
143
190
|
Figures (throughput, scaling, matrix-scoring overhead, collisions, E-value matrix, epitope
|
|
@@ -15,11 +15,12 @@ Two search engines over one trie:
|
|
|
15
15
|
- **`seqtm`** — branch-and-bound enumeration. Exact per-type edit caps
|
|
16
16
|
(`max_subs` / `max_ins` / `max_dels`) and a fast Hamming-only path. Best for
|
|
17
17
|
small edit distances (UMI collapse, error correction, CDR3/epitope matching).
|
|
18
|
-
- **`seqtrie`** —
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
- **`seqtrie`** — full-width edit-distance DP carried down the trie. Honours the
|
|
19
|
+
`max_penalty` score budget only; it **ignores the per-type edit caps**. Use it
|
|
20
|
+
when the budget is the whole specification.
|
|
21
21
|
|
|
22
|
-
`engine="auto"` picks
|
|
22
|
+
`engine="auto"` always picks `seqtm`, because it is the only engine that enforces
|
|
23
|
+
the caps you asked for. Results are payload-agnostic:
|
|
23
24
|
`(ref_id, score, n_subs, n_ins, n_dels)`. Downstream libraries map `ref_id` back
|
|
24
25
|
to their own payloads (V gene, MHC, counts) and filter.
|
|
25
26
|
|
|
@@ -36,6 +37,23 @@ Beyond search, seqtree ships:
|
|
|
36
37
|
- **E-values / significance** — calibrate hit counts against a background control repertoire
|
|
37
38
|
(`load_control` + `evalues`), the TCRNET approach on a finite-sample footing. See the
|
|
38
39
|
[E-value guide](https://antigenomics.github.io/seqtree/evalue.html).
|
|
40
|
+
- **Calibrated cutoffs** — `threshold_for_evalue` inverts the E-value into the score cutoff that
|
|
41
|
+
achieves it, **per query**. A fixed cutoff is not a calibrated one: a control repertoire is
|
|
42
|
+
dense near germline and sparse among rare junctions, so the same threshold buys a common query
|
|
43
|
+
far more chance neighbours than a rare one.
|
|
44
|
+
- **Gap-block alignment** — `gapblock` restricts alignment to one contiguous indel, which is the
|
|
45
|
+
right model for a V(D)J junction and, measured against unrestricted affine alignment, is
|
|
46
|
+
exactly optimal on **98.8%** of genuinely related pairs at a calibrated `gap_open`. A gap
|
|
47
|
+
prior (`central_prior`, `profile_prior`, `frame_prior`) chooses where the block goes — a
|
|
48
|
+
sequence score alone cannot. `score_matrix` scores a whole query set against a whole reference
|
|
49
|
+
set in one GIL-released C++ call (**532 M pairs/s** on 16 cores; `numpy.asarray` wraps the
|
|
50
|
+
result with no copy), the shape a prototype-distance embedding needs.
|
|
51
|
+
- **Island profiles** — `IslandProfile.fit` builds a position weight matrix over a set of
|
|
52
|
+
frame-aligned junctions (an *island*) and scores a query column by column against the island
|
|
53
|
+
consensus, as a non-negative penalty that flows through `threshold_for_evalue` unchanged. At a
|
|
54
|
+
repertoire-scale cutoff it recovers **48.5%** of held-out members against **37.6%** for
|
|
55
|
+
min-over-members; at a loose cutoff the two are indistinguishable, so it earns its keep only
|
|
56
|
+
where the cutoff is strict.
|
|
39
57
|
|
|
40
58
|
## Install
|
|
41
59
|
|
|
@@ -52,7 +70,7 @@ needs a C++17 compiler and CMake (pulled in automatically by the build).
|
|
|
52
70
|
```fish
|
|
53
71
|
bash setup.sh # repo-local .venv + editable install
|
|
54
72
|
bash setup.sh --tests # + pytest
|
|
55
|
-
bash setup.sh --bench # + benchmark deps (huggingface_hub,
|
|
73
|
+
bash setup.sh --bench # + benchmark deps (huggingface_hub, psutil)
|
|
56
74
|
```
|
|
57
75
|
|
|
58
76
|
## Quickstart
|
|
@@ -86,6 +104,33 @@ target = seqtree.Index.build(vdjdb_cdr3s, alphabet="aa")
|
|
|
86
104
|
for q, r in zip(queries, seqtree.evalues(target, control, queries, p)):
|
|
87
105
|
if r["p_enrichment"] < 1e-3:
|
|
88
106
|
print(q, r["E"], r["n_target"], r["n_control"])
|
|
107
|
+
|
|
108
|
+
# ...and the cutoff that achieves a target E, per query (-1 = unreachable at this control size)
|
|
109
|
+
ceiling = seqtree.SearchParams(max_subs=14, max_penalty=50, matrix="BLOSUM62", engine="seqtm")
|
|
110
|
+
thetas = seqtree.threshold_for_evalue(target, control, queries, ceiling, e_target=0.05)
|
|
111
|
+
|
|
112
|
+
# one contiguous gap block, placed by a prior rather than by the score alone
|
|
113
|
+
from seqtree.gapblock import GapBlockIndex, central_prior, embed_in_frame
|
|
114
|
+
|
|
115
|
+
gbi = GapBlockIndex(cdr3s, "aa", d_max=2)
|
|
116
|
+
mat = seqtree.SubstitutionMatrix.blosum62()
|
|
117
|
+
for ref_id, score, block_len, block_pos in gbi.search(
|
|
118
|
+
"CASSLGQAYEQYF", 40, mat, gap_open=2 * mat.scale(),
|
|
119
|
+
gap_prior=central_prior(int(1.5 * mat.scale()))):
|
|
120
|
+
...
|
|
121
|
+
|
|
122
|
+
# a fixed frame column makes gap placement transitive -- and a column index, hence a PWM, possible
|
|
123
|
+
embed_in_frame("CASSGQAYEQYF", width=14, c=4) # 'CASS--GQAYEQYF'
|
|
124
|
+
|
|
125
|
+
# a whole query set vs a whole reference set, in one GIL-released C++ call
|
|
126
|
+
from seqtree.gapblock import score_matrix, IslandProfile
|
|
127
|
+
sm = score_matrix(clonotypes, prototypes, mat, gap_open=2 * mat.scale(), threads=0)
|
|
128
|
+
import numpy as np
|
|
129
|
+
distances = np.asarray(sm) # (len(clonotypes), len(prototypes)) int32, zero-copy
|
|
130
|
+
|
|
131
|
+
# a position weight matrix over an island, still a non-negative penalty (feeds threshold_for_evalue)
|
|
132
|
+
profile = IslandProfile.fit(island_members)
|
|
133
|
+
profile.score("CASSLGQAYEQYF") # 0 on the consensus, > 0 for deviations
|
|
89
134
|
```
|
|
90
135
|
|
|
91
136
|
## Tests
|
|
@@ -105,6 +150,8 @@ python bench/bench.py # recall vs ground truth (real VDJdb data)
|
|
|
105
150
|
python bench/bench_evalue.py # true E-value benchmark (target vs background control)
|
|
106
151
|
python bench/bench_evalue_matrix.py # significance across reference/control/query/scope grid
|
|
107
152
|
python bench/bench_epitope.py # epitope detection-complexity (GIL vs NLV)
|
|
153
|
+
python bench/bench_gapblock.py # the gap-freedom ladder: fixed centre → prior → flat → affine
|
|
154
|
+
python bench/bench_score_matrix.py # dense batch gap-block throughput (µs/pair, M pairs/s, RSS)
|
|
108
155
|
```
|
|
109
156
|
|
|
110
157
|
Figures (throughput, scaling, matrix-scoring overhead, collisions, E-value matrix, epitope
|
|
@@ -24,12 +24,18 @@ between them, written so an agent developing either package can pick it up cold:
|
|
|
24
24
|
|
|
25
25
|
| Capability | Module / symbol | Notes |
|
|
26
26
|
|---|---|---|
|
|
27
|
-
| Fuzzy fixed-length search | `seqtree.Index`, engines `seqtm` / `seqtrie` | per-type edit caps + Hamming fast path
|
|
28
|
-
|
|
|
27
|
+
| Fuzzy fixed-length search | `seqtree.Index`, engines `seqtm` / `seqtrie` | seqtm: per-type edit caps + Hamming fast path. seqtrie: budget-only full-width DP over the trie (not banded), ignores per-type caps |
|
|
28
|
+
| Single-gap-block loop alignment | `seqtree.gapblock` (Python) | anchored CDR3/junction alignment: one contiguous indel + positional gap prior |
|
|
29
|
+
| Dense batch gap-block scoring | `seqtree.gapblock.score_matrix` → `ScoreMatrix` | every query × every reference in one GIL-released C++ call (~532 M pairs/s on 16 cores); `numpy.asarray` is zero-copy. The shape a prototype-distance embedding needs, where nothing can be pruned |
|
|
30
|
+
| Per-island profile (PWM) | `seqtree.gapblock.IslandProfile` | position weight matrix over a frame-aligned island; `pen(j,a) = round(lam·log(p_max_j/p_j(a)))` is `≥ 0` and `0` on the consensus, so it stays a ball and flows through `thetas_from_scores` unchanged. Beats min-over-members only at a strict cutoff |
|
|
29
31
|
| Substitution scoring | `SubstitutionMatrix` (Gram → squared-distance penalty `s_aa+s_bb−2·s_ab`) | payload-agnostic |
|
|
30
32
|
| Per-position scoring | `PositionalMatrix` (`penalty(pos,a,b)` = base × per-position weight; weight 0 = free/anchor) | the hook for PSSMs and anchor masking |
|
|
31
33
|
| k-mer seed index | `KmerIndex` (C++): unique-k-mer trie + CSR postings + per-peptide allele tag; `seed_and_gather` (GIL-released, parallel) | million-scale candidate generation |
|
|
32
34
|
| Control-calibrated E-values | `seqtree.evalues`, `seqtree.load_control` | `Ê = (N/M)·n_C`, Poisson tail, `exclude_exact` |
|
|
35
|
+
| Calibrated score cutoffs | `seqtree.threshold_for_evalue`, `thetas_from_scores` | inverts `Ê` to a **per-query** θ. A fixed θ is not calibrated: at `gapblock_score ≤ 60`, 31.7% of *random control* junctions land in a component of ≥5 — structure invented by the threshold, which per-query cutoffs remove entirely |
|
|
36
|
+
| Gap-placement priors | `central_prior`, `profile_prior`, `frame_prior`, `positions_prior`, `embed_in_frame` | `prior(i, d, m) ≥ 0` and `= 0` at `d = 0`. Only a **constant-`i`** rule (`frame_prior`) makes a frame transitive — a column index, hence a PWM, which `IslandProfile` now ships |
|
|
37
|
+
| Seed significance | `seqtree.seeds`: `core_kmers`, `SeedIndex` | precision, not recall: ~0.5% cross-island coverage |
|
|
38
|
+
| Background control | `seqtree.load_control`, `sanitize` | uniform reservoir sample over unique **productive** clonotypes. *Planned:* an out-of-frame control as an empirical `P_gen` sample for the `π̂_gen` fallback — needs the `*.ntvj` tables, since the `.aa` table's `_` has already collapsed a run of positions |
|
|
33
39
|
| Anchor / layout model | `seqtree.layout`: `AnchorSpec`, `DEFAULTS`, `mask_anchors`, `kmers`, `presentation_features`, `weight_profile` | parametrized anchors; class-II register trick |
|
|
34
40
|
| pMHC homology + reverse | `seqtree.pmhc`: `PMHCStore`, `search_homologs`, `assign_allele`, `find_mimics`, `build_kmer_index` | reference impl; mhcmatch productionizes |
|
|
35
41
|
| Presentation-aware E-values | `seqtree.pmhc_evalue.homolog_evalue` | per-allele null |
|
|
@@ -37,7 +43,7 @@ between them, written so an agent developing either package can pick it up cold:
|
|
|
37
43
|
**E-value theory** lives in `appendix/evalue.tex`: the empirical-control null (§Setup, §Null), the
|
|
38
44
|
Poisson/Chen–Stein bound (§Poisson), multiple testing (§E-value), the closest-hit Gumbel law
|
|
39
45
|
(§Gumbel), Karlin–Altschul as the i.i.d. special case (§KA), the pMHC presentation-aware extension
|
|
40
|
-
(§Epitopes
|
|
46
|
+
(§Epitopes), and elementary applications — UMI/barcode birthday-collision and
|
|
41
47
|
CDR3-nt error clustering (§Related applications).
|
|
42
48
|
|
|
43
49
|
**Datasets.** `isalgo/pmhc_data` ships two tiers (see `appendix` Table "Scope of the two pmhc_data
|
|
@@ -115,8 +121,8 @@ additions below.
|
|
|
115
121
|
presentation-aware E-values via `pmhc_evalue.homolog_evalue`). Positive control: the Dolton et al.
|
|
116
122
|
A\*02:01 cross-reactive trio.
|
|
117
123
|
- Allele guessing (reverse problem): `assign_allele` + the vote-fraction ranking / register trick
|
|
118
|
-
validated in `bench/bench_mhc_guess.py` (appendix §
|
|
119
|
-
- **Non-binder filter** (appendix §
|
|
124
|
+
validated in `bench/bench_mhc_guess.py` (appendix §Epitopes). ROC-AUC 0.90–0.98.
|
|
125
|
+
- **Non-binder filter** (appendix §Epitopes, "Non-binders and class-II promiscuity"): _binds no MHC_ →
|
|
120
126
|
best-over-panel E-value high / no allele scores above background; _doesn't bind allele a_ →
|
|
121
127
|
per-allele `E_a > α`. mhcmatch exposes both thresholds.
|
|
122
128
|
- Tier choice (full vs shortlist, §1 Datasets).
|
seqtree-0.3.0/SOURCES.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Data sources
|
|
2
|
+
|
|
3
|
+
Every dataset seqtree ships, downloads, or reads in a benchmark. Provenance is stated as
|
|
4
|
+
**experimental** (measured from a biological sample), **derived** (computed from experimental data),
|
|
5
|
+
or **synthetic** (generated by a model). Never conflate the three.
|
|
6
|
+
|
|
7
|
+
## Bundled with the wheel
|
|
8
|
+
|
|
9
|
+
### `python/seqtree/data/control_human_trb_aa.txt`
|
|
10
|
+
|
|
11
|
+
| | |
|
|
12
|
+
|---|---|
|
|
13
|
+
| what | 250,000 human TRB junction amino-acid sequences, the null distribution `P₀` |
|
|
14
|
+
| provenance | **experimental**, then **derived** by uniform subsampling |
|
|
15
|
+
| origin | HuggingFace `antigenomics/vdjdb-cdr3-frequencies`, human TRB pooled clonotype table |
|
|
16
|
+
| format | one junction per line, productive only (20 standard amino acids) |
|
|
17
|
+
| regenerate | `python bench/gen_control.py --species human --chain trb --size 250000 --seed 0` |
|
|
18
|
+
|
|
19
|
+
The upstream table is sorted by clonotype count, descending. The sample is a **seeded reservoir
|
|
20
|
+
sample over unique clonotypes**, then shuffled, so any prefix `bundled[:k]` is itself a uniform
|
|
21
|
+
sub-sample. Taking the first *k* rows of the upstream table instead yields the abundance head, which
|
|
22
|
+
is 25.8× more self-similar and carries 3.1× the ball mass — it violates `ass:indep` in
|
|
23
|
+
`appendix/evalue.tex` and corrupts every E-value. See `CHANGELOG.md` [0.3.0] Breaking.
|
|
24
|
+
|
|
25
|
+
Out-of-frame clonotypes (VDJtools `_`) and in-frame stops (`*`) are dropped. `_` marks a *collapsed
|
|
26
|
+
run* of untranslatable positions, so the residue count is already lost and no amino-acid repair is
|
|
27
|
+
possible; and out-of-frame junctions escape thymic selection, making them a sample of `P_gen`, which
|
|
28
|
+
`lem:hierarchy` proves is not `P₀`.
|
|
29
|
+
|
|
30
|
+
### Substitution matrices — `src/blosum62.inc`, `pam250.inc`, `pam100.inc`, `structural.inc`
|
|
31
|
+
|
|
32
|
+
| | |
|
|
33
|
+
|---|---|
|
|
34
|
+
| provenance | **derived** (published log-odds / interaction-strength matrices) |
|
|
35
|
+
| origin | BLOSUM62 (NCBI); EMBOSS EPAM250, EPAM100; Miyazawa–Jernigan interaction strengths |
|
|
36
|
+
| regenerate | `python bench/gen_matrices.py` |
|
|
37
|
+
|
|
38
|
+
Stored as non-negative penalties via the Gram transform `pen[a][b] = s[a][a] + s[b][b] − 2·s[a][b]`,
|
|
39
|
+
clamped at zero, so `penalty(a, a) == 0` and the score defines a ball.
|
|
40
|
+
|
|
41
|
+
## Downloaded on demand
|
|
42
|
+
|
|
43
|
+
### `load_control(name)` → `~/.cache/seqtree/control_*.sqtree`
|
|
44
|
+
|
|
45
|
+
| | |
|
|
46
|
+
|---|---|
|
|
47
|
+
| what | larger control repertoires, e.g. `mouse_trb_aa` (694,241 productive clonotypes) |
|
|
48
|
+
| provenance | **experimental**, then **derived** by uniform subsampling |
|
|
49
|
+
| origin | HuggingFace `antigenomics/vdjdb-cdr3-frequencies` |
|
|
50
|
+
| fetch | `seqtree.load_control("mouse_trb_aa")` (uses `huggingface_hub`, an optional dependency) |
|
|
51
|
+
|
|
52
|
+
The cached index is keyed by name only, **not** by content. Delete `~/.cache/seqtree/control_*.sqtree`
|
|
53
|
+
after upgrading across a control change.
|
|
54
|
+
|
|
55
|
+
## Benchmark inputs — not shipped, passed by path
|
|
56
|
+
|
|
57
|
+
These live in sibling repositories. seqtree does not depend on either; the benchmarks that read them
|
|
58
|
+
take `--prototypes` / `--vdjdb` arguments and parse the files with the standard library.
|
|
59
|
+
|
|
60
|
+
### VDJdb slim dump
|
|
61
|
+
|
|
62
|
+
| | |
|
|
63
|
+
|---|---|
|
|
64
|
+
| what | TCR↔epitope pairs with `v.end` / `j.start` templated-flank annotations |
|
|
65
|
+
| provenance | **experimental** (curated from published studies) |
|
|
66
|
+
| origin | `../mirpy/airr_benchmark/vdjdb/vdjdb-2025-12-29/vdjdb.slim.txt.gz` |
|
|
67
|
+
| upstream | https://github.com/antigenomics/vdjdb-db — release 2025-12-29 |
|
|
68
|
+
| covers | human TRA/TRB, mouse TRA/TRB, macaque TRB. Dotted column names (`v.segm`, `antigen.epitope`) |
|
|
69
|
+
| used by | `bench/gap_rules_by_locus.py` (criterion A) |
|
|
70
|
+
|
|
71
|
+
### Per-locus prototype sets
|
|
72
|
+
|
|
73
|
+
| | |
|
|
74
|
+
|---|---|
|
|
75
|
+
| what | 10,000 clonotypes per (species, locus): `v_call`, `j_call`, `junction_aa` |
|
|
76
|
+
| provenance | **experimental** where a real control repertoire exists, otherwise **synthetic** (OLGA) |
|
|
77
|
+
| origin | `../mirpy/mir/resources/prototypes/{species}_{locus}.tsv` |
|
|
78
|
+
| covers | human TRA TRB TRG TRD IGH IGK IGL; mouse TRA TRB |
|
|
79
|
+
| regenerate | `python ../mirpy/mir/resources/prototypes/generate_prototypes.py` (committed; never rebuilt at test time) |
|
|
80
|
+
| used by | `bench/gap_rules_by_locus.py` (criteria B and C) |
|
|
81
|
+
|
|
82
|
+
Junctions, not IMGT CDR3: they include both anchors (`C…F` or `C…W`). Median lengths run from 12
|
|
83
|
+
(IGK) to 20 (IGH), with IGH reaching 50 — which is why a fixed gap-position set calibrated on
|
|
84
|
+
length-14 TRB does not transfer.
|
|
85
|
+
|
|
86
|
+
### IMGT gene library
|
|
87
|
+
|
|
88
|
+
| | |
|
|
89
|
+
|---|---|
|
|
90
|
+
| what | germline V/D/J nucleotide sequences, 2,710 (species, allele) entries |
|
|
91
|
+
| provenance | **experimental** (IMGT reference directory) |
|
|
92
|
+
| origin | `../mirpy/mir/resources/gene_library/imgt_gene_library.txt` |
|
|
93
|
+
| caveat | **V genes translate in frame 0; J genes do not** (TRBJ2-7 is frame 1, TRBJ1-1 frame 2). The companion `region_annotations.txt` carries `jcdr3_aa`, which excludes the conserved F/W — it is CDR3 convention, not junction, and it disagrees with the translated library on some alleles (TRBV1\*01, IGHV1-2\*01). |
|
|
94
|
+
|
|
95
|
+
Not currently read by any committed benchmark. Recorded because a germline-templated-flank prior was
|
|
96
|
+
built against it and **falsified**: it recovers the structurally correct block position 0.4% of the
|
|
97
|
+
time on TRA against 42.4% for a central prior.
|
|
Binary file
|