seqtree 1.0.0__tar.gz → 1.1.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-1.0.0 → seqtree-1.1.0}/.gitignore +12 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/CHANGELOG.md +124 -1
- {seqtree-1.0.0 → seqtree-1.1.0}/CMakeLists.txt +1 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/PKG-INFO +5 -2
- {seqtree-1.0.0 → seqtree-1.1.0}/README.md +4 -1
- {seqtree-1.0.0 → seqtree-1.1.0}/include/seqtree/kmer_index.hpp +1 -1
- seqtree-1.1.0/include/seqtree/parallel.hpp +134 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/include/seqtree/seqtree.hpp +105 -5
- {seqtree-1.0.0 → seqtree-1.1.0}/include/seqtree/text_index.hpp +21 -2
- {seqtree-1.0.0 → seqtree-1.1.0}/pyproject.toml +4 -3
- {seqtree-1.0.0 → seqtree-1.1.0}/python/seqtree/__init__.py +3 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/python/seqtree/evalue.py +22 -9
- {seqtree-1.0.0 → seqtree-1.1.0}/python/seqtree/gapblock.py +290 -2
- seqtree-1.1.0/python/seqtree/text_evalue.py +142 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/_bindings.cpp +124 -19
- seqtree-1.1.0/src/count_unit.hpp +87 -0
- seqtree-1.1.0/src/gapblock.cpp +499 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/index.cpp +25 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/text_index.cpp +229 -6
- seqtree-1.0.0/ROADMAP.md +0 -178
- seqtree-1.0.0/SOURCES.md +0 -118
- seqtree-1.0.0/appendix/.gitignore +0 -10
- seqtree-1.0.0/appendix/.latexmkrc +0 -2
- seqtree-1.0.0/appendix/Makefile +0 -17
- seqtree-1.0.0/appendix/epitope_detection.pdf +0 -0
- seqtree-1.0.0/appendix/evalue.pdf +0 -0
- seqtree-1.0.0/appendix/evalue.tex +0 -879
- seqtree-1.0.0/appendix/evalue_matrix.pdf +0 -0
- seqtree-1.0.0/appendix/mhc1_rocpr.pdf +0 -0
- seqtree-1.0.0/appendix/mhc2_rocpr.pdf +0 -0
- seqtree-1.0.0/appendix/refs.bib +0 -206
- seqtree-1.0.0/examples/01_gapped_search.py +0 -118
- seqtree-1.0.0/examples/02_sequence_dendrogram.py +0 -144
- seqtree-1.0.0/examples/03_indel_positions.py +0 -169
- seqtree-1.0.0/examples/04_island_profile.py +0 -151
- seqtree-1.0.0/include/seqtree/parallel.hpp +0 -70
- seqtree-1.0.0/skills/seqtree/SKILL.md +0 -328
- seqtree-1.0.0/src/gapblock.cpp +0 -123
- {seqtree-1.0.0 → seqtree-1.1.0}/.gitattributes +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/LICENSE +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/include/seqtree/types.hpp +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/python/seqtree/control.py +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/python/seqtree/distance.py +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/python/seqtree/layout.py +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/python/seqtree/pairwise.py +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/python/seqtree/pmhc.py +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/python/seqtree/pmhc_evalue.py +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/python/seqtree/py.typed +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/python/seqtree/seeds.py +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/atomic_write.hpp +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/blosum45.inc +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/blosum62.inc +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/blosum80.inc +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/codec.cpp +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/distance.cpp +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/engine_seqtm.cpp +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/engine_seqtrie.cpp +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/engines.hpp +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/kmer_index.cpp +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/pairwise.cpp +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/pam100.inc +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/pam250.inc +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/positional_matrix.cpp +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/searcher.cpp +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/structural.inc +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/substitution_matrix.cpp +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/trie.cpp +0 -0
- {seqtree-1.0.0 → seqtree-1.1.0}/src/trie.hpp +0 -0
|
@@ -16,3 +16,15 @@ bench/cache/
|
|
|
16
16
|
.coverage
|
|
17
17
|
.coverage.*
|
|
18
18
|
htmlcov/
|
|
19
|
+
|
|
20
|
+
# scratch worktrees and probe output
|
|
21
|
+
tmp/
|
|
22
|
+
|
|
23
|
+
# Local agent instructions and planning notes
|
|
24
|
+
/AGENTS.md
|
|
25
|
+
/CLAUDE.md
|
|
26
|
+
/ROADMAP.md
|
|
27
|
+
/SOURCES.md
|
|
28
|
+
/skills/
|
|
29
|
+
/add_bindings.py
|
|
30
|
+
/add_native.py
|
|
@@ -3,6 +3,129 @@
|
|
|
3
3
|
All notable changes to `seqtree`. Dates are release dates. From 1.0.0 the project follows semantic
|
|
4
4
|
versioning: breaking changes need a **major** bump.
|
|
5
5
|
|
|
6
|
+
## [1.1.0] — 2026-10-11
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Native batched edit-count histograms, with an exact bit-vector fast path for bounded unit-cost searches.
|
|
11
|
+
- Gap-block length profiles and symmetric per-input positional weights, optional group penalties,
|
|
12
|
+
exhaustive balls, linked sum-distance counts, integer linear mass, and streaming group audits.
|
|
13
|
+
- TextIndex E-values and per-query mismatch cutoffs using distinct-record counts, with native
|
|
14
|
+
reduction of repeated offsets and rejection of truncated results.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Stable small Poisson tails and tiny expectation probabilities.
|
|
19
|
+
- Shared native batches respect detected CPU allocations and reject oversubscribed
|
|
20
|
+
worker counts; serial budgets execute inline and all pools close before returning.
|
|
21
|
+
- Setup combines requested test and benchmark extras and propagates installation errors.
|
|
22
|
+
- Local agent instructions, planning notes and scratch scripts are excluded from distributions.
|
|
23
|
+
|
|
24
|
+
### API details
|
|
25
|
+
|
|
26
|
+
- **E-values for substring hits.** `text_evalues` and `text_threshold_for_evalue`
|
|
27
|
+
calibrate a `TextIndex` hit against a background text, the way `evalues` already
|
|
28
|
+
does for an `Index`:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
proteome = seqtree.TextIndex.build(records, "aa", k=5)
|
|
32
|
+
decoy = seqtree.TextIndex.build(shuffled, "aa", k=5)
|
|
33
|
+
for q, r in zip(peptides, seqtree.text_evalues(proteome, decoy, peptides, max_subs=2)):
|
|
34
|
+
if r["p_enrichment"] < 0.01:
|
|
35
|
+
...
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The arithmetic is `evalue.evalue_result` unchanged; what is specific to text search
|
|
39
|
+
is two counting choices. **Counts are per distinct record, not per
|
|
40
|
+
offset**: one query typically hits a record at several overlapping offsets, those
|
|
41
|
+
events are strongly dependent, and counting them separately gives the
|
|
42
|
+
over-dispersed compound-Poisson count `appendix/evalue.tex` collapses away by
|
|
43
|
+
counting unique clonotypes rather than reads. `N` and `M` are `num_refs`. **The ball
|
|
44
|
+
is `max_subs`, never `TextResult.score`**: that score is a similarity, `0` at an
|
|
45
|
+
exact match and more negative for a worse one, the opposite sign to the
|
|
46
|
+
non-negative penalty `thetas_from_scores` inverts; `n_subs` is non-negative,
|
|
47
|
+
integer and monotone in the scope, so it nests the way the proof requires.
|
|
48
|
+
|
|
49
|
+
Two limits, both refusals rather than partial answers. A search whose hits were
|
|
50
|
+
capped by `max_hits` raises, because a truncated count is a floor and an E-value
|
|
51
|
+
computed from it is simply wrong. And there is no `load_control` for texts: the
|
|
52
|
+
background must be composition- and length-matched to the target, that assumption
|
|
53
|
+
is what the whole calibration rests on, and nothing in the library can check it, so
|
|
54
|
+
it stays the caller's explicit choice.
|
|
55
|
+
|
|
56
|
+
- **Bounded exhaustive gapblock reductions.** `topk_batch` keeps
|
|
57
|
+
stable `(score, ref_id)` top-k rows without a dense score matrix; `count_batch`
|
|
58
|
+
counts all ties at arbitrary per-query thresholds. Linked two-view variants use
|
|
59
|
+
the maximum lane penalty and preserve both lane scores. Exact identity exclusion
|
|
60
|
+
precedes selection and removes only both-view identity for linked inputs.
|
|
61
|
+
`Index.ref_seqs()` retrieves stored reference strings in ID order in one native
|
|
62
|
+
call. All scoring reuses the existing single-gap-block cell; no calibration,
|
|
63
|
+
domain payload or runtime dependency is introduced.
|
|
64
|
+
|
|
65
|
+
- **`TextIndex` searches with insertions and deletions.** `search_batch` takes `max_indels`
|
|
66
|
+
alongside `max_subs`, capped independently — `max_subs=2, max_indels=1` accepts two
|
|
67
|
+
substitutions *and* one gap, not three edits of any kind.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
res = ix.search_batch(peptides, max_subs=1, max_indels=1, threads=0)
|
|
71
|
+
h = res[0][0]
|
|
72
|
+
ix.ref_seq(h.ref_id)[h.offset : h.offset + h.length] # the matched text
|
|
73
|
+
h.n_subs, h.n_ins, h.n_dels
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**The index, the build and the probe enumeration are unchanged.** With
|
|
77
|
+
`b = max_subs + max_indels + 1` disjoint blocks the total edit count cannot reach the block
|
|
78
|
+
count, so the same pigeonhole argument still leaves a **zero-error** block — and a zero-error
|
|
79
|
+
block matches the text exactly whether or not the occurrence carries gaps, so the existing
|
|
80
|
+
exact seed lookup finds it. Verified before any of this was written: 2,800 planted
|
|
81
|
+
occurrences with random insertions and deletions, **recall 1.000** from the unmodified exact
|
|
82
|
+
lookup.
|
|
83
|
+
|
|
84
|
+
What changes is verification. A seed no longer pins the start — the prefix before the block
|
|
85
|
+
may have gained or lost up to `max_indels` residues — so every start in
|
|
86
|
+
`[pos - base - max_indels, pos - base + max_indels]` is aligned by a banded DP over the states
|
|
87
|
+
`(query position, net offset, indels spent)`. An alignment must begin and end on an aligned
|
|
88
|
+
pair: a gap at either edge would stretch the reported interval over a residue that matches
|
|
89
|
+
nothing, and a leading gap is the same occurrence starting one residue over, so allowing it
|
|
90
|
+
would report one match several times.
|
|
91
|
+
|
|
92
|
+
`TextHit` gains `n_ins`, `n_dels` and **`length`** — a gapped match is no longer `len(query)`
|
|
93
|
+
residues wide, and `length` is `len(query) + n_dels - n_ins`. `TextResult.arrays()` gains the
|
|
94
|
+
three matching columns.
|
|
95
|
+
|
|
96
|
+
Two limits, both refusals rather than partial answers. Every seed block must be exact, so a
|
|
97
|
+
query must be at least `(max_subs + max_indels + 1) * k` long — at `k = 4` with
|
|
98
|
+
`max_subs=1, max_indels=1` that is 12 residues — and a shorter one raises with the bound in
|
|
99
|
+
the message. And `mismatches` is empty on this path, with `matrix=` ignored: recovering which
|
|
100
|
+
columns were substituted needs an alignment traceback the verifier does not keep.
|
|
101
|
+
|
|
102
|
+
Measured on a synthetic 5,000,000-residue corpus at `k = 4`, one thread, against the same
|
|
103
|
+
queries with `max_indels = 0`:
|
|
104
|
+
|
|
105
|
+
| query length | `max_subs` | ms/query, no indels | ms/query, `max_indels=1` |
|
|
106
|
+
|--:|--:|--:|--:|
|
|
107
|
+
| 16 | 1 | 0.001 | 0.028 |
|
|
108
|
+
| 16 | 2 | 0.001 | 0.043 |
|
|
109
|
+
| 20 | 3 | 0.002 | 0.065 |
|
|
110
|
+
| 24 | 3 | 0.002 | 0.067 |
|
|
111
|
+
|
|
112
|
+
25–37× an ungapped search at the same length and `max_subs`, and still under 0.1 ms/query.
|
|
113
|
+
An early exit when every banded state has already breached a cap is worth 2.5–3× of that; the
|
|
114
|
+
DP was otherwise walking all L rows on candidates the Hamming path rejects after two residues.
|
|
115
|
+
|
|
116
|
+
Completeness is held to the same standard as the substitution path: **set equality against an
|
|
117
|
+
independent brute force**, over `k` ∈ {3, 4, 5} × `max_subs` 0–3 × `max_indels` 1–2,
|
|
118
|
+
**6,732 occurrences, zero missing and zero extra**. `max_indels=0` is byte-identical to the
|
|
119
|
+
previous substitution path, hit for hit, and pays none of the cost.
|
|
120
|
+
|
|
121
|
+
## [1.0.1] — 2026-10-09
|
|
122
|
+
|
|
123
|
+
### Fixed
|
|
124
|
+
|
|
125
|
+
- A one-thread batch executes on the calling thread instead of launching a worker.
|
|
126
|
+
- Worker scratch-allocation exceptions propagate to the caller rather than terminating the process.
|
|
127
|
+
- A partially started worker pool joins its running workers if thread creation fails.
|
|
128
|
+
|
|
6
129
|
## [1.0.0] — 2026-09-06
|
|
7
130
|
|
|
8
131
|
### Added
|
|
@@ -572,7 +695,7 @@ Numbers that constrain the API, all reproducible from `bench/` and the downstrea
|
|
|
572
695
|
|
|
573
696
|
### Docs
|
|
574
697
|
|
|
575
|
-
-
|
|
698
|
+
- Public API surface, invariants, and gotchas are documented in `docs/api.rst` and the API guides.
|
|
576
699
|
- `docs/gapblock.rst` — a worked guide: why one gap block, how to choose its position, why a
|
|
577
700
|
placement rule is a column frame, and why a fixed score cutoff is not a calibrated one.
|
|
578
701
|
- README corrected: `seqtrie` is a full-width DP that ignores per-type caps, not a banded one, and
|
|
@@ -65,6 +65,7 @@ if(SEQTREE_TESTS)
|
|
|
65
65
|
tests/cpp/test_positional.cpp
|
|
66
66
|
tests/cpp/test_kmer_index.cpp
|
|
67
67
|
tests/cpp/test_text_index.cpp
|
|
68
|
+
tests/cpp/test_parallel.cpp
|
|
68
69
|
)
|
|
69
70
|
target_include_directories(seqtree_tests PRIVATE tests/cpp src)
|
|
70
71
|
target_link_libraries(seqtree_tests PRIVATE seqtree_core)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: seqtree
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.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>
|
|
@@ -135,7 +135,10 @@ Beyond search, seqtree ships:
|
|
|
135
135
|
completeness pinned by brute-force **set equality** over L 6–30 × `max_subs` 0–3 ×
|
|
136
136
|
k ∈ {3,4,5}, and by the answer being identical across `k`. On the human proteome
|
|
137
137
|
(69,578,135 residues) one thread answers a 9-mer within 2 substitutions in **1.3 ms** and a
|
|
138
|
-
15-mer within 3 in **1.5 ms**.
|
|
138
|
+
15-mer within 3 in **1.5 ms**. `max_indels` adds insertions and deletions on the same index
|
|
139
|
+
and the same scheme — with `max_subs + max_indels + 1` blocks the edit count is still below
|
|
140
|
+
the block count, so pigeonhole leaves a zero-error block and the *unchanged* exact seed lookup
|
|
141
|
+
finds gapped occurrences too; only verification becomes a banded DP. Results come back as flat CSR arrays with zero-copy
|
|
139
142
|
`numpy` views, mismatches as `(pos, query_aa, text_aa)` pairs, an optional fold onto
|
|
140
143
|
caller-supplied group ids that makes a tie explicit, and a cap that is always reported.
|
|
141
144
|
- **E-values / significance** — calibrate hit counts against a background control repertoire
|
|
@@ -103,7 +103,10 @@ Beyond search, seqtree ships:
|
|
|
103
103
|
completeness pinned by brute-force **set equality** over L 6–30 × `max_subs` 0–3 ×
|
|
104
104
|
k ∈ {3,4,5}, and by the answer being identical across `k`. On the human proteome
|
|
105
105
|
(69,578,135 residues) one thread answers a 9-mer within 2 substitutions in **1.3 ms** and a
|
|
106
|
-
15-mer within 3 in **1.5 ms**.
|
|
106
|
+
15-mer within 3 in **1.5 ms**. `max_indels` adds insertions and deletions on the same index
|
|
107
|
+
and the same scheme — with `max_subs + max_indels + 1` blocks the edit count is still below
|
|
108
|
+
the block count, so pigeonhole leaves a zero-error block and the *unchanged* exact seed lookup
|
|
109
|
+
finds gapped occurrences too; only verification becomes a banded DP. Results come back as flat CSR arrays with zero-copy
|
|
107
110
|
`numpy` views, mismatches as `(pos, query_aa, text_aa)` pairs, an optional fold onto
|
|
108
111
|
caller-supplied group ids that makes a tie explicit, and a cap that is always reported.
|
|
109
112
|
- **E-values / significance** — calibrate hit counts against a background control repertoire
|
|
@@ -39,7 +39,7 @@ public:
|
|
|
39
39
|
// For each query (its k-mer list), return candidates with >= min_shared shared k-mers,
|
|
40
40
|
// ranked by (shared_kmers desc, best_score asc). `params` drives the per-k-mer fuzzy
|
|
41
41
|
// match (max_subs / pos_matrix / engine). allele_filter >= 0 keeps only peptides with
|
|
42
|
-
// that allele tag; < 0 means no filter. threads <= 0 =>
|
|
42
|
+
// that allele tag; < 0 means no filter. threads <= 0 => available CPU budget.
|
|
43
43
|
std::vector<std::vector<Candidate>> seed_and_gather(
|
|
44
44
|
const std::vector<std::vector<std::string>>& query_kmers, const SearchParams& params,
|
|
45
45
|
uint32_t min_shared = 1, int64_t allele_filter = -1, int threads = 0) const;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
// One parallel-for scaffold for the whole tree. Every batch entry point in seqtree used to
|
|
3
|
+
// carry its own copy of this: an atomic cursor, an adaptive chunk, an exception_ptr, a vector
|
|
4
|
+
// of threads, and a rethrow after the join. Seven copies drifted -- one of them had no
|
|
5
|
+
// exception plumbing at all.
|
|
6
|
+
#include <algorithm>
|
|
7
|
+
#include <atomic>
|
|
8
|
+
#include <cstddef>
|
|
9
|
+
#include <cstdint>
|
|
10
|
+
#include <cstdlib>
|
|
11
|
+
#include <exception>
|
|
12
|
+
#include <fstream>
|
|
13
|
+
#include <mutex>
|
|
14
|
+
#include <stdexcept>
|
|
15
|
+
#include <thread>
|
|
16
|
+
#include <vector>
|
|
17
|
+
|
|
18
|
+
#ifdef __linux__
|
|
19
|
+
#include <sched.h>
|
|
20
|
+
#elif defined(__APPLE__)
|
|
21
|
+
#include <sys/sysctl.h>
|
|
22
|
+
#elif defined(_WIN32)
|
|
23
|
+
#ifndef NOMINMAX
|
|
24
|
+
#define NOMINMAX
|
|
25
|
+
#endif
|
|
26
|
+
#ifndef WIN32_LEAN_AND_MEAN
|
|
27
|
+
#define WIN32_LEAN_AND_MEAN
|
|
28
|
+
#endif
|
|
29
|
+
#include <windows.h>
|
|
30
|
+
#endif
|
|
31
|
+
|
|
32
|
+
namespace seqtree {
|
|
33
|
+
|
|
34
|
+
// Resolve once per batch; affinity/quota limits can change between calls.
|
|
35
|
+
inline unsigned available_cpus() {
|
|
36
|
+
unsigned count = std::max(1u, std::thread::hardware_concurrency());
|
|
37
|
+
#ifdef __linux__
|
|
38
|
+
cpu_set_t affinity;
|
|
39
|
+
if (sched_getaffinity(0, sizeof(affinity), &affinity) == 0)
|
|
40
|
+
count = std::min(count, unsigned(std::max(1, CPU_COUNT(&affinity))));
|
|
41
|
+
std::ifstream limits("/sys/fs/cgroup/cpu.max");
|
|
42
|
+
uint64_t quota, period;
|
|
43
|
+
if (limits >> quota >> period && period)
|
|
44
|
+
count = unsigned(std::min<uint64_t>(count, std::max<uint64_t>(1, quota / period)));
|
|
45
|
+
for (const char* name : {"SLURM_CPUS_PER_TASK", "SLURM_CPUS_ON_NODE"}) {
|
|
46
|
+
if (const char* value = std::getenv(name)) {
|
|
47
|
+
char* end = nullptr;
|
|
48
|
+
const auto allocated = std::strtoul(value, &end, 10);
|
|
49
|
+
if (end != value && !*end && allocated)
|
|
50
|
+
count = unsigned(std::min<unsigned long>(count, allocated));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
#elif defined(__APPLE__)
|
|
54
|
+
unsigned physical = count;
|
|
55
|
+
size_t size = sizeof(physical);
|
|
56
|
+
if (sysctlbyname("hw.physicalcpu", &physical, &size, nullptr, 0) == 0 && physical)
|
|
57
|
+
count = std::min(count, physical);
|
|
58
|
+
#elif defined(_WIN32)
|
|
59
|
+
DWORD_PTR process_mask, system_mask;
|
|
60
|
+
if (GetProcessAffinityMask(GetCurrentProcess(), &process_mask, &system_mask)) {
|
|
61
|
+
unsigned allowed = 0;
|
|
62
|
+
for (; process_mask; process_mask &= process_mask - 1) ++allowed;
|
|
63
|
+
if (allowed) count = std::min(count, allowed);
|
|
64
|
+
}
|
|
65
|
+
#endif
|
|
66
|
+
return count;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Runs body(i, local) for every i in [0, n), across nt workers pulling contiguous chunks off
|
|
70
|
+
// one atomic cursor. `threads <= 0` means the detected available CPU budget.
|
|
71
|
+
//
|
|
72
|
+
// `make_local()` is called once per worker and its result handed to every body() that worker
|
|
73
|
+
// runs -- that is how a Searcher or a scratch buffer gets allocated once per thread rather
|
|
74
|
+
// than once per item. Return a dummy for kernels that need no state.
|
|
75
|
+
//
|
|
76
|
+
// The chunk is adaptive: large batches keep ~1024 items per grab so the atomic is not the
|
|
77
|
+
// bottleneck, small ones split ~8 chunks per thread so no worker goes idle. Raise `chunk_min`
|
|
78
|
+
// when the per-item output struct is small enough that neighbouring workers would share a
|
|
79
|
+
// cache line (TextIndex's QueryOut is ~80 bytes, so it passes 8).
|
|
80
|
+
//
|
|
81
|
+
// Workers catch, the first exception is kept, and it is rethrown after the join. This is not
|
|
82
|
+
// optional: an exception escaping a std::thread entry function calls std::terminate, which
|
|
83
|
+
// from Python is an uncatchable SIGABRT rather than a raise.
|
|
84
|
+
template <class MakeLocal, class Body>
|
|
85
|
+
void parallel_for(size_t n, int threads, MakeLocal make_local, Body body,
|
|
86
|
+
size_t chunk_min = 1, size_t chunk_max = 1024) {
|
|
87
|
+
const unsigned available = available_cpus();
|
|
88
|
+
unsigned nt = threads > 0 ? unsigned(threads) : available;
|
|
89
|
+
if (nt > available || nt >= 1000)
|
|
90
|
+
throw std::invalid_argument("worker count exceeds the available CPU budget; request fewer threads");
|
|
91
|
+
if (n == 0) return;
|
|
92
|
+
nt = unsigned(std::min<size_t>(nt, n)); // Do not start idle workers for absent work.
|
|
93
|
+
|
|
94
|
+
// A serial budget runs on the calling thread; spawning one worker only adds overhead.
|
|
95
|
+
if (nt == 1) {
|
|
96
|
+
auto local = make_local();
|
|
97
|
+
for (size_t i = 0; i < n; ++i) body(i, local);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
std::atomic<size_t> next{0};
|
|
102
|
+
const size_t chunk = std::clamp<size_t>(n / (size_t(nt) * 8), chunk_min, chunk_max);
|
|
103
|
+
std::exception_ptr err;
|
|
104
|
+
std::mutex emu;
|
|
105
|
+
|
|
106
|
+
auto worker = [&] {
|
|
107
|
+
// Scratch construction can allocate and throw too. Nothing may escape the entry point.
|
|
108
|
+
try {
|
|
109
|
+
auto local = make_local();
|
|
110
|
+
for (;;) {
|
|
111
|
+
const size_t start = next.fetch_add(chunk);
|
|
112
|
+
if (start >= n) break;
|
|
113
|
+
const size_t end = std::min(n, start + chunk);
|
|
114
|
+
for (size_t i = start; i < end; ++i) {
|
|
115
|
+
body(i, local);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (...) {
|
|
119
|
+
std::lock_guard<std::mutex> lk(emu);
|
|
120
|
+
if (!err) err = std::current_exception();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// A failed thread launch must also join workers already started before unwinding.
|
|
125
|
+
std::vector<std::jthread> pool;
|
|
126
|
+
pool.reserve(nt);
|
|
127
|
+
// The caller is one compute thread: a budget B creates only B-1 OS workers.
|
|
128
|
+
for (unsigned t = 1; t < nt; ++t) pool.emplace_back(worker);
|
|
129
|
+
worker();
|
|
130
|
+
for (auto& th : pool) th.join();
|
|
131
|
+
if (err) std::rethrow_exception(err);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
} // namespace seqtree
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
#include <memory>
|
|
6
6
|
#include <string>
|
|
7
7
|
#include <string_view>
|
|
8
|
+
#include <tuple>
|
|
8
9
|
#include <vector>
|
|
9
10
|
|
|
10
11
|
namespace seqtree {
|
|
@@ -120,10 +121,15 @@ public:
|
|
|
120
121
|
Alignment align(std::string_view query, uint32_t ref_id, const SearchParams&) const;
|
|
121
122
|
|
|
122
123
|
// Parallel batch: pure C++ (no Python), one Searcher per worker thread.
|
|
123
|
-
// threads <= 0 =>
|
|
124
|
+
// threads <= 0 => available CPU budget. Results aligned with input order.
|
|
124
125
|
std::vector<std::vector<Hit>> search_batch(const std::vector<std::string>& queries,
|
|
125
126
|
const SearchParams&, int threads = 0) const;
|
|
126
127
|
|
|
128
|
+
// Exact edit-count bins 0..max_total_edits; one reusable hit buffer per worker.
|
|
129
|
+
std::vector<std::vector<uint64_t>> edit_histogram_batch(
|
|
130
|
+
const std::vector<std::string>& queries, const SearchParams&,
|
|
131
|
+
int threads = 0, bool exclude_exact = false) const;
|
|
132
|
+
|
|
127
133
|
// Per-query count of seqtm collisions: how often a reference was re-reached via a
|
|
128
134
|
// different edit path during branch-and-bound (0 for the seqtrie engine / Hamming).
|
|
129
135
|
std::vector<uint64_t> collisions_batch(const std::vector<std::string>& queries,
|
|
@@ -194,7 +200,7 @@ Alignment align_pair(std::string_view query, std::string_view ref, const Substit
|
|
|
194
200
|
Alphabet, AlignMode, int32_t gap_open, int32_t gap_extend);
|
|
195
201
|
|
|
196
202
|
// Dense N*K similarity matrix, row-major (row i is queries[i] vs every ref). Parallel;
|
|
197
|
-
// threads <= 0 =>
|
|
203
|
+
// threads <= 0 => available CPU budget.
|
|
198
204
|
std::vector<int32_t> align_score_matrix(const std::vector<std::string>& queries,
|
|
199
205
|
const std::vector<std::string>& refs,
|
|
200
206
|
const SubstitutionMatrix&, Alphabet, AlignMode,
|
|
@@ -214,12 +220,106 @@ std::vector<int32_t> align_dist_matrix(const std::vector<std::string>& queries,
|
|
|
214
220
|
// The block of length d = |m-n| is placed at whichever of the min(m,n)+1 positions minimises
|
|
215
221
|
// substitution cost plus `prior`. `prior` is the gap prior flattened to [m][d][i] with stride
|
|
216
222
|
// (prior_width+1); pass it empty for no prior. It is read only when d > 0, which is what keeps
|
|
217
|
-
// the diagonal zero. `matrix` may be null for unit cost. threads <= 0 =>
|
|
223
|
+
// the diagonal zero. `matrix` may be null for unit cost. threads <= 0 => available CPU budget.
|
|
224
|
+
// Optional integer weights are indexed [longer length][longer position], symmetric in q/r.
|
|
225
|
+
// All input frames must be covered; empty tables retain the existing unweighted scorer.
|
|
226
|
+
// Alternatively, per-input rows index each sequence's full coordinates. Aligned
|
|
227
|
+
// substitution costs use max(query weight, reference weight); gaps are unchanged.
|
|
228
|
+
// Both axes are required; these rows cannot combine with length profiles.
|
|
229
|
+
// Optional group IDs index a nonnegative rectangular penalty table added per pair.
|
|
230
|
+
// Exact exclusion remains encoded junction identity, independent of these groups.
|
|
218
231
|
std::vector<int32_t> gapblock_matrix(const std::vector<std::string>& queries,
|
|
219
232
|
const std::vector<std::string>& refs, Alphabet,
|
|
220
233
|
const SubstitutionMatrix* matrix, int32_t gap_open,
|
|
221
234
|
int32_t gap_extend, const std::vector<int32_t>& prior,
|
|
222
|
-
uint32_t prior_width, int threads = 0
|
|
235
|
+
uint32_t prior_width, int threads = 0,
|
|
236
|
+
const std::vector<std::vector<int32_t>>& weights = {},
|
|
237
|
+
const std::vector<int32_t>& query_groups = {},
|
|
238
|
+
const std::vector<int32_t>& reference_groups = {},
|
|
239
|
+
const std::vector<std::vector<int32_t>>& group_distances = {},
|
|
240
|
+
const std::vector<std::vector<int32_t>>& query_weights = {},
|
|
241
|
+
const std::vector<std::vector<int32_t>>& reference_weights = {});
|
|
242
|
+
|
|
243
|
+
// Exhaustive ball rows in reference order; hit edit fields are unset (zero).
|
|
244
|
+
std::vector<std::vector<Hit>> gapblock_ball_batch(const std::vector<std::string>& queries,
|
|
245
|
+
const std::vector<std::string>& refs, const std::vector<int32_t>& thresholds,
|
|
246
|
+
Alphabet alphabet, const SubstitutionMatrix* matrix, int32_t go, int32_t ge,
|
|
247
|
+
const std::vector<int32_t>& prior, uint32_t width, int threads, bool exclude_exact,
|
|
248
|
+
const std::vector<std::vector<int32_t>>& weights = {},
|
|
249
|
+
const std::vector<int32_t>& query_groups = {},
|
|
250
|
+
const std::vector<int32_t>& reference_groups = {},
|
|
251
|
+
const std::vector<std::vector<int32_t>>& group_distances = {},
|
|
252
|
+
const std::vector<std::vector<int32_t>>& query_weights = {},
|
|
253
|
+
const std::vector<std::vector<int32_t>>& reference_weights = {});
|
|
254
|
+
|
|
255
|
+
// Exhaustive bounded reductions using the same gapblock cell. Top-k ties use
|
|
256
|
+
// (penalty, ref_id); paired penalty is max(lane scores). Exact exclusion uses
|
|
257
|
+
// encoded sequence identity (both lanes for pairs), never zero penalty alone.
|
|
258
|
+
// Count the component ball before/after group offsets; minimum full score within it.
|
|
259
|
+
// One threshold per query; nearest=-1 denotes an empty component ball. O(Q) output.
|
|
260
|
+
std::tuple<std::vector<uint64_t>, std::vector<uint64_t>, std::vector<int32_t>>
|
|
261
|
+
gapblock_group_audit_batch(const std::vector<std::string>& queries,
|
|
262
|
+
const std::vector<std::string>& refs, const std::vector<int32_t>& thresholds,
|
|
263
|
+
Alphabet alphabet, const SubstitutionMatrix* matrix, int32_t go, int32_t ge,
|
|
264
|
+
const std::vector<int32_t>& prior, uint32_t width, int threads, bool exclude_exact,
|
|
265
|
+
const std::vector<std::vector<int32_t>>& weights = {},
|
|
266
|
+
const std::vector<int32_t>& query_groups = {},
|
|
267
|
+
const std::vector<int32_t>& reference_groups = {},
|
|
268
|
+
const std::vector<std::vector<int32_t>>& group_distances = {},
|
|
269
|
+
const std::vector<std::vector<int32_t>>& query_weights = {},
|
|
270
|
+
const std::vector<std::vector<int32_t>>& reference_weights = {});
|
|
271
|
+
|
|
272
|
+
std::vector<std::vector<Hit>> gapblock_topk_batch(const std::vector<std::string>& queries,
|
|
273
|
+
const std::vector<std::string>& refs, uint32_t k, Alphabet alphabet,
|
|
274
|
+
const SubstitutionMatrix* matrix, int32_t go, int32_t ge, const std::vector<int32_t>& prior,
|
|
275
|
+
uint32_t width, int threads, bool exclude_exact);
|
|
276
|
+
std::vector<std::vector<std::tuple<uint32_t, int32_t, int32_t>>> gapblock_paired_topk_batch(
|
|
277
|
+
const std::vector<std::string>& qa, const std::vector<std::string>& qb,
|
|
278
|
+
const std::vector<std::string>& ra, const std::vector<std::string>& rb,
|
|
279
|
+
uint32_t k, Alphabet alphabet, const SubstitutionMatrix* matrix, int32_t go, int32_t ge,
|
|
280
|
+
const std::vector<int32_t>& prior, uint32_t width, int threads, bool exclude_exact);
|
|
281
|
+
std::vector<std::vector<uint64_t>> gapblock_count_batch(const std::vector<std::string>& queries,
|
|
282
|
+
const std::vector<std::string>& refs, const std::vector<std::vector<int32_t>>& thresholds,
|
|
283
|
+
Alphabet alphabet, const SubstitutionMatrix* matrix, int32_t go, int32_t ge,
|
|
284
|
+
const std::vector<int32_t>& prior, uint32_t width, int threads, bool exclude_exact,
|
|
285
|
+
const std::vector<std::vector<int32_t>>& weights = {},
|
|
286
|
+
const std::vector<int32_t>& query_groups = {},
|
|
287
|
+
const std::vector<int32_t>& reference_groups = {},
|
|
288
|
+
const std::vector<std::vector<int32_t>>& group_distances = {},
|
|
289
|
+
const std::vector<std::vector<int32_t>>& query_weights = {},
|
|
290
|
+
const std::vector<std::vector<int32_t>>& reference_weights = {});
|
|
291
|
+
std::pair<std::vector<std::vector<uint64_t>>, std::vector<std::vector<uint64_t>>> gapblock_count_mass_batch(const std::vector<std::string>& queries,
|
|
292
|
+
const std::vector<std::string>& refs, const std::vector<std::vector<int32_t>>& thresholds,
|
|
293
|
+
Alphabet alphabet, const SubstitutionMatrix* matrix, int32_t go, int32_t ge,
|
|
294
|
+
const std::vector<int32_t>& prior, uint32_t width, int threads, bool exclude_exact,
|
|
295
|
+
const std::vector<std::vector<int32_t>>& weights = {},
|
|
296
|
+
const std::vector<int32_t>& query_groups = {},
|
|
297
|
+
const std::vector<int32_t>& reference_groups = {},
|
|
298
|
+
const std::vector<std::vector<int32_t>>& group_distances = {},
|
|
299
|
+
const std::vector<std::vector<int32_t>>& query_weights = {},
|
|
300
|
+
const std::vector<std::vector<int32_t>>& reference_weights = {});
|
|
301
|
+
std::vector<std::vector<uint64_t>> gapblock_paired_count_batch(
|
|
302
|
+
const std::vector<std::string>& qa, const std::vector<std::string>& qb,
|
|
303
|
+
const std::vector<std::string>& ra, const std::vector<std::string>& rb,
|
|
304
|
+
const std::vector<std::vector<int32_t>>& thresholds,
|
|
305
|
+
Alphabet alphabet, const SubstitutionMatrix* matrix, int32_t go, int32_t ge,
|
|
306
|
+
const std::vector<int32_t>& prior, uint32_t width, int threads, bool exclude_exact);
|
|
307
|
+
std::pair<std::vector<std::vector<uint64_t>>, std::vector<std::vector<uint64_t>>> gapblock_paired_sum_count_batch(
|
|
308
|
+
const std::vector<std::string>& qa, const std::vector<std::string>& qb,
|
|
309
|
+
const std::vector<std::string>& ra, const std::vector<std::string>& rb,
|
|
310
|
+
const std::vector<std::vector<int32_t>>& thresholds,
|
|
311
|
+
Alphabet alphabet, const SubstitutionMatrix* matrix, int32_t go, int32_t ge,
|
|
312
|
+
const std::vector<int32_t>& prior, uint32_t width, int threads, bool exclude_exact,
|
|
313
|
+
const std::vector<std::vector<int32_t>>& weights_alpha,
|
|
314
|
+
const std::vector<std::vector<int32_t>>& weights_beta,
|
|
315
|
+
const std::vector<int32_t>& query_groups_alpha, const std::vector<int32_t>& query_groups_beta,
|
|
316
|
+
const std::vector<int32_t>& reference_groups_alpha, const std::vector<int32_t>& reference_groups_beta,
|
|
317
|
+
const std::vector<std::vector<int32_t>>& group_distances_alpha,
|
|
318
|
+
const std::vector<std::vector<int32_t>>& group_distances_beta,
|
|
319
|
+
const std::vector<std::vector<int32_t>>& query_weights_alpha = {},
|
|
320
|
+
const std::vector<std::vector<int32_t>>& query_weights_beta = {},
|
|
321
|
+
const std::vector<std::vector<int32_t>>& reference_weights_alpha = {},
|
|
322
|
+
const std::vector<std::vector<int32_t>>& reference_weights_beta = {});
|
|
223
323
|
|
|
224
324
|
// ---------------------------------------------------------------------------------------
|
|
225
325
|
// Plain string edit distances on raw characters, unit costs. Deliberately alphabet-agnostic:
|
|
@@ -235,7 +335,7 @@ int hamming(std::string_view a, std::string_view b);
|
|
|
235
335
|
int levenshtein(std::string_view a, std::string_view b);
|
|
236
336
|
|
|
237
337
|
// Dense N*K distance matrices, row-major (row i is a[i] vs every b[k]). Parallel;
|
|
238
|
-
// threads <= 0 =>
|
|
338
|
+
// threads <= 0 => available CPU budget. hamming_matrix throws on any length-mismatched pair.
|
|
239
339
|
std::vector<int32_t> hamming_matrix(const std::vector<std::string>& a,
|
|
240
340
|
const std::vector<std::string>& b, int threads = 0);
|
|
241
341
|
std::vector<int32_t> levenshtein_matrix(const std::vector<std::string>& a,
|
|
@@ -13,12 +13,24 @@ struct TextStore; // internal, src/text_index.cpp
|
|
|
13
13
|
// One row of the optional group fold: hits collapsed onto the build-time group ids.
|
|
14
14
|
struct TextGroup {
|
|
15
15
|
uint32_t group_id = 0;
|
|
16
|
-
uint16_t min_subs = 0; // best (smallest)
|
|
16
|
+
uint16_t min_subs = 0; // best (smallest) substitution count reaching this group
|
|
17
17
|
uint32_t n_hits = 0; // how many hits fell in it, at any distance
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
struct TextQueryOpts {
|
|
21
21
|
uint16_t max_subs = 0;
|
|
22
|
+
// Insertions + deletions allowed, capped independently of max_subs. 0 keeps the pure
|
|
23
|
+
// Hamming predicate and its verification path untouched.
|
|
24
|
+
//
|
|
25
|
+
// The seed table does not change: with b = max_subs + max_indels + 1 disjoint blocks the
|
|
26
|
+
// total edit count is below the block count, so by pigeonhole SOME block still matches
|
|
27
|
+
// EXACTLY -- the existing exact lookup finds every occurrence, indels included. What that
|
|
28
|
+
// costs is verification, which becomes a banded alignment over the 2*max_indels+1 possible
|
|
29
|
+
// starts rather than one Hamming scan at a fixed start.
|
|
30
|
+
//
|
|
31
|
+
// Because every block must be exact, this needs L >= (max_subs + max_indels + 1) * k;
|
|
32
|
+
// search_batch refuses a shorter query rather than answering incompletely.
|
|
33
|
+
uint16_t max_indels = 0;
|
|
22
34
|
bool exclude_exact = false; // drop 0-mismatch hits (a candidate that IS the text)
|
|
23
35
|
bool best_only = false; // keep only the minimum-n_subs shell (still ALL of it)
|
|
24
36
|
bool group_by = false; // also fold hits onto the build-time group ids
|
|
@@ -40,6 +52,11 @@ struct TextResult {
|
|
|
40
52
|
std::vector<uint32_t> query_begin; // n_queries + 1, CSR into the hit arrays
|
|
41
53
|
std::vector<uint32_t> ref_id, offset;
|
|
42
54
|
std::vector<uint16_t> n_subs;
|
|
55
|
+
// Indels in the alignment, and the matched substring's length in the text. With
|
|
56
|
+
// max_indels = 0 these are 0, 0 and the query length for every hit. With indels the match
|
|
57
|
+
// is no longer len(query) residues wide, so `length` is what bounds it:
|
|
58
|
+
// ref_seq(ref_id)[offset : offset + length].
|
|
59
|
+
std::vector<uint16_t> n_ins, n_dels, length;
|
|
43
60
|
std::vector<int32_t> score;
|
|
44
61
|
std::vector<uint32_t> mm_begin; // n_hits + 1, CSR into the mismatch arrays
|
|
45
62
|
std::vector<uint16_t> mm_pos;
|
|
@@ -53,6 +70,8 @@ struct TextResult {
|
|
|
53
70
|
|
|
54
71
|
size_t num_queries() const { return query_begin.empty() ? 0 : query_begin.size() - 1; }
|
|
55
72
|
size_t num_hits() const { return ref_id.size(); }
|
|
73
|
+
// Best substitution distance per distinct record, sorted per query. Reject capped hits.
|
|
74
|
+
std::vector<std::vector<uint16_t>> record_distances() const;
|
|
56
75
|
};
|
|
57
76
|
|
|
58
77
|
// Exact k-mismatch (Hamming) search over a CONCATENATED reference text.
|
|
@@ -91,7 +110,7 @@ public:
|
|
|
91
110
|
|
|
92
111
|
// Every query must be at least k long: seeds are only indexed where a whole k-mer fits
|
|
93
112
|
// inside a record, so a shorter query could match within k-L of a record end and be
|
|
94
|
-
// missed. Raises rather than answering incompletely. threads <= 0 =>
|
|
113
|
+
// missed. Raises rather than answering incompletely. threads <= 0 => available CPU budget.
|
|
95
114
|
TextResult search_batch(const std::vector<std::string>& queries, const TextQueryOpts&,
|
|
96
115
|
int threads = 0) const;
|
|
97
116
|
|
|
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "seqtree"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.1.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"
|
|
@@ -40,9 +40,10 @@ wheel.packages = ["python/seqtree"]
|
|
|
40
40
|
cmake.build-type = "Release"
|
|
41
41
|
# Keep the sdist lean: ship only what builds the package. Anchor with a leading "/".
|
|
42
42
|
sdist.exclude = [
|
|
43
|
-
"/bench", "/tests", "/docs", "/.github", "/setup.sh", "/build",
|
|
43
|
+
"/bench", "/tests", "/docs", "/.github", "/setup.sh", "/build", "/appendix", "/examples",
|
|
44
44
|
# Local agent config: machine-specific absolute paths and unrelated repo names.
|
|
45
|
-
"/.claude", "/CLAUDE.md",
|
|
45
|
+
"/.claude", "/CLAUDE.md", "/AGENTS.md", "/ROADMAP.md", "/SOURCES.md", "/skills",
|
|
46
|
+
"/tmp", "/add_bindings.py", "/add_native.py", "/appendix/immunogenicity_hooks.md",
|
|
46
47
|
]
|
|
47
48
|
editable.rebuild = false
|
|
48
49
|
|
|
@@ -34,6 +34,7 @@ from ._core import (
|
|
|
34
34
|
)
|
|
35
35
|
from .control import load_control
|
|
36
36
|
from .evalue import evalues, thetas_from_scores, threshold_for_evalue
|
|
37
|
+
from .text_evalue import text_evalues, text_threshold_for_evalue
|
|
37
38
|
from . import distance, gapblock, layout, pairwise, pmhc, seeds
|
|
38
39
|
from .gapblock import (
|
|
39
40
|
GapBlockIndex, IslandProfile, ScoreMatrix, central_prior, embed_in_frame, frame_prior,
|
|
@@ -87,4 +88,6 @@ __all__ = [
|
|
|
87
88
|
"evalues",
|
|
88
89
|
"thetas_from_scores",
|
|
89
90
|
"threshold_for_evalue",
|
|
91
|
+
"text_evalues",
|
|
92
|
+
"text_threshold_for_evalue",
|
|
90
93
|
]
|
|
@@ -22,17 +22,31 @@ import warnings
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def _poisson_sf(k, lam):
|
|
25
|
-
"""P(Poisson(lam) >= k)
|
|
25
|
+
"""P(Poisson(lam) >= k), summing the smaller tail from a log-PMF seed."""
|
|
26
26
|
if k <= 0:
|
|
27
27
|
return 1.0
|
|
28
28
|
if lam <= 0.0:
|
|
29
29
|
return 0.0
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
if lam == math.inf:
|
|
31
|
+
return 1.0
|
|
32
|
+
# Sum the upper tail directly when small: 1-CDF loses significant digits.
|
|
33
|
+
upper = k > lam
|
|
34
|
+
i = k if upper else k - 1
|
|
35
|
+
log_pmf = i * math.log(lam) - lam - math.lgamma(i + 1)
|
|
36
|
+
# Normalize before summation so a subnormal seed cannot erase a representable tail.
|
|
37
|
+
term = total = 1.0
|
|
38
|
+
while term > total * 1e-15:
|
|
39
|
+
if upper:
|
|
40
|
+
i += 1
|
|
41
|
+
term *= lam / i
|
|
42
|
+
else:
|
|
43
|
+
if i == 0:
|
|
44
|
+
break
|
|
45
|
+
term *= i / lam
|
|
46
|
+
i -= 1
|
|
47
|
+
total += term
|
|
48
|
+
probability = math.exp(log_pmf + math.log(total))
|
|
49
|
+
return min(1.0, max(0.0, probability if upper else 1.0 - probability))
|
|
36
50
|
|
|
37
51
|
|
|
38
52
|
def evalue_result(n_target, n_control, n_ref, m_control):
|
|
@@ -61,8 +75,7 @@ def evalue_result(n_target, n_control, n_ref, m_control):
|
|
|
61
75
|
"n_target": n_target,
|
|
62
76
|
"n_control": n_control,
|
|
63
77
|
"E": E,
|
|
64
|
-
|
|
65
|
-
"p_any": 1.0 - math.exp(-E) if E < 700 else 1.0,
|
|
78
|
+
"p_any": -math.expm1(-E),
|
|
66
79
|
"p_enrichment": _poisson_sf(n_target, E),
|
|
67
80
|
"rule_of_three": rule3,
|
|
68
81
|
}
|