seqtree 0.6.0__tar.gz → 0.7.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.6.0 → seqtree-0.7.0}/CHANGELOG.md +57 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/PKG-INFO +17 -3
- {seqtree-0.6.0 → seqtree-0.7.0}/README.md +16 -2
- {seqtree-0.6.0 → seqtree-0.7.0}/pyproject.toml +1 -1
- {seqtree-0.6.0 → seqtree-0.7.0}/python/seqtree/__init__.py +7 -0
- seqtree-0.7.0/python/seqtree/distance.py +282 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/skills/seqtree/SKILL.md +26 -1
- {seqtree-0.6.0 → seqtree-0.7.0}/src/gapblock.cpp +5 -5
- seqtree-0.6.0/python/seqtree/distance.py +0 -119
- {seqtree-0.6.0 → seqtree-0.7.0}/.gitattributes +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/.gitignore +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/CMakeLists.txt +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/LICENSE +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/ROADMAP.md +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/SOURCES.md +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/appendix/.gitignore +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/appendix/.latexmkrc +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/appendix/Makefile +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/appendix/epitope_detection.pdf +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/appendix/evalue.pdf +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/appendix/evalue.tex +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/appendix/evalue_matrix.pdf +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/appendix/mhc1_rocpr.pdf +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/appendix/mhc2_rocpr.pdf +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/appendix/refs.bib +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/examples/01_gapped_search.py +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/examples/02_sequence_dendrogram.py +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/examples/03_indel_positions.py +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/examples/04_island_profile.py +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/include/seqtree/kmer_index.hpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/include/seqtree/seqtree.hpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/include/seqtree/types.hpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/python/seqtree/control.py +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/python/seqtree/evalue.py +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/python/seqtree/gapblock.py +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/python/seqtree/layout.py +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/python/seqtree/pairwise.py +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/python/seqtree/pmhc.py +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/python/seqtree/pmhc_evalue.py +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/python/seqtree/py.typed +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/python/seqtree/seeds.py +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/_bindings.cpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/atomic_write.hpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/blosum45.inc +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/blosum62.inc +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/blosum80.inc +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/codec.cpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/distance.cpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/engine_seqtm.cpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/engine_seqtrie.cpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/engines.hpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/index.cpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/kmer_index.cpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/pairwise.cpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/pam100.inc +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/pam250.inc +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/positional_matrix.cpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/searcher.cpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/structural.inc +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/substitution_matrix.cpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/trie.cpp +0 -0
- {seqtree-0.6.0 → seqtree-0.7.0}/src/trie.hpp +0 -0
|
@@ -3,6 +3,63 @@
|
|
|
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.7.0] — 2026-08-16
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`seqtree.distance` now enumerates a Hamming ball, not just scores one.**
|
|
11
|
+
`neighbourhood(seq, r=1, alphabet=None, include_self=True, shell=False)` lists the members of
|
|
12
|
+
the closed ball (`19·L + 1` at `r = 1` over the 20 standard residues);
|
|
13
|
+
`neighbourhood_union(seqs, ...)` takes the union over many centres, emitting each distinct
|
|
14
|
+
sequence **once**; `union_size(seqs, ...)` returns the cardinality without building or sorting
|
|
15
|
+
the result list. `shell=True` returns the sphere at exactly `r` from the *nearest* centre —
|
|
16
|
+
a per-shell quantity has to be estimated per shell.
|
|
17
|
+
|
|
18
|
+
Substitution only, fixed length: Hamming distance is undefined across lengths (`hamming`
|
|
19
|
+
already raises), and indels are `gapblock`'s problem. Deduplication happens during a
|
|
20
|
+
multi-source breadth-first walk, so the `Σ 19·L_i` multiset is never materialised — which is
|
|
21
|
+
the point, since near-duplicate centres overlap heavily. For 200 length-14 junctions all within
|
|
22
|
+
distance 1 of a common centre, the per-sequence balls double-count **41.7%** (53,400 → 31,122);
|
|
23
|
+
at spread 2, 4.5%; at spread ≥ 3, nothing.
|
|
24
|
+
|
|
25
|
+
Pure Python, measured before being written in C++: 300 junctions of length 14 at `r = 1` is
|
|
26
|
+
80,100 distinct sequences in **23 ms** on one M3 core (11 ms for `union_size`). `r = 2` over
|
|
27
|
+
the same input is 9.9 M sequences, 6.8 s and ~1.8 GB.
|
|
28
|
+
|
|
29
|
+
`alphabet=None` defaults to the 20 standard amino acids — `amino_acids()` **minus** the
|
|
30
|
+
ambiguity codes `B`/`Z`/`X` and the stop `*`, which that function does include.
|
|
31
|
+
|
|
32
|
+
Shells **partition** the closed ball exactly: shells `0..r` are pairwise disjoint, their union
|
|
33
|
+
is the ball, and every member sits at distance exactly `d` from its *nearest* centre. A
|
|
34
|
+
per-shell quantity therefore sums back to the per-ball one. Pinned by a test that checks the
|
|
35
|
+
distance with the C++ `hamming`, not with the generator's own bookkeeping.
|
|
36
|
+
|
|
37
|
+
`neighbourhood_union` and `union_size` **raise `TypeError` on a single string** rather than
|
|
38
|
+
iterating it. A `str` is iterable, so `union_size("CASSLGQYF")` would otherwise take the
|
|
39
|
+
sequence's 8 distinct *characters* as the centres and answer `20` instead of `172` — a
|
|
40
|
+
plausible integer, no error, no way to notice. Pass `["CASSLGQYF"]`, or call `neighbourhood`.
|
|
41
|
+
|
|
42
|
+
- **`seqtree.__version__`.** Read from the installed distribution metadata, so `pyproject.toml`'s
|
|
43
|
+
`project.version` is the single source and a release that bumps it cannot leave a stale literal
|
|
44
|
+
behind. Asserted against the distribution metadata *and* against `pyproject.toml` by a test.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- **The documentation build stamped every page `0.6.1` while the package was `0.7.0`.**
|
|
49
|
+
`docs/conf.py` hand-copied the version into `release`/`version` and the 0.7.0 bump touched only
|
|
50
|
+
`pyproject.toml`. Both now read `seqtree.__version__`, which the docs job already installs.
|
|
51
|
+
|
|
52
|
+
## [0.6.1] — 2026-07-30
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- **`gapblock_matrix`'s out-of-alphabet error named only the bad symbol**, not which sequence it
|
|
57
|
+
came from (`"symbol '_' is not in the alphabet"`) — unhelpful when a single malformed row (e.g.
|
|
58
|
+
a `junction_aa` containing a legacy out-of-frame marker) is buried in a batch of hundreds of
|
|
59
|
+
thousands. The message now names the side, index, and offending string:
|
|
60
|
+
`"queries[42] ('CASSIRS_YEQYF'): symbol '_' is not in the alphabet"`. No change to which symbols
|
|
61
|
+
are accepted (still the standard 20 + B/Z/X/* for `alphabet="aa"`).
|
|
62
|
+
|
|
6
63
|
## [0.6.0] — 2026-07-17
|
|
7
64
|
|
|
8
65
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: seqtree
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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>
|
|
@@ -94,7 +94,11 @@ Beyond search, seqtree ships:
|
|
|
94
94
|
`hamming_matrix` / `levenshtein_matrix` score a whole set against a whole set in one GIL-released
|
|
95
95
|
C++ call (`numpy.asarray` wraps the result with no copy) — no `python-Levenshtein` or `rapidfuzz`
|
|
96
96
|
dependency needed. Hamming requires equal lengths (it raises otherwise); comparison is
|
|
97
|
-
case-sensitive.
|
|
97
|
+
case-sensitive. The same module *enumerates* a Hamming ball as well as scoring one:
|
|
98
|
+
`neighbourhood(seq, r)` lists its `19·L + 1` members, and `neighbourhood_union(seqs, r)` takes
|
|
99
|
+
the union over many centres with each distinct sequence emitted **once** — deduplicated during
|
|
100
|
+
the walk, so the `Σ 19·L_i` multiset never exists. For a tight specificity group that is a 41.7%
|
|
101
|
+
saving, not a rounding correction.
|
|
98
102
|
- **Island profiles** — `IslandProfile.fit` builds a position weight matrix over a set of
|
|
99
103
|
frame-aligned junctions (an *island*) and scores a query column by column against the island
|
|
100
104
|
consensus, as a non-negative penalty that flows through `threshold_for_evalue` unchanged. At a
|
|
@@ -114,8 +118,11 @@ needs a C++17 compiler and CMake (pulled in automatically by the build).
|
|
|
114
118
|
|
|
115
119
|
## Build from source
|
|
116
120
|
|
|
121
|
+
Needs [uv](https://docs.astral.sh/uv/) (`brew install uv`); `setup.sh` uses it for the venv and
|
|
122
|
+
the editable install.
|
|
123
|
+
|
|
117
124
|
```fish
|
|
118
|
-
bash setup.sh #
|
|
125
|
+
bash setup.sh # uv-managed .venv + editable install
|
|
119
126
|
bash setup.sh --tests # + pytest
|
|
120
127
|
bash setup.sh --bench # + benchmark deps (huggingface_hub, psutil)
|
|
121
128
|
```
|
|
@@ -193,6 +200,13 @@ from seqtree.distance import hamming, levenshtein, hamming_matrix, levenshtein_m
|
|
|
193
200
|
hamming("CASSLGQYF", "CASSPGQYF") # 1 (equal length only)
|
|
194
201
|
levenshtein("kitten", "sitting") # 3
|
|
195
202
|
h = np.asarray(hamming_matrix(umis, umis, threads=0)) # (len, len) int32, zero-copy
|
|
203
|
+
|
|
204
|
+
# enumerate the ball, deduplicated across centres (substitution only, fixed length)
|
|
205
|
+
from seqtree.distance import neighbourhood, neighbourhood_union, union_size
|
|
206
|
+
neighbourhood("CASSLGQYF") # 172 = 19*9 + 1
|
|
207
|
+
union_size(junctions) # size the job before running it
|
|
208
|
+
for variant in neighbourhood_union(junctions, r=1): # each distinct sequence once
|
|
209
|
+
...
|
|
196
210
|
```
|
|
197
211
|
|
|
198
212
|
## Tests
|
|
@@ -60,7 +60,11 @@ Beyond search, seqtree ships:
|
|
|
60
60
|
`hamming_matrix` / `levenshtein_matrix` score a whole set against a whole set in one GIL-released
|
|
61
61
|
C++ call (`numpy.asarray` wraps the result with no copy) — no `python-Levenshtein` or `rapidfuzz`
|
|
62
62
|
dependency needed. Hamming requires equal lengths (it raises otherwise); comparison is
|
|
63
|
-
case-sensitive.
|
|
63
|
+
case-sensitive. The same module *enumerates* a Hamming ball as well as scoring one:
|
|
64
|
+
`neighbourhood(seq, r)` lists its `19·L + 1` members, and `neighbourhood_union(seqs, r)` takes
|
|
65
|
+
the union over many centres with each distinct sequence emitted **once** — deduplicated during
|
|
66
|
+
the walk, so the `Σ 19·L_i` multiset never exists. For a tight specificity group that is a 41.7%
|
|
67
|
+
saving, not a rounding correction.
|
|
64
68
|
- **Island profiles** — `IslandProfile.fit` builds a position weight matrix over a set of
|
|
65
69
|
frame-aligned junctions (an *island*) and scores a query column by column against the island
|
|
66
70
|
consensus, as a non-negative penalty that flows through `threshold_for_evalue` unchanged. At a
|
|
@@ -80,8 +84,11 @@ needs a C++17 compiler and CMake (pulled in automatically by the build).
|
|
|
80
84
|
|
|
81
85
|
## Build from source
|
|
82
86
|
|
|
87
|
+
Needs [uv](https://docs.astral.sh/uv/) (`brew install uv`); `setup.sh` uses it for the venv and
|
|
88
|
+
the editable install.
|
|
89
|
+
|
|
83
90
|
```fish
|
|
84
|
-
bash setup.sh #
|
|
91
|
+
bash setup.sh # uv-managed .venv + editable install
|
|
85
92
|
bash setup.sh --tests # + pytest
|
|
86
93
|
bash setup.sh --bench # + benchmark deps (huggingface_hub, psutil)
|
|
87
94
|
```
|
|
@@ -159,6 +166,13 @@ from seqtree.distance import hamming, levenshtein, hamming_matrix, levenshtein_m
|
|
|
159
166
|
hamming("CASSLGQYF", "CASSPGQYF") # 1 (equal length only)
|
|
160
167
|
levenshtein("kitten", "sitting") # 3
|
|
161
168
|
h = np.asarray(hamming_matrix(umis, umis, threads=0)) # (len, len) int32, zero-copy
|
|
169
|
+
|
|
170
|
+
# enumerate the ball, deduplicated across centres (substitution only, fixed length)
|
|
171
|
+
from seqtree.distance import neighbourhood, neighbourhood_union, union_size
|
|
172
|
+
neighbourhood("CASSLGQYF") # 172 = 19*9 + 1
|
|
173
|
+
union_size(junctions) # size the job before running it
|
|
174
|
+
for variant in neighbourhood_union(junctions, r=1): # each distinct sequence once
|
|
175
|
+
...
|
|
162
176
|
```
|
|
163
177
|
|
|
164
178
|
## Tests
|
|
@@ -9,6 +9,8 @@ it ignores the per-type caps). Payload-agnostic: results are
|
|
|
9
9
|
For anchored loops (CDR3 / junction), :mod:`seqtree.gapblock` restricts the alignment
|
|
10
10
|
to one contiguous indel and picks its position with a gap prior.
|
|
11
11
|
"""
|
|
12
|
+
from importlib.metadata import version as _distribution_version
|
|
13
|
+
|
|
12
14
|
from ._core import (
|
|
13
15
|
Index,
|
|
14
16
|
SearchParams,
|
|
@@ -32,6 +34,11 @@ from .gapblock import (
|
|
|
32
34
|
from .seeds import SeedIndex, core_kmers
|
|
33
35
|
from .pmhc import PMHCStore, find_mimics
|
|
34
36
|
|
|
37
|
+
#: Read from the installed distribution metadata, so ``pyproject.toml``'s ``project.version``
|
|
38
|
+
#: is the single source. A hand-copied literal here would silently drift from the version
|
|
39
|
+
#: actually published to PyPI.
|
|
40
|
+
__version__ = _distribution_version("seqtree")
|
|
41
|
+
|
|
35
42
|
__all__ = [
|
|
36
43
|
"distance",
|
|
37
44
|
"gapblock",
|
|
@@ -0,0 +1,282 @@
|
|
|
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
|
+
The same module also *enumerates* a Hamming ball rather than scoring a pair you already hold:
|
|
16
|
+
|
|
17
|
+
* :func:`neighbourhood` -- every sequence within ``r`` substitutions of one centre;
|
|
18
|
+
* :func:`neighbourhood_union` -- the same over **many** centres, each distinct sequence emitted
|
|
19
|
+
once. Deduplication happens during generation, never as a pass over the ``sum(19*L_i)``
|
|
20
|
+
multiset;
|
|
21
|
+
* :func:`union_size` -- the cardinality alone, for sizing a job before running it.
|
|
22
|
+
|
|
23
|
+
Comparison is **case-sensitive**, byte for byte -- unlike the search engines, which fold case.
|
|
24
|
+
For a *weighted* alignment (a substitution matrix, affine gaps, local mode), use
|
|
25
|
+
:mod:`seqtree.pairwise` instead.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> from seqtree.distance import hamming, levenshtein, union_size
|
|
29
|
+
>>> hamming("CASSLGQYF", "CASSPGQYF")
|
|
30
|
+
1
|
|
31
|
+
>>> levenshtein("kitten", "sitting")
|
|
32
|
+
3
|
|
33
|
+
>>> union_size(["CASSLGQYF", "CASSPGQYF"]) # 2 * 172 balls, 20 sequences shared
|
|
34
|
+
324
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from collections.abc import Iterable, Sequence
|
|
39
|
+
|
|
40
|
+
from ._core import ScoreMatrix
|
|
41
|
+
from ._core import amino_acids as _amino_acids
|
|
42
|
+
from ._core import hamming as _hamming
|
|
43
|
+
from ._core import hamming_matrix as _hamming_matrix
|
|
44
|
+
from ._core import levenshtein as _levenshtein
|
|
45
|
+
from ._core import levenshtein_matrix as _levenshtein_matrix
|
|
46
|
+
|
|
47
|
+
__all__ = ["hamming", "levenshtein", "hamming_matrix", "levenshtein_matrix",
|
|
48
|
+
"neighbourhood", "neighbourhood_union", "union_size"]
|
|
49
|
+
|
|
50
|
+
#: The 20 standard residues: :func:`~seqtree.amino_acids` minus the ambiguity codes ``B``/``Z``/
|
|
51
|
+
#: ``X`` and the stop ``*``, none of which is a substitution anybody means.
|
|
52
|
+
_STANDARD_AA = "".join(c for c in _amino_acids() if c not in "BZX*")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def hamming(a: str, b: str) -> int:
|
|
56
|
+
"""Number of positions at which ``a`` and ``b`` differ.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
a: First sequence.
|
|
60
|
+
b: Second sequence, of the **same length** as ``a``.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The count of differing positions (0 when identical).
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: If ``a`` and ``b`` have different lengths -- Hamming distance is undefined
|
|
67
|
+
for unequal lengths; use :func:`levenshtein` for that.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> hamming("AAAA", "AAAA")
|
|
71
|
+
0
|
|
72
|
+
>>> hamming("AAAA", "ATAT")
|
|
73
|
+
2
|
|
74
|
+
"""
|
|
75
|
+
return _hamming(a, b)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def levenshtein(a: str, b: str) -> int:
|
|
79
|
+
"""Edit distance: fewest single-character insert / delete / substitute steps from ``a`` to ``b``.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
a: First sequence.
|
|
83
|
+
b: Second sequence; may be any length.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
The edit distance (0 when identical, ``max(len(a), len(b))`` at most).
|
|
87
|
+
|
|
88
|
+
Example:
|
|
89
|
+
>>> levenshtein("flaw", "lawn")
|
|
90
|
+
2
|
|
91
|
+
>>> levenshtein("CASSLGQAYEQYF", "CASSPGQAYEQF")
|
|
92
|
+
2
|
|
93
|
+
"""
|
|
94
|
+
return _levenshtein(a, b)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def hamming_matrix(a: Sequence[str], b: Sequence[str], threads: int = 0) -> ScoreMatrix:
|
|
98
|
+
"""Hamming distance of every ``a`` against every ``b``, in parallel C++.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
a: Query sequences (the rows).
|
|
102
|
+
b: Reference sequences (the columns).
|
|
103
|
+
threads: Worker threads; ``0`` uses all cores.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
A :class:`~seqtree.ScoreMatrix` of shape ``(len(a), len(b))`` of int32 distances.
|
|
107
|
+
``numpy.asarray`` wraps it without copying.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
ValueError: If any ``(a[i], b[k])`` pair has mismatched lengths.
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
>>> import numpy as np
|
|
114
|
+
>>> d = np.asarray(hamming_matrix(["AAAA", "AAAT"], ["AAAA", "TTTT"]))
|
|
115
|
+
>>> d.tolist()
|
|
116
|
+
[[0, 4], [1, 3]]
|
|
117
|
+
"""
|
|
118
|
+
return _hamming_matrix(list(a), list(b), threads=threads)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def levenshtein_matrix(a: Sequence[str], b: Sequence[str], threads: int = 0) -> ScoreMatrix:
|
|
122
|
+
"""Levenshtein distance of every ``a`` against every ``b``, in parallel C++.
|
|
123
|
+
|
|
124
|
+
Unlike :func:`hamming_matrix`, sequences may differ in length freely.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
a: Query sequences (the rows).
|
|
128
|
+
b: Reference sequences (the columns).
|
|
129
|
+
threads: Worker threads; ``0`` uses all cores.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
A :class:`~seqtree.ScoreMatrix` of shape ``(len(a), len(b))`` of int32 distances,
|
|
133
|
+
zero-copy through ``numpy.asarray``.
|
|
134
|
+
"""
|
|
135
|
+
return _levenshtein_matrix(list(a), list(b), threads=threads)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _shells(seqs: Iterable[str], r: int, alphabet: str | None,
|
|
139
|
+
include_self: bool, shell: bool) -> list[set[str]]:
|
|
140
|
+
"""The selected shells of the union, as sets: shell ``r`` alone, or shells ``0..r``.
|
|
141
|
+
|
|
142
|
+
Multi-source breadth-first walk with one shared ``seen`` set, so a sequence lands in the
|
|
143
|
+
shell of its *nearest* centre and is generated at most once -- the ``sum(19*L_i)`` multiset
|
|
144
|
+
is never materialised. Substitution only, so centres of different lengths never meet.
|
|
145
|
+
"""
|
|
146
|
+
if r < 0:
|
|
147
|
+
raise ValueError(f"radius must be non-negative, got r={r}")
|
|
148
|
+
if isinstance(seqs, str):
|
|
149
|
+
# A str is iterable, so set(seqs) would silently take its CHARACTERS as the centres:
|
|
150
|
+
# union_size("CASSLGQYF") would answer 20 instead of 172 -- a plausible number, no error.
|
|
151
|
+
raise TypeError(f"seqs must be a collection of sequences, not one string; "
|
|
152
|
+
f"pass [{seqs!r}] or call neighbourhood({seqs!r})")
|
|
153
|
+
alpha = _STANDARD_AA if alphabet is None else alphabet
|
|
154
|
+
frontier = set(seqs)
|
|
155
|
+
seen = set(frontier)
|
|
156
|
+
levels = [frontier]
|
|
157
|
+
for _ in range(r):
|
|
158
|
+
nxt: set[str] = set()
|
|
159
|
+
for s in frontier:
|
|
160
|
+
for i, c in enumerate(s):
|
|
161
|
+
head, tail = s[:i], s[i + 1:]
|
|
162
|
+
for a in alpha:
|
|
163
|
+
if a != c:
|
|
164
|
+
t = head + a + tail
|
|
165
|
+
if t not in seen:
|
|
166
|
+
seen.add(t)
|
|
167
|
+
nxt.add(t)
|
|
168
|
+
levels.append(nxt)
|
|
169
|
+
frontier = nxt
|
|
170
|
+
if not include_self:
|
|
171
|
+
levels[0] = set() # the centres are exactly the r = 0 shell
|
|
172
|
+
return [levels[r]] if shell else levels
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def neighbourhood_union(seqs: Iterable[str], r: int = 1, alphabet: str | None = None,
|
|
176
|
+
include_self: bool = True, shell: bool = False) -> list[str]:
|
|
177
|
+
"""Every sequence within ``r`` substitutions of **any** of ``seqs``, each listed once.
|
|
178
|
+
|
|
179
|
+
This is the union of the per-sequence Hamming balls, not their concatenation. Near-duplicate
|
|
180
|
+
centres -- co-specific TCR junctions, an error family around one UMI -- have balls that
|
|
181
|
+
overlap heavily, so the difference is the dominant term rather than a correction. The
|
|
182
|
+
dedup happens as the ball is walked, so the ``sum(19*L_i)`` multiset never exists.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
seqs: The centres. Duplicates and mixed lengths are fine; substitution-only means
|
|
186
|
+
balls of different lengths cannot overlap.
|
|
187
|
+
r: Radius in substitutions.
|
|
188
|
+
alphabet: Substituting alphabet; ``None`` is the 20 standard amino acids. Characters of
|
|
189
|
+
``seqs`` outside it are still substitutable, so a centre containing ``X`` has
|
|
190
|
+
``|alphabet|`` neighbours at that position rather than ``|alphabet| - 1``.
|
|
191
|
+
include_self: Keep the centres themselves. ``False`` drops **every** centre, including
|
|
192
|
+
one that happens to be a neighbour of another -- the ``r = 0`` shell of a union is
|
|
193
|
+
exactly ``set(seqs)``.
|
|
194
|
+
shell: Return only the members at distance exactly ``r`` from the *nearest* centre,
|
|
195
|
+
instead of the whole closed ball.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
The distinct members, ordered by distance and sorted within each shell (so the centres
|
|
199
|
+
come first). Empty input gives ``[]``.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
ValueError: If ``r`` is negative.
|
|
203
|
+
TypeError: If ``seqs`` is a single string -- its characters would otherwise become the
|
|
204
|
+
centres, silently. Use :func:`neighbourhood` for one sequence.
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
>>> neighbourhood_union(["AA", "AC"], 1, alphabet="AC")
|
|
208
|
+
['AA', 'AC', 'CA', 'CC']
|
|
209
|
+
>>> a = neighbourhood("CASSLGQYF")
|
|
210
|
+
>>> b = neighbourhood("CASSPGQYF") # one substitution away
|
|
211
|
+
>>> len(a) + len(b), len(neighbourhood_union(["CASSLGQYF", "CASSPGQYF"]))
|
|
212
|
+
(344, 324)
|
|
213
|
+
"""
|
|
214
|
+
return [s for level in _shells(seqs, r, alphabet, include_self, shell) for s in sorted(level)]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def neighbourhood(seq: str, r: int = 1, alphabet: str | None = None,
|
|
218
|
+
include_self: bool = True, shell: bool = False) -> list[str]:
|
|
219
|
+
"""Every sequence within ``r`` substitutions of ``seq``: its closed Hamming ball.
|
|
220
|
+
|
|
221
|
+
Substitution only, so every member has ``len(seq)`` characters -- Hamming distance is
|
|
222
|
+
undefined across lengths (see :func:`hamming`), and an indel is a different question. Over a
|
|
223
|
+
``k``-letter alphabet the ball holds ``sum((k-1)^d * C(L, d) for d in 0..r)`` sequences;
|
|
224
|
+
at ``r = 1`` that is ``19*L + 1``.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
seq: The centre.
|
|
228
|
+
r: Radius in substitutions.
|
|
229
|
+
alphabet: Substituting alphabet; ``None`` is the 20 standard amino acids.
|
|
230
|
+
include_self: Keep ``seq`` itself (the ``d = 0`` member).
|
|
231
|
+
shell: Return only the members at distance exactly ``r``, instead of the closed ball.
|
|
232
|
+
The cognacy-retention profile is estimated per shell, not per ball.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
The distinct members, ordered by distance and sorted within each shell, so ``seq``
|
|
236
|
+
itself comes first when ``include_self``.
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
ValueError: If ``r`` is negative.
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
>>> len(neighbourhood("CASSLGQYF")) # 19 * 9 + 1
|
|
243
|
+
172
|
|
244
|
+
>>> len(neighbourhood("CASSLGQYF", include_self=False))
|
|
245
|
+
171
|
|
246
|
+
>>> neighbourhood("A", 1, alphabet="ACGT")
|
|
247
|
+
['A', 'C', 'G', 'T']
|
|
248
|
+
>>> len(neighbourhood("CASSLGQYF", 2, shell=True)) # 19^2 * C(9, 2)
|
|
249
|
+
12996
|
|
250
|
+
"""
|
|
251
|
+
return neighbourhood_union([seq], r, alphabet, include_self, shell)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def union_size(seqs: Iterable[str], r: int = 1, alphabet: str | None = None,
|
|
255
|
+
include_self: bool = True, shell: bool = False) -> int:
|
|
256
|
+
"""How many distinct sequences :func:`neighbourhood_union` would return.
|
|
257
|
+
|
|
258
|
+
Same walk, but the result list is never built or sorted -- use it to size a job (an OLGA
|
|
259
|
+
``P_gen`` pass over the union, say) before committing to it. The dedup set is still held,
|
|
260
|
+
so this bounds the output, not the peak memory.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
seqs: The centres.
|
|
264
|
+
r: Radius in substitutions.
|
|
265
|
+
alphabet: Substituting alphabet; ``None`` is the 20 standard amino acids.
|
|
266
|
+
include_self: Count the centres themselves.
|
|
267
|
+
shell: Count only distance-exactly-``r`` members.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
``len(neighbourhood_union(seqs, r, alphabet, include_self, shell))``.
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
ValueError: If ``r`` is negative.
|
|
274
|
+
TypeError: If ``seqs`` is a single string, as for :func:`neighbourhood_union`.
|
|
275
|
+
|
|
276
|
+
Example:
|
|
277
|
+
>>> union_size(["CASSLGQYF", "CASSPGQYF"]) # 344 with double-counting
|
|
278
|
+
324
|
|
279
|
+
>>> union_size(["CASSLGQYF", "CASSLGQYF"]) # a repeat adds nothing
|
|
280
|
+
172
|
|
281
|
+
"""
|
|
282
|
+
return sum(len(level) for level in _shells(seqs, r, alphabet, include_self, shell))
|
|
@@ -111,6 +111,31 @@ dist_matrix(queries, refs, ...) -> ScoreMatrix # d = s(a,a)+s(b,b)-2s(a,
|
|
|
111
111
|
`similarity()` (signed log-odds, for these aligners). The Gram transform is lossy -- it zeroes
|
|
112
112
|
the diagonal -- so a similarity cannot be recovered from a penalty.
|
|
113
113
|
|
|
114
|
+
## Plain distances and Hamming balls — `seqtree.distance`
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
hamming(a, b) -> int # equal length only; RAISES otherwise. Case-SENSITIVE.
|
|
118
|
+
levenshtein(a, b) -> int
|
|
119
|
+
hamming_matrix(a, b, threads=0) / levenshtein_matrix(...) -> ScoreMatrix # GIL released
|
|
120
|
+
|
|
121
|
+
neighbourhood(seq, r=1, alphabet=None, include_self=True, shell=False) -> list[str]
|
|
122
|
+
neighbourhood_union(seqs, r=1, ...) -> list[str] # dedup DURING the walk
|
|
123
|
+
union_size(seqs, r=1, ...) -> int # size the job first
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Enumeration is **substitution only, fixed length** — no indels, because Hamming is undefined
|
|
127
|
+
across lengths and `P_gen` is length-conditioned. `alphabet=None` is the 20 standard residues,
|
|
128
|
+
which is `amino_acids()` **minus** `B`/`Z`/`X`/`*` — passing `amino_acids()` itself gives 23·L
|
|
129
|
+
neighbours, not 19·L. `shell=True` is the sphere at exactly `r` from the *nearest* centre;
|
|
130
|
+
`include_self=False` drops the whole `r = 0` shell, i.e. every centre.
|
|
131
|
+
|
|
132
|
+
**Take the union, never the sum of the balls.** 200 length-14 junctions drawn within distance `d`
|
|
133
|
+
of a common centre, `r = 1`: at `d = 1` the per-sequence balls double-count **41.7%**
|
|
134
|
+
(53,400 → 31,122), at `d = 2` 4.5%, at `d ≥ 3` nothing. 300 junctions of length 14 at `r = 1` is
|
|
135
|
+
80,100 distinct sequences in **23 ms** on one M3 core (`union_size` 11 ms), so this is pure
|
|
136
|
+
Python and does not need the C++ core. `r = 2` over the same 300 is 9.9 M sequences, 6.8 s,
|
|
137
|
+
~1.8 GB — there the dedup is what keeps it to one copy.
|
|
138
|
+
|
|
114
139
|
## Gap-block alignment — `seqtree.gapblock`
|
|
115
140
|
|
|
116
141
|
A V(D)J junction's length variation is **one** contiguous indel event, so restrict alignment to
|
|
@@ -237,7 +262,7 @@ Count it in the control.
|
|
|
237
262
|
4. OLGA generates from `P_gen`, which is **not** the post-selection background `P_0`. Do not use
|
|
238
263
|
an OLGA sample as the null for an operational cutoff.
|
|
239
264
|
5. Rebuild after pulling: a stale `_core.so` has silently shadowed new Python-visible C++ methods
|
|
240
|
-
more than once (`pip install -e .`).
|
|
265
|
+
more than once (`bash setup.sh`, or `uv pip install -e .`).
|
|
241
266
|
|
|
242
267
|
## Layering
|
|
243
268
|
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
namespace seqtree {
|
|
10
10
|
namespace {
|
|
11
11
|
|
|
12
|
-
std::vector<uint8_t> encode(const Codec& c, const std::string& s) {
|
|
12
|
+
std::vector<uint8_t> encode(const Codec& c, const std::string& s, const char* label, size_t idx) {
|
|
13
13
|
std::vector<uint8_t> out(s.size());
|
|
14
14
|
for (size_t i = 0; i < s.size(); ++i) {
|
|
15
15
|
uint8_t v = c.encode(s[i]);
|
|
16
16
|
if (v == Codec::kInvalid)
|
|
17
|
-
throw std::invalid_argument(std::string("
|
|
18
|
-
"' is not in the alphabet");
|
|
17
|
+
throw std::invalid_argument(label + std::string("[") + std::to_string(idx) + "] ('" + s +
|
|
18
|
+
"'): symbol '" + s[i] + "' is not in the alphabet");
|
|
19
19
|
out[i] = v;
|
|
20
20
|
}
|
|
21
21
|
return out;
|
|
@@ -85,11 +85,11 @@ std::vector<int32_t> gapblock_matrix(const std::vector<std::string>& queries,
|
|
|
85
85
|
std::vector<std::vector<uint8_t>> qc(N), rc(K);
|
|
86
86
|
uint32_t longest = 0;
|
|
87
87
|
for (size_t i = 0; i < N; ++i) {
|
|
88
|
-
qc[i] = encode(codec, queries[i]);
|
|
88
|
+
qc[i] = encode(codec, queries[i], "queries", i);
|
|
89
89
|
longest = std::max<uint32_t>(longest, uint32_t(qc[i].size()));
|
|
90
90
|
}
|
|
91
91
|
for (size_t k = 0; k < K; ++k) {
|
|
92
|
-
rc[k] = encode(codec, refs[k]);
|
|
92
|
+
rc[k] = encode(codec, refs[k], "refs", k);
|
|
93
93
|
longest = std::max<uint32_t>(longest, uint32_t(rc[k].size()));
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -1,119 +0,0 @@
|
|
|
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)
|
|
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
|
|
File without changes
|
|
File without changes
|