seqtree 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {seqtree-0.3.0 → seqtree-0.4.0}/CHANGELOG.md +100 -3
- {seqtree-0.3.0 → seqtree-0.4.0}/CMakeLists.txt +1 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/PKG-INFO +19 -2
- {seqtree-0.3.0 → seqtree-0.4.0}/README.md +17 -1
- {seqtree-0.3.0 → seqtree-0.4.0}/include/seqtree/seqtree.hpp +61 -2
- {seqtree-0.3.0 → seqtree-0.4.0}/pyproject.toml +4 -2
- {seqtree-0.3.0 → seqtree-0.4.0}/python/seqtree/__init__.py +2 -1
- seqtree-0.4.0/python/seqtree/control.py +321 -0
- seqtree-0.4.0/python/seqtree/pairwise.py +167 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/skills/seqtree/SKILL.md +29 -1
- {seqtree-0.3.0 → seqtree-0.4.0}/src/_bindings.cpp +103 -3
- seqtree-0.4.0/src/atomic_write.hpp +75 -0
- seqtree-0.4.0/src/blosum45.inc +32 -0
- seqtree-0.4.0/src/blosum80.inc +32 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/index.cpp +26 -19
- {seqtree-0.3.0 → seqtree-0.4.0}/src/kmer_index.cpp +26 -21
- seqtree-0.4.0/src/pairwise.cpp +354 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/substitution_matrix.cpp +22 -0
- seqtree-0.3.0/python/seqtree/control.py +0 -185
- {seqtree-0.3.0 → seqtree-0.4.0}/.gitattributes +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/.gitignore +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/LICENSE +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/ROADMAP.md +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/SOURCES.md +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/appendix/.gitignore +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/appendix/.latexmkrc +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/appendix/Makefile +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/appendix/epitope_detection.pdf +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/appendix/evalue.pdf +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/appendix/evalue.tex +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/appendix/evalue_matrix.pdf +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/appendix/mhc1_rocpr.pdf +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/appendix/mhc2_rocpr.pdf +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/appendix/refs.bib +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/examples/01_gapped_search.py +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/examples/02_sequence_dendrogram.py +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/examples/03_indel_positions.py +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/examples/04_island_profile.py +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/include/seqtree/kmer_index.hpp +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/include/seqtree/types.hpp +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/python/seqtree/evalue.py +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/python/seqtree/gapblock.py +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/python/seqtree/layout.py +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/python/seqtree/pmhc.py +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/python/seqtree/pmhc_evalue.py +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/python/seqtree/py.typed +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/python/seqtree/seeds.py +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/blosum62.inc +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/codec.cpp +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/engine_seqtm.cpp +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/engine_seqtrie.cpp +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/engines.hpp +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/gapblock.cpp +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/pam100.inc +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/pam250.inc +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/positional_matrix.cpp +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/searcher.cpp +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/structural.inc +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/trie.cpp +0 -0
- {seqtree-0.3.0 → seqtree-0.4.0}/src/trie.hpp +0 -0
|
@@ -3,6 +3,101 @@
|
|
|
3
3
|
All notable changes to `seqtree`. Dates are release dates; the project is pre-1.0, so a **minor**
|
|
4
4
|
bump may carry breaking changes.
|
|
5
5
|
|
|
6
|
+
## [0.4.0] — 2026-07-11
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`seqtree.pairwise` — Needleman-Wunsch and Smith-Waterman, so ordinary protein alignment no
|
|
11
|
+
longer needs BioPython.** Everything else in seqtree *minimises a non-negative penalty*, which
|
|
12
|
+
is what a search ball and an E-value need. These **maximise a raw log-odds similarity**, the way
|
|
13
|
+
BLAST and BioPython do, because that is what a pairwise alignment means.
|
|
14
|
+
|
|
15
|
+
| | |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `pairwise.score(q, r, matrix, mode=...)` | optimal score, `O(min(m,n))` memory |
|
|
18
|
+
| `pairwise.align(...)` | plus the aligned strings and ops |
|
|
19
|
+
| `pairwise.score_matrix(queries, refs, ...)` | dense `n × K`, GIL released, zero-copy numpy |
|
|
20
|
+
| `pairwise.dist_matrix(...)` | `d = s(a,a) + s(b,b) − 2·s(a,b)`: non-negative, zero on the diagonal |
|
|
21
|
+
|
|
22
|
+
`mode="global"` is Needleman-Wunsch, `mode="local"` Smith-Waterman, and **`gap_open == gap_extend`
|
|
23
|
+
gives linear gaps** — no separate mode. A gap run of length `L` costs `gap_open + (L-1)·gap_extend`,
|
|
24
|
+
and global charges end gaps (true NW, not semi-global).
|
|
25
|
+
|
|
26
|
+
**It is a drop-in.** `tests/python/test_pairwise.py` runs it against `Bio.Align.PairwiseAligner`
|
|
27
|
+
as an oracle over three matrices × ten gap/mode settings × sixty sequence shapes — **zero
|
|
28
|
+
disagreements**, including on real germline V genes. BioPython is a *test-only* dependency;
|
|
29
|
+
seqtree still has **zero required runtime dependencies** and never imports it.
|
|
30
|
+
|
|
31
|
+
Measured on an M3 (all-against-all, BLOSUM62, global, 11/1):
|
|
32
|
+
|
|
33
|
+
| sequence length | seqtree, 1 thread | seqtree, 16 threads | BioPython | speedup |
|
|
34
|
+
|---|---|---|---|---|
|
|
35
|
+
| 15 (a junction) | 1.7 M pairs/s | **20.1 M pairs/s** | 0.31 M pairs/s | **65×** |
|
|
36
|
+
| 90 (a germline V gene) | 72 k pairs/s | **893 k pairs/s** | 10 k pairs/s | **87×** |
|
|
37
|
+
|
|
38
|
+
- **`SubstitutionMatrix.similarity(a, b)`** — the raw signed log-odds, alongside the existing
|
|
39
|
+
non-negative `penalty(a, b)`. The Gram transform `pen = s(a,a) + s(b,b) − 2·s(a,b)` is **lossy**:
|
|
40
|
+
it forces the diagonal to zero and destroys `s(a,a)`, so a similarity cannot be recovered from a
|
|
41
|
+
penalty. Both views are now stored.
|
|
42
|
+
|
|
43
|
+
- **`SubstitutionMatrix.blosum45()` and `.blosum80()`**, and the names `"BLOSUM45"` / `"BLOSUM80"`
|
|
44
|
+
wherever a matrix name is accepted. Shallower and deeper than BLOSUM62 — for remote and close
|
|
45
|
+
homologs respectively.
|
|
46
|
+
|
|
47
|
+
## [0.3.1] — 2026-07-10 (never published; folded into 0.4.0)
|
|
48
|
+
|
|
49
|
+
> Tagged but not released to PyPI. Everything below ships in **0.4.0**, so upgrading from 0.3.0
|
|
50
|
+
> straight to 0.4.0 picks it all up. Kept as its own section because it is a distinct set of fixes.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **A cold cache shared by concurrent processes could hand back a half-written index.**
|
|
56
|
+
`Index::save` wrote straight into the destination, so for the whole duration of the write the
|
|
57
|
+
file existed but was truncated. A second process that checked `os.path.exists(cache)` in that
|
|
58
|
+
window loaded a stub and raised `RuntimeError: truncated or corrupt index`. On a 45 MB control
|
|
59
|
+
index the window is ~55 ms, and a reader racing a writer hit it **10 times out of 10**.
|
|
60
|
+
|
|
61
|
+
This is the first-use-only failure of any multi-process fan-out sharing `~/.cache`: pytest-xdist,
|
|
62
|
+
a Snakemake or Nextflow pipeline calling `load_control` in parallel, a `multiprocessing` pool.
|
|
63
|
+
Once the cache is warm it is read-only and was always safe. CI matrix jobs were never affected —
|
|
64
|
+
separate runners, separate caches.
|
|
65
|
+
|
|
66
|
+
`Index::save` and `KmerIndex::save` now serialize into a uniquely-named temporary beside the
|
|
67
|
+
destination and `rename` it into place. Rename is atomic on the same filesystem, on POSIX and
|
|
68
|
+
Windows alike, so a reader sees either the previous complete file or the new complete file and
|
|
69
|
+
never a partial one. A failed save cleans up its temporary and leaves any pre-existing index
|
|
70
|
+
intact.
|
|
71
|
+
|
|
72
|
+
- **A corrupt or stale cache now rebuilds instead of raising.** A file truncated by a full disk,
|
|
73
|
+
left by a killed process, or written by an older seqtree sent `load_control` into an exception;
|
|
74
|
+
it now falls back to rebuilding. The cache was always best-effort and now behaves that way.
|
|
75
|
+
|
|
76
|
+
- **The control cache is content-addressed, so a stale cache can no longer be served silently.**
|
|
77
|
+
The key was `control_{name}_{size}.sqtree`, which named neither the **alphabet**, nor the
|
|
78
|
+
**seed**, nor the **source data**. Three consequences, all live:
|
|
79
|
+
|
|
80
|
+
- Two calls differing only in `seed` — which must draw *different* reservoir samples — shared one
|
|
81
|
+
cache file, so the second silently received the first's sequences. Same for `alphabet`.
|
|
82
|
+
- An upgrade that changed the bundled control kept the same filename, so a warm cache served the
|
|
83
|
+
**previous release's** control. This is exactly how 0.3.0's corrected (uniform) control could be
|
|
84
|
+
masked by a stale 0.2.0 (abundance-head) cache — and why 0.3.0's notes had to ask people to
|
|
85
|
+
`rm ~/.cache/seqtree/control_*.sqtree` by hand.
|
|
86
|
+
|
|
87
|
+
The key now carries a fingerprint of the bundled asset's own bytes (or, on the download path, the
|
|
88
|
+
source and seed), so a control that changed simply misses the old cache. Superseded caches,
|
|
89
|
+
including pre-fingerprint ones from earlier releases, are deleted on the next build.
|
|
90
|
+
|
|
91
|
+
**You no longer need to clear `~/.cache/seqtree` when upgrading.** Doing so is harmless.
|
|
92
|
+
|
|
93
|
+
### Added
|
|
94
|
+
|
|
95
|
+
- **`load_control` takes an inter-process lock around build-and-save when `filelock` is available**
|
|
96
|
+
(it arrives with `huggingface_hub`). This is an optimisation, not the fix: correctness comes from
|
|
97
|
+
the atomic rename and holds with no lock at all. What the lock saves is work — without it, a cold
|
|
98
|
+
fan-out of N workers has every worker build the same 250k-clonotype index and discard N−1 of them.
|
|
99
|
+
seqtree still has **zero required runtime dependencies**; the import is guarded.
|
|
100
|
+
|
|
6
101
|
## [0.3.0] — 2026-07-10
|
|
7
102
|
|
|
8
103
|
Gap-block alignment, calibrated cutoffs, seed significance — the removal of several engine paths
|
|
@@ -20,9 +115,11 @@ E-value.
|
|
|
20
115
|
(mean n_C 110.1 vs 35.5). Both are now uniform reservoir samples over unique **productive**
|
|
21
116
|
clonotypes, seeded and shuffled so any prefix is itself a valid sub-sample.
|
|
22
117
|
|
|
23
|
-
**Every E-value moves.** Delete `~/.cache/seqtree/control_*.sqtree` after upgrading
|
|
24
|
-
|
|
25
|
-
|
|
118
|
+
**Every E-value moves.** Delete `~/.cache/seqtree/control_*.sqtree` after upgrading — a warm
|
|
119
|
+
cache from 0.2.0 would otherwise be served in place of the corrected control. (Fixed in 0.3.1:
|
|
120
|
+
the cache is now content-addressed and a stale one simply misses. Upgrading straight from 0.2.0
|
|
121
|
+
to ≥0.3.1 needs no manual step.) Numbers derived from the control are corrected throughout this
|
|
122
|
+
file, `seeds.py`, `SKILL.md` and the appendix.
|
|
26
123
|
- **Controls are filtered to productive clonotypes.** VDJtools marks out-of-frame rearrangements
|
|
27
124
|
with `_` and in-frame stops with `*`; 13.7% of the mouse TRB table is out of frame. `_` cannot be
|
|
28
125
|
repaired at the amino-acid level — VDJtools collapses a *run* of untranslatable positions into one
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: seqtree
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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>
|
|
@@ -18,6 +18,7 @@ Requires-Python: >=3.10
|
|
|
18
18
|
Provides-Extra: test
|
|
19
19
|
Requires-Dist: pytest>=7.4; extra == "test"
|
|
20
20
|
Requires-Dist: pytest-cov>=4.1; extra == "test"
|
|
21
|
+
Requires-Dist: biopython>=1.81; extra == "test"
|
|
21
22
|
Provides-Extra: bench
|
|
22
23
|
Requires-Dist: huggingface_hub; extra == "bench"
|
|
23
24
|
Requires-Dist: psutil; extra == "bench"
|
|
@@ -59,7 +60,7 @@ to their own payloads (V gene, MHC, counts) and filter.
|
|
|
59
60
|
|
|
60
61
|
Beyond search, seqtree ships:
|
|
61
62
|
|
|
62
|
-
- **Substitution matrices** — built-in `identity`, `BLOSUM62`, `PAM250`, `PAM100`, and `structural`
|
|
63
|
+
- **Substitution matrices** — built-in `identity`, `BLOSUM45`, `BLOSUM62`, `BLOSUM80`, `PAM250`, `PAM100`, and `structural`
|
|
63
64
|
— a **Miyazawa–Jernigan interaction-strength** matrix: each residue's strength `q(a)=mean_b e(a,b)`
|
|
64
65
|
is read off the MJ contact potential, so substitutions between residues of like interaction strength
|
|
65
66
|
are cheap. It separates strong (hydrophobic `F W C L Y M I V`) from weak (polar/charged
|
|
@@ -81,6 +82,13 @@ Beyond search, seqtree ships:
|
|
|
81
82
|
sequence score alone cannot. `score_matrix` scores a whole query set against a whole reference
|
|
82
83
|
set in one GIL-released C++ call (**532 M pairs/s** on 16 cores; `numpy.asarray` wraps the
|
|
83
84
|
result with no copy), the shape a prototype-distance embedding needs.
|
|
85
|
+
- **Pairwise alignment without BioPython** — `seqtree.pairwise` is Needleman–Wunsch
|
|
86
|
+
(`mode="global"`) and Smith–Waterman (`mode="local"`) with affine or linear gaps, on the raw
|
|
87
|
+
log-odds scale. It is a **drop-in for `Bio.Align.PairwiseAligner`** — verified against it as an
|
|
88
|
+
oracle across three matrices, ten gap/mode settings and sixty sequence shapes with **zero
|
|
89
|
+
disagreements** — and **65–87× faster**, since there is no Python in the per-pair loop.
|
|
90
|
+
`dist_matrix` gives `d = s(a,a) + s(b,b) − 2·s(a,b)` directly. BioPython is a *test-only*
|
|
91
|
+
dependency; seqtree still needs nothing at runtime.
|
|
84
92
|
- **Island profiles** — `IslandProfile.fit` builds a position weight matrix over a set of
|
|
85
93
|
frame-aligned junctions (an *island*) and scores a query column by column against the island
|
|
86
94
|
consensus, as a non-negative penalty that flows through `threshold_for_evalue` unchanged. At a
|
|
@@ -164,6 +172,15 @@ distances = np.asarray(sm) # (len(clonotypes), len(prot
|
|
|
164
172
|
# a position weight matrix over an island, still a non-negative penalty (feeds threshold_for_evalue)
|
|
165
173
|
profile = IslandProfile.fit(island_members)
|
|
166
174
|
profile.score("CASSLGQAYEQYF") # 0 on the consensus, > 0 for deviations
|
|
175
|
+
|
|
176
|
+
# ordinary pairwise alignment -- Needleman-Wunsch / Smith-Waterman, no BioPython
|
|
177
|
+
from seqtree.pairwise import align, score, dist_matrix
|
|
178
|
+
score("CASSLGQAYEQYF", "CASSPGQAYEQF", mat) # global, BLAST defaults (11/1)
|
|
179
|
+
score("WWWAAAWWW", "KKKAAAKKK", mat, mode="local") # Smith-Waterman
|
|
180
|
+
score("AAA", "AAAAA", mat, gap_open=5, gap_extend=5) # linear gaps: open == extend
|
|
181
|
+
aln = align("CASSLGQAYEQYF", "CASSPGQAYEQF", mat) # + aligned strings and ops
|
|
182
|
+
|
|
183
|
+
d = np.asarray(dist_matrix(v_genes, v_genes, mat, threads=0)) # s(a,a)+s(b,b)-2s(a,b), zero diagonal
|
|
167
184
|
```
|
|
168
185
|
|
|
169
186
|
## Tests
|
|
@@ -26,7 +26,7 @@ to their own payloads (V gene, MHC, counts) and filter.
|
|
|
26
26
|
|
|
27
27
|
Beyond search, seqtree ships:
|
|
28
28
|
|
|
29
|
-
- **Substitution matrices** — built-in `identity`, `BLOSUM62`, `PAM250`, `PAM100`, and `structural`
|
|
29
|
+
- **Substitution matrices** — built-in `identity`, `BLOSUM45`, `BLOSUM62`, `BLOSUM80`, `PAM250`, `PAM100`, and `structural`
|
|
30
30
|
— a **Miyazawa–Jernigan interaction-strength** matrix: each residue's strength `q(a)=mean_b e(a,b)`
|
|
31
31
|
is read off the MJ contact potential, so substitutions between residues of like interaction strength
|
|
32
32
|
are cheap. It separates strong (hydrophobic `F W C L Y M I V`) from weak (polar/charged
|
|
@@ -48,6 +48,13 @@ Beyond search, seqtree ships:
|
|
|
48
48
|
sequence score alone cannot. `score_matrix` scores a whole query set against a whole reference
|
|
49
49
|
set in one GIL-released C++ call (**532 M pairs/s** on 16 cores; `numpy.asarray` wraps the
|
|
50
50
|
result with no copy), the shape a prototype-distance embedding needs.
|
|
51
|
+
- **Pairwise alignment without BioPython** — `seqtree.pairwise` is Needleman–Wunsch
|
|
52
|
+
(`mode="global"`) and Smith–Waterman (`mode="local"`) with affine or linear gaps, on the raw
|
|
53
|
+
log-odds scale. It is a **drop-in for `Bio.Align.PairwiseAligner`** — verified against it as an
|
|
54
|
+
oracle across three matrices, ten gap/mode settings and sixty sequence shapes with **zero
|
|
55
|
+
disagreements** — and **65–87× faster**, since there is no Python in the per-pair loop.
|
|
56
|
+
`dist_matrix` gives `d = s(a,a) + s(b,b) − 2·s(a,b)` directly. BioPython is a *test-only*
|
|
57
|
+
dependency; seqtree still needs nothing at runtime.
|
|
51
58
|
- **Island profiles** — `IslandProfile.fit` builds a position weight matrix over a set of
|
|
52
59
|
frame-aligned junctions (an *island*) and scores a query column by column against the island
|
|
53
60
|
consensus, as a non-negative penalty that flows through `threshold_for_evalue` unchanged. At a
|
|
@@ -131,6 +138,15 @@ distances = np.asarray(sm) # (len(clonotypes), len(prot
|
|
|
131
138
|
# a position weight matrix over an island, still a non-negative penalty (feeds threshold_for_evalue)
|
|
132
139
|
profile = IslandProfile.fit(island_members)
|
|
133
140
|
profile.score("CASSLGQAYEQYF") # 0 on the consensus, > 0 for deviations
|
|
141
|
+
|
|
142
|
+
# ordinary pairwise alignment -- Needleman-Wunsch / Smith-Waterman, no BioPython
|
|
143
|
+
from seqtree.pairwise import align, score, dist_matrix
|
|
144
|
+
score("CASSLGQAYEQYF", "CASSPGQAYEQF", mat) # global, BLAST defaults (11/1)
|
|
145
|
+
score("WWWAAAWWW", "KKKAAAKKK", mat, mode="local") # Smith-Waterman
|
|
146
|
+
score("AAA", "AAAAA", mat, gap_open=5, gap_extend=5) # linear gaps: open == extend
|
|
147
|
+
aln = align("CASSLGQAYEQYF", "CASSPGQAYEQF", mat) # + aligned strings and ops
|
|
148
|
+
|
|
149
|
+
d = np.asarray(dist_matrix(v_genes, v_genes, mat, threads=0)) # s(a,a)+s(b,b)-2s(a,b), zero diagonal
|
|
134
150
|
```
|
|
135
151
|
|
|
136
152
|
## Tests
|
|
@@ -30,31 +30,48 @@ private:
|
|
|
30
30
|
char dec_[32];
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// A substitution matrix in two views.
|
|
34
|
+
//
|
|
35
|
+
// penalty(a,b) non-negative, penalty(a,a)==0. What search and the E-value ball need:
|
|
36
|
+
// a distance-like cost that is minimised.
|
|
37
|
+
// similarity(a,b) the raw log-odds, signed. What Needleman-Wunsch and Smith-Waterman need:
|
|
38
|
+
// a score that is maximised.
|
|
39
|
+
//
|
|
40
|
+
// The penalty is the Gram / squared-distance transform of the similarity, which is LOSSY --
|
|
41
|
+
// it forces the diagonal to zero, destroying s(a,a) -- so the raw grid is retained rather
|
|
42
|
+
// than reconstructed.
|
|
34
43
|
class SubstitutionMatrix {
|
|
35
44
|
public:
|
|
36
45
|
// Unit cost: 0 for a match, 1 for a mismatch (this is the "identity" matrix).
|
|
37
46
|
static SubstitutionMatrix unit(uint8_t size);
|
|
38
47
|
// Built-in amino-acid matrices (valid only for the AminoAcid codec order).
|
|
39
48
|
static SubstitutionMatrix blosum62();
|
|
49
|
+
static SubstitutionMatrix blosum45(); // shallower: remote homologs
|
|
50
|
+
static SubstitutionMatrix blosum80(); // deeper: close homologs
|
|
40
51
|
static SubstitutionMatrix pam250(); // EMBOSS EPAM250 (NCBI log-odds)
|
|
41
52
|
static SubstitutionMatrix pam100(); // EMBOSS EPAM100 (NCBI log-odds)
|
|
42
53
|
static SubstitutionMatrix structural(); // Miyazawa-Jernigan interaction-strength similarity
|
|
43
54
|
// Convert a similarity matrix (row-major, size*size) to penalties via the Gram /
|
|
44
|
-
// squared-distance transform pen[a][b] = sim[a][a] + sim[b][b] - 2*sim[a][b].
|
|
55
|
+
// squared-distance transform pen[a][b] = sim[a][a] + sim[b][b] - 2*sim[a][b]. The raw
|
|
56
|
+
// similarity is kept too.
|
|
45
57
|
static SubstitutionMatrix from_similarity(uint8_t size, const int32_t* sim);
|
|
46
58
|
|
|
47
59
|
uint8_t size() const { return size_; }
|
|
48
60
|
int32_t penalty(uint8_t a, uint8_t b) const { return pen_[a * size_ + b]; }
|
|
61
|
+
int32_t similarity(uint8_t a, uint8_t b) const { return sim_[a * size_ + b]; }
|
|
49
62
|
// Median penalty over all mismatched symbol pairs -- the natural unit of this matrix.
|
|
50
63
|
// Gap costs must be on this scale: the Gram transform makes a typical BLOSUM62 mismatch
|
|
51
64
|
// cost ~15, so the default gap_open of 1 would make gaps ~15x cheaper than substitutions
|
|
52
65
|
// and the aligner would gap rather than substitute. Use gap_open ~ 1-2 * scale().
|
|
66
|
+
//
|
|
67
|
+
// This is the PENALTY scale. A similarity-scoring aligner lives on the raw log-odds scale
|
|
68
|
+
// instead, where the usual BLAST defaults (gap_open 11, gap_extend 1) apply.
|
|
53
69
|
int32_t scale() const;
|
|
54
70
|
|
|
55
71
|
private:
|
|
56
72
|
uint8_t size_ = 0;
|
|
57
73
|
std::vector<int32_t> pen_;
|
|
74
|
+
std::vector<int32_t> sim_;
|
|
58
75
|
};
|
|
59
76
|
|
|
60
77
|
// Per-position substitution penalties pen(pos, a, b) over a fixed frame width W.
|
|
@@ -149,6 +166,48 @@ std::vector<std::vector<Hit>> pairwise_batch(const std::vector<std::string>& a,
|
|
|
149
166
|
const std::vector<std::string>& b,
|
|
150
167
|
Alphabet, const SearchParams&, int threads = 0);
|
|
151
168
|
|
|
169
|
+
// ---------------------------------------------------------------------------------------
|
|
170
|
+
// Pairwise similarity alignment: Needleman-Wunsch (global) and Smith-Waterman (local).
|
|
171
|
+
//
|
|
172
|
+
// These MAXIMISE the raw log-odds similarity, unlike everything else in this header, which
|
|
173
|
+
// minimises a non-negative penalty. They exist so a caller does not need BioPython for an
|
|
174
|
+
// ordinary protein alignment; the conventions match Bio.Align.PairwiseAligner exactly:
|
|
175
|
+
//
|
|
176
|
+
// * a gap of length L costs gap_open + (L-1)*gap_extend (so gap_open is the cost of the
|
|
177
|
+
// first gap column, not an extra charge on top of it);
|
|
178
|
+
// * gap_open == gap_extend gives LINEAR gaps -- no separate mode is needed;
|
|
179
|
+
// * in global mode END gaps are charged like any other (true Needleman-Wunsch, not
|
|
180
|
+
// semi-global / overlap);
|
|
181
|
+
// * in local mode the score never drops below zero and the best local cell wins
|
|
182
|
+
// (Smith-Waterman).
|
|
183
|
+
//
|
|
184
|
+
// Gap costs are given as POSITIVE magnitudes and subtracted. BLAST's protein defaults are
|
|
185
|
+
// gap_open = 11, gap_extend = 1.
|
|
186
|
+
enum class AlignMode { Global, Local };
|
|
187
|
+
|
|
188
|
+
// Optimal score only: O(min(m,n)) memory, no traceback. This is the hot path.
|
|
189
|
+
int32_t align_score(std::string_view query, std::string_view ref, const SubstitutionMatrix&,
|
|
190
|
+
Alphabet, AlignMode, int32_t gap_open, int32_t gap_extend);
|
|
191
|
+
|
|
192
|
+
// Optimal score plus the aligned strings and ops. O(m*n) memory for the traceback.
|
|
193
|
+
Alignment align_pair(std::string_view query, std::string_view ref, const SubstitutionMatrix&,
|
|
194
|
+
Alphabet, AlignMode, int32_t gap_open, int32_t gap_extend);
|
|
195
|
+
|
|
196
|
+
// Dense N*K similarity matrix, row-major (row i is queries[i] vs every ref). Parallel;
|
|
197
|
+
// threads <= 0 => hardware_concurrency.
|
|
198
|
+
std::vector<int32_t> align_score_matrix(const std::vector<std::string>& queries,
|
|
199
|
+
const std::vector<std::string>& refs,
|
|
200
|
+
const SubstitutionMatrix&, Alphabet, AlignMode,
|
|
201
|
+
int32_t gap_open, int32_t gap_extend, int threads = 0);
|
|
202
|
+
|
|
203
|
+
// Dense N*K distance matrix d(a,b) = s(a,a) + s(b,b) - 2*s(a,b) >= 0, the Gram transform
|
|
204
|
+
// applied at the SEQUENCE level to the alignment scores above. This is the distance a
|
|
205
|
+
// prototype-embedding wants, and the one BioPython users hand-roll.
|
|
206
|
+
std::vector<int32_t> align_dist_matrix(const std::vector<std::string>& queries,
|
|
207
|
+
const std::vector<std::string>& refs,
|
|
208
|
+
const SubstitutionMatrix&, Alphabet, AlignMode,
|
|
209
|
+
int32_t gap_open, int32_t gap_extend, int threads = 0);
|
|
210
|
+
|
|
152
211
|
// Dense N*K single-gap-block penalty matrix, row-major (row i is queries[i] vs every ref).
|
|
153
212
|
// Unlike pairwise_batch this is exhaustive: no budget, no trie, every cell scored.
|
|
154
213
|
//
|
|
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "seqtree"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Fast fuzzy search over biological sequences (C++ core, Python bindings)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -22,7 +22,9 @@ classifiers = [
|
|
|
22
22
|
dependencies = []
|
|
23
23
|
|
|
24
24
|
[project.optional-dependencies]
|
|
25
|
-
|
|
25
|
+
# biopython is the ORACLE for seqtree.pairwise (NW/SW) -- a test-only dependency.
|
|
26
|
+
# seqtree itself has no runtime dependencies and must never import it.
|
|
27
|
+
test = ["pytest>=7.4", "pytest-cov>=4.1", "biopython>=1.81"]
|
|
26
28
|
bench = ["huggingface_hub", "psutil"]
|
|
27
29
|
docs = ["sphinx", "pydata-sphinx-theme", "nbsphinx"]
|
|
28
30
|
control = ["huggingface_hub"] # only needed to download controls larger than the bundled subset
|
|
@@ -24,7 +24,7 @@ from ._core import (
|
|
|
24
24
|
)
|
|
25
25
|
from .control import load_control
|
|
26
26
|
from .evalue import evalues, thetas_from_scores, threshold_for_evalue
|
|
27
|
-
from . import gapblock, layout, pmhc, seeds
|
|
27
|
+
from . import gapblock, layout, pairwise, pmhc, seeds
|
|
28
28
|
from .gapblock import (
|
|
29
29
|
GapBlockIndex, IslandProfile, ScoreMatrix, central_prior, embed_in_frame, frame_prior,
|
|
30
30
|
gapblock_score, positions_prior, profile_prior, score_matrix,
|
|
@@ -35,6 +35,7 @@ from .pmhc import PMHCStore, find_mimics
|
|
|
35
35
|
__all__ = [
|
|
36
36
|
"gapblock",
|
|
37
37
|
"layout",
|
|
38
|
+
"pairwise",
|
|
38
39
|
"pmhc",
|
|
39
40
|
"seeds",
|
|
40
41
|
"GapBlockIndex",
|