seqtree 0.4.0__tar.gz → 0.5.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.4.0 → seqtree-0.5.0}/CHANGELOG.md +22 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/CMakeLists.txt +1 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/PKG-INFO +13 -1
- {seqtree-0.4.0 → seqtree-0.5.0}/README.md +12 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/include/seqtree/seqtree.hpp +20 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/pyproject.toml +1 -1
- {seqtree-0.4.0 → seqtree-0.5.0}/python/seqtree/__init__.py +2 -1
- seqtree-0.5.0/python/seqtree/distance.py +119 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/_bindings.cpp +39 -0
- seqtree-0.5.0/src/distance.cpp +111 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/.gitattributes +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/.gitignore +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/LICENSE +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/ROADMAP.md +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/SOURCES.md +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/appendix/.gitignore +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/appendix/.latexmkrc +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/appendix/Makefile +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/appendix/epitope_detection.pdf +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/appendix/evalue.pdf +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/appendix/evalue.tex +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/appendix/evalue_matrix.pdf +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/appendix/mhc1_rocpr.pdf +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/appendix/mhc2_rocpr.pdf +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/appendix/refs.bib +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/examples/01_gapped_search.py +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/examples/02_sequence_dendrogram.py +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/examples/03_indel_positions.py +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/examples/04_island_profile.py +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/include/seqtree/kmer_index.hpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/include/seqtree/types.hpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/python/seqtree/control.py +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/python/seqtree/evalue.py +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/python/seqtree/gapblock.py +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/python/seqtree/layout.py +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/python/seqtree/pairwise.py +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/python/seqtree/pmhc.py +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/python/seqtree/pmhc_evalue.py +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/python/seqtree/py.typed +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/python/seqtree/seeds.py +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/skills/seqtree/SKILL.md +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/atomic_write.hpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/blosum45.inc +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/blosum62.inc +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/blosum80.inc +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/codec.cpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/engine_seqtm.cpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/engine_seqtrie.cpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/engines.hpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/gapblock.cpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/index.cpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/kmer_index.cpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/pairwise.cpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/pam100.inc +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/pam250.inc +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/positional_matrix.cpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/searcher.cpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/structural.inc +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/substitution_matrix.cpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/trie.cpp +0 -0
- {seqtree-0.4.0 → seqtree-0.5.0}/src/trie.hpp +0 -0
|
@@ -3,6 +3,28 @@
|
|
|
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.5.0] — 2026-07-16
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`seqtree.distance` — plain Hamming and Levenshtein distances, in C++, without a dependency.**
|
|
11
|
+
The *unweighted* corner of the library: unit costs, no substitution matrix, no gap model, no
|
|
12
|
+
alphabet. When all you need is "how many edits apart are these two strings", you should not have
|
|
13
|
+
to build a `SubstitutionMatrix` or add `python-Levenshtein` / `rapidfuzz`. seqtree still needs
|
|
14
|
+
nothing at runtime.
|
|
15
|
+
|
|
16
|
+
| | |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `distance.hamming(a, b)` | differing positions; **equal length only** (raises `ValueError` otherwise) |
|
|
19
|
+
| `distance.levenshtein(a, b)` | insertions + deletions + substitutions, each cost 1, `O(min(m,n))` memory |
|
|
20
|
+
| `distance.hamming_matrix(a, b, threads=0)` | dense `len(a) × len(b)` int32, GIL released, zero-copy numpy |
|
|
21
|
+
| `distance.levenshtein_matrix(a, b, threads=0)` | same, for mixed-length sequences |
|
|
22
|
+
|
|
23
|
+
Comparison is **case-sensitive**, byte for byte — the one place the library does not fold case,
|
|
24
|
+
since a generic string distance should report the difference it is asked about. For a *weighted*
|
|
25
|
+
alignment (a matrix, affine gaps, local mode) use `seqtree.pairwise` instead. Checked against
|
|
26
|
+
pure-Python oracles over random data in `tests/python/test_distance.py`.
|
|
27
|
+
|
|
6
28
|
## [0.4.0] — 2026-07-11
|
|
7
29
|
|
|
8
30
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: seqtree
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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>
|
|
@@ -89,6 +89,12 @@ Beyond search, seqtree ships:
|
|
|
89
89
|
disagreements** — and **65–87× faster**, since there is no Python in the per-pair loop.
|
|
90
90
|
`dist_matrix` gives `d = s(a,a) + s(b,b) − 2·s(a,b)` directly. BioPython is a *test-only*
|
|
91
91
|
dependency; seqtree still needs nothing at runtime.
|
|
92
|
+
- **Plain edit distances** — `seqtree.distance` is unweighted `hamming` and `levenshtein` (unit
|
|
93
|
+
costs, no matrix, no alphabet) for when you just need a number, not a scored alignment.
|
|
94
|
+
`hamming_matrix` / `levenshtein_matrix` score a whole set against a whole set in one GIL-released
|
|
95
|
+
C++ call (`numpy.asarray` wraps the result with no copy) — no `python-Levenshtein` or `rapidfuzz`
|
|
96
|
+
dependency needed. Hamming requires equal lengths (it raises otherwise); comparison is
|
|
97
|
+
case-sensitive.
|
|
92
98
|
- **Island profiles** — `IslandProfile.fit` builds a position weight matrix over a set of
|
|
93
99
|
frame-aligned junctions (an *island*) and scores a query column by column against the island
|
|
94
100
|
consensus, as a non-negative penalty that flows through `threshold_for_evalue` unchanged. At a
|
|
@@ -181,6 +187,12 @@ score("AAA", "AAAAA", mat, gap_open=5, gap_extend=5) # linear gaps: op
|
|
|
181
187
|
aln = align("CASSLGQAYEQYF", "CASSPGQAYEQF", mat) # + aligned strings and ops
|
|
182
188
|
|
|
183
189
|
d = np.asarray(dist_matrix(v_genes, v_genes, mat, threads=0)) # s(a,a)+s(b,b)-2s(a,b), zero diagonal
|
|
190
|
+
|
|
191
|
+
# plain edit distances -- unweighted Hamming / Levenshtein, no matrix, no dependency
|
|
192
|
+
from seqtree.distance import hamming, levenshtein, hamming_matrix, levenshtein_matrix
|
|
193
|
+
hamming("CASSLGQYF", "CASSPGQYF") # 1 (equal length only)
|
|
194
|
+
levenshtein("kitten", "sitting") # 3
|
|
195
|
+
h = np.asarray(hamming_matrix(umis, umis, threads=0)) # (len, len) int32, zero-copy
|
|
184
196
|
```
|
|
185
197
|
|
|
186
198
|
## Tests
|
|
@@ -55,6 +55,12 @@ Beyond search, seqtree ships:
|
|
|
55
55
|
disagreements** — and **65–87× faster**, since there is no Python in the per-pair loop.
|
|
56
56
|
`dist_matrix` gives `d = s(a,a) + s(b,b) − 2·s(a,b)` directly. BioPython is a *test-only*
|
|
57
57
|
dependency; seqtree still needs nothing at runtime.
|
|
58
|
+
- **Plain edit distances** — `seqtree.distance` is unweighted `hamming` and `levenshtein` (unit
|
|
59
|
+
costs, no matrix, no alphabet) for when you just need a number, not a scored alignment.
|
|
60
|
+
`hamming_matrix` / `levenshtein_matrix` score a whole set against a whole set in one GIL-released
|
|
61
|
+
C++ call (`numpy.asarray` wraps the result with no copy) — no `python-Levenshtein` or `rapidfuzz`
|
|
62
|
+
dependency needed. Hamming requires equal lengths (it raises otherwise); comparison is
|
|
63
|
+
case-sensitive.
|
|
58
64
|
- **Island profiles** — `IslandProfile.fit` builds a position weight matrix over a set of
|
|
59
65
|
frame-aligned junctions (an *island*) and scores a query column by column against the island
|
|
60
66
|
consensus, as a non-negative penalty that flows through `threshold_for_evalue` unchanged. At a
|
|
@@ -147,6 +153,12 @@ score("AAA", "AAAAA", mat, gap_open=5, gap_extend=5) # linear gaps: op
|
|
|
147
153
|
aln = align("CASSLGQAYEQYF", "CASSPGQAYEQF", mat) # + aligned strings and ops
|
|
148
154
|
|
|
149
155
|
d = np.asarray(dist_matrix(v_genes, v_genes, mat, threads=0)) # s(a,a)+s(b,b)-2s(a,b), zero diagonal
|
|
156
|
+
|
|
157
|
+
# plain edit distances -- unweighted Hamming / Levenshtein, no matrix, no dependency
|
|
158
|
+
from seqtree.distance import hamming, levenshtein, hamming_matrix, levenshtein_matrix
|
|
159
|
+
hamming("CASSLGQYF", "CASSPGQYF") # 1 (equal length only)
|
|
160
|
+
levenshtein("kitten", "sitting") # 3
|
|
161
|
+
h = np.asarray(hamming_matrix(umis, umis, threads=0)) # (len, len) int32, zero-copy
|
|
150
162
|
```
|
|
151
163
|
|
|
152
164
|
## Tests
|
|
@@ -221,4 +221,24 @@ std::vector<int32_t> gapblock_matrix(const std::vector<std::string>& queries,
|
|
|
221
221
|
int32_t gap_extend, const std::vector<int32_t>& prior,
|
|
222
222
|
uint32_t prior_width, int threads = 0);
|
|
223
223
|
|
|
224
|
+
// ---------------------------------------------------------------------------------------
|
|
225
|
+
// Plain string edit distances on raw characters, unit costs. Deliberately alphabet-agnostic:
|
|
226
|
+
// no Codec, no substitution matrix, no gap model -- the distance you want when you just need a
|
|
227
|
+
// number, not a scored alignment. Comparison is byte-for-byte and CASE-SENSITIVE; unlike the
|
|
228
|
+
// search Codec, these do not fold case.
|
|
229
|
+
//
|
|
230
|
+
// hamming -- number of differing positions; defined only for EQUAL-length sequences and
|
|
231
|
+
// throws std::invalid_argument otherwise;
|
|
232
|
+
// levenshtein -- classic insertion/deletion/substitution distance, each edit cost 1,
|
|
233
|
+
// O(min(m,n)) memory.
|
|
234
|
+
int hamming(std::string_view a, std::string_view b);
|
|
235
|
+
int levenshtein(std::string_view a, std::string_view b);
|
|
236
|
+
|
|
237
|
+
// Dense N*K distance matrices, row-major (row i is a[i] vs every b[k]). Parallel;
|
|
238
|
+
// threads <= 0 => hardware_concurrency. hamming_matrix throws on any length-mismatched pair.
|
|
239
|
+
std::vector<int32_t> hamming_matrix(const std::vector<std::string>& a,
|
|
240
|
+
const std::vector<std::string>& b, int threads = 0);
|
|
241
|
+
std::vector<int32_t> levenshtein_matrix(const std::vector<std::string>& a,
|
|
242
|
+
const std::vector<std::string>& b, int threads = 0);
|
|
243
|
+
|
|
224
244
|
} // namespace seqtree
|
|
@@ -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, pairwise, pmhc, seeds
|
|
27
|
+
from . import distance, 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,
|
|
@@ -33,6 +33,7 @@ from .seeds import SeedIndex, core_kmers
|
|
|
33
33
|
from .pmhc import PMHCStore, find_mimics
|
|
34
34
|
|
|
35
35
|
__all__ = [
|
|
36
|
+
"distance",
|
|
36
37
|
"gapblock",
|
|
37
38
|
"layout",
|
|
38
39
|
"pairwise",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Plain string edit distances: Hamming and Levenshtein, in C++, without a dependency.
|
|
2
|
+
|
|
3
|
+
These are the *unweighted* distances -- unit costs, no substitution matrix, no gap model, no
|
|
4
|
+
alphabet. That is the whole point: when all you need is "how many edits apart are these two
|
|
5
|
+
strings", you should not have to build an :class:`~seqtree.SubstitutionMatrix` or reach for
|
|
6
|
+
``python-Levenshtein`` / ``rapidfuzz``. seqtree still needs nothing at runtime.
|
|
7
|
+
|
|
8
|
+
* :func:`hamming` -- number of differing positions; defined only for **equal-length** sequences,
|
|
9
|
+
and raises :class:`ValueError` otherwise;
|
|
10
|
+
* :func:`levenshtein` -- the classic insertion / deletion / substitution edit distance, each edit
|
|
11
|
+
costing 1;
|
|
12
|
+
* :func:`hamming_matrix` / :func:`levenshtein_matrix` -- every ``a`` against every ``b`` in one
|
|
13
|
+
GIL-released, multi-threaded C++ call, returned as a zero-copy :class:`~seqtree.ScoreMatrix`.
|
|
14
|
+
|
|
15
|
+
Comparison is **case-sensitive**, byte for byte -- unlike the search engines, which fold case.
|
|
16
|
+
For a *weighted* alignment (a substitution matrix, affine gaps, local mode), use
|
|
17
|
+
:mod:`seqtree.pairwise` instead.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> from seqtree.distance import hamming, levenshtein
|
|
21
|
+
>>> hamming("CASSLGQYF", "CASSPGQYF")
|
|
22
|
+
1
|
|
23
|
+
>>> levenshtein("kitten", "sitting")
|
|
24
|
+
3
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from collections.abc import Sequence
|
|
29
|
+
|
|
30
|
+
from ._core import ScoreMatrix
|
|
31
|
+
from ._core import hamming as _hamming
|
|
32
|
+
from ._core import hamming_matrix as _hamming_matrix
|
|
33
|
+
from ._core import levenshtein as _levenshtein
|
|
34
|
+
from ._core import levenshtein_matrix as _levenshtein_matrix
|
|
35
|
+
|
|
36
|
+
__all__ = ["hamming", "levenshtein", "hamming_matrix", "levenshtein_matrix"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def hamming(a: str, b: str) -> int:
|
|
40
|
+
"""Number of positions at which ``a`` and ``b`` differ.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
a: First sequence.
|
|
44
|
+
b: Second sequence, of the **same length** as ``a``.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The count of differing positions (0 when identical).
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If ``a`` and ``b`` have different lengths -- Hamming distance is undefined
|
|
51
|
+
for unequal lengths; use :func:`levenshtein` for that.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> hamming("AAAA", "AAAA")
|
|
55
|
+
0
|
|
56
|
+
>>> hamming("AAAA", "ATAT")
|
|
57
|
+
2
|
|
58
|
+
"""
|
|
59
|
+
return _hamming(a, b)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def levenshtein(a: str, b: str) -> int:
|
|
63
|
+
"""Edit distance: fewest single-character insert / delete / substitute steps from ``a`` to ``b``.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
a: First sequence.
|
|
67
|
+
b: Second sequence; may be any length.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The edit distance (0 when identical, ``max(len(a), len(b))`` at most).
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
>>> levenshtein("flaw", "lawn")
|
|
74
|
+
2
|
|
75
|
+
>>> levenshtein("CASSLGQAYEQYF", "CASSPGQAYEQF")
|
|
76
|
+
2
|
|
77
|
+
"""
|
|
78
|
+
return _levenshtein(a, b)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def hamming_matrix(a: Sequence[str], b: Sequence[str], threads: int = 0) -> ScoreMatrix:
|
|
82
|
+
"""Hamming distance of every ``a`` against every ``b``, in parallel C++.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
a: Query sequences (the rows).
|
|
86
|
+
b: Reference sequences (the columns).
|
|
87
|
+
threads: Worker threads; ``0`` uses all cores.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A :class:`~seqtree.ScoreMatrix` of shape ``(len(a), len(b))`` of int32 distances.
|
|
91
|
+
``numpy.asarray`` wraps it without copying.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If any ``(a[i], b[k])`` pair has mismatched lengths.
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
>>> import numpy as np
|
|
98
|
+
>>> d = np.asarray(hamming_matrix(["AAAA", "AAAT"], ["AAAA", "TTTT"]))
|
|
99
|
+
>>> d.tolist()
|
|
100
|
+
[[0, 4], [1, 3]]
|
|
101
|
+
"""
|
|
102
|
+
return _hamming_matrix(list(a), list(b), threads=threads)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def levenshtein_matrix(a: Sequence[str], b: Sequence[str], threads: int = 0) -> ScoreMatrix:
|
|
106
|
+
"""Levenshtein distance of every ``a`` against every ``b``, in parallel C++.
|
|
107
|
+
|
|
108
|
+
Unlike :func:`hamming_matrix`, sequences may differ in length freely.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
a: Query sequences (the rows).
|
|
112
|
+
b: Reference sequences (the columns).
|
|
113
|
+
threads: Worker threads; ``0`` uses all cores.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
A :class:`~seqtree.ScoreMatrix` of shape ``(len(a), len(b))`` of int32 distances,
|
|
117
|
+
zero-copy through ``numpy.asarray``.
|
|
118
|
+
"""
|
|
119
|
+
return _levenshtein_matrix(list(a), list(b), threads=threads)
|
|
@@ -575,6 +575,45 @@ PYBIND11_MODULE(_core, m) {
|
|
|
575
575
|
"`prior` is the gap prior flattened to [m][d][i]; see seqtree.gapblock.score_matrix, "
|
|
576
576
|
"which builds it for you.");
|
|
577
577
|
|
|
578
|
+
m.def("hamming", &hamming, py::arg("a"), py::arg("b"),
|
|
579
|
+
"Hamming distance: the number of positions at which two EQUAL-length sequences differ. "
|
|
580
|
+
"Raises ValueError on a length mismatch. Case-sensitive, byte-for-byte.");
|
|
581
|
+
|
|
582
|
+
m.def("levenshtein", &levenshtein, py::arg("a"), py::arg("b"),
|
|
583
|
+
"Levenshtein (edit) distance: the minimum number of single-character insertions, "
|
|
584
|
+
"deletions, and substitutions to turn `a` into `b`, each cost 1. Case-sensitive.");
|
|
585
|
+
|
|
586
|
+
m.def(
|
|
587
|
+
"hamming_matrix",
|
|
588
|
+
[](const std::vector<std::string>& a, const std::vector<std::string>& b, int threads) {
|
|
589
|
+
ScoreMatrix out;
|
|
590
|
+
out.rows = a.size();
|
|
591
|
+
out.cols = b.size();
|
|
592
|
+
{
|
|
593
|
+
py::gil_scoped_release release;
|
|
594
|
+
out.data = hamming_matrix(a, b, threads);
|
|
595
|
+
}
|
|
596
|
+
return out;
|
|
597
|
+
},
|
|
598
|
+
py::arg("a"), py::arg("b"), py::arg("threads") = 0,
|
|
599
|
+
"Dense (len(a), len(b)) int32 Hamming-distance matrix, GIL released. Raises ValueError "
|
|
600
|
+
"if any pair has mismatched lengths.");
|
|
601
|
+
|
|
602
|
+
m.def(
|
|
603
|
+
"levenshtein_matrix",
|
|
604
|
+
[](const std::vector<std::string>& a, const std::vector<std::string>& b, int threads) {
|
|
605
|
+
ScoreMatrix out;
|
|
606
|
+
out.rows = a.size();
|
|
607
|
+
out.cols = b.size();
|
|
608
|
+
{
|
|
609
|
+
py::gil_scoped_release release;
|
|
610
|
+
out.data = levenshtein_matrix(a, b, threads);
|
|
611
|
+
}
|
|
612
|
+
return out;
|
|
613
|
+
},
|
|
614
|
+
py::arg("a"), py::arg("b"), py::arg("threads") = 0,
|
|
615
|
+
"Dense (len(a), len(b)) int32 Levenshtein-distance matrix, GIL released.");
|
|
616
|
+
|
|
578
617
|
py::class_<Candidate>(m, "Candidate",
|
|
579
618
|
"A seed-and-gather hit: peptide_id, shared_kmers (distinct query k-mers "
|
|
580
619
|
"that hit it), best_score. Iterable as (peptide_id, shared_kmers, best_score).")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Plain string edit distances -- Hamming and Levenshtein -- on raw characters, with unit
|
|
2
|
+
// costs. Deliberately alphabet-agnostic: no Codec, no substitution matrix, no gap model. This
|
|
3
|
+
// is the distance you want when you just need a number, not a scored alignment. Everything
|
|
4
|
+
// else in this library scores against a matrix; these two do not, so they live apart from
|
|
5
|
+
// pairwise.cpp even though the threading plumbing is the same.
|
|
6
|
+
//
|
|
7
|
+
// Comparison is byte-for-byte and case-sensitive -- it does NOT fold case the way the search
|
|
8
|
+
// Codec does, because these are generic string distances, not sequence-search primitives.
|
|
9
|
+
|
|
10
|
+
#include "seqtree/seqtree.hpp"
|
|
11
|
+
|
|
12
|
+
#include <algorithm>
|
|
13
|
+
#include <atomic>
|
|
14
|
+
#include <cstdint>
|
|
15
|
+
#include <mutex>
|
|
16
|
+
#include <stdexcept>
|
|
17
|
+
#include <string>
|
|
18
|
+
#include <thread>
|
|
19
|
+
#include <vector>
|
|
20
|
+
|
|
21
|
+
namespace seqtree {
|
|
22
|
+
namespace {
|
|
23
|
+
|
|
24
|
+
// Shared driver for both N*K matrices. `pair` is the scalar distance applied to every cell;
|
|
25
|
+
// it may throw (Hamming does, on a length mismatch), so the worker catches and the exception
|
|
26
|
+
// is rethrown after the join -- an exception escaping a std::thread entry calls std::terminate
|
|
27
|
+
// and would take the interpreter down with an uncatchable SIGABRT. Same plumbing as
|
|
28
|
+
// pairwise.cpp / Index::search_batch.
|
|
29
|
+
template <class Pair>
|
|
30
|
+
std::vector<int32_t> matrix_impl(const std::vector<std::string>& a,
|
|
31
|
+
const std::vector<std::string>& b, int threads, Pair pair) {
|
|
32
|
+
const size_t N = a.size(), K = b.size();
|
|
33
|
+
std::vector<int32_t> out(N * K);
|
|
34
|
+
if (N == 0 || K == 0) return out;
|
|
35
|
+
|
|
36
|
+
unsigned nt =
|
|
37
|
+
threads > 0 ? unsigned(threads) : std::max(1u, std::thread::hardware_concurrency());
|
|
38
|
+
nt = std::min<unsigned>(nt, std::max<size_t>(1, N));
|
|
39
|
+
|
|
40
|
+
std::atomic<size_t> next{0};
|
|
41
|
+
std::exception_ptr err;
|
|
42
|
+
std::mutex emu;
|
|
43
|
+
|
|
44
|
+
auto worker = [&] {
|
|
45
|
+
for (;;) {
|
|
46
|
+
const size_t i = next.fetch_add(1);
|
|
47
|
+
if (i >= N) break;
|
|
48
|
+
try {
|
|
49
|
+
int32_t* row = out.data() + i * K;
|
|
50
|
+
for (size_t k = 0; k < K; ++k) row[k] = pair(a[i], b[k]);
|
|
51
|
+
} catch (...) {
|
|
52
|
+
std::lock_guard<std::mutex> lk(emu);
|
|
53
|
+
if (!err) err = std::current_exception();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
std::vector<std::thread> pool;
|
|
60
|
+
for (unsigned t = 0; t < nt; ++t) pool.emplace_back(worker);
|
|
61
|
+
for (auto& th : pool) th.join();
|
|
62
|
+
if (err) std::rethrow_exception(err);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
} // namespace
|
|
67
|
+
|
|
68
|
+
int hamming(std::string_view a, std::string_view b) {
|
|
69
|
+
if (a.size() != b.size())
|
|
70
|
+
throw std::invalid_argument("seqtree: hamming distance is only defined for equal-length "
|
|
71
|
+
"sequences (got " + std::to_string(a.size()) + " and " +
|
|
72
|
+
std::to_string(b.size()) + ")");
|
|
73
|
+
int d = 0;
|
|
74
|
+
for (size_t i = 0; i < a.size(); ++i) d += (a[i] != b[i]);
|
|
75
|
+
return d;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
int levenshtein(std::string_view a, std::string_view b) {
|
|
79
|
+
// Keep the inner row over the shorter sequence: O(min(m,n)) memory. Two rows, no traceback.
|
|
80
|
+
if (b.size() > a.size()) std::swap(a, b);
|
|
81
|
+
const size_t m = a.size(), n = b.size();
|
|
82
|
+
if (n == 0) return int(m);
|
|
83
|
+
|
|
84
|
+
std::vector<int> prev(n + 1), cur(n + 1);
|
|
85
|
+
for (size_t j = 0; j <= n; ++j) prev[j] = int(j);
|
|
86
|
+
for (size_t i = 1; i <= m; ++i) {
|
|
87
|
+
cur[0] = int(i);
|
|
88
|
+
for (size_t j = 1; j <= n; ++j) {
|
|
89
|
+
const int cost = (a[i - 1] == b[j - 1]) ? 0 : 1;
|
|
90
|
+
cur[j] = std::min({prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost});
|
|
91
|
+
}
|
|
92
|
+
prev.swap(cur);
|
|
93
|
+
}
|
|
94
|
+
return prev[n];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
std::vector<int32_t> hamming_matrix(const std::vector<std::string>& a,
|
|
98
|
+
const std::vector<std::string>& b, int threads) {
|
|
99
|
+
return matrix_impl(a, b, threads, [](std::string_view x, std::string_view y) {
|
|
100
|
+
return hamming(x, y);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
std::vector<int32_t> levenshtein_matrix(const std::vector<std::string>& a,
|
|
105
|
+
const std::vector<std::string>& b, int threads) {
|
|
106
|
+
return matrix_impl(a, b, threads, [](std::string_view x, std::string_view y) {
|
|
107
|
+
return levenshtein(x, y);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
} // namespace seqtree
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|