seqtree 1.1.1__tar.gz → 1.2.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.1.1 → seqtree-1.2.0}/CHANGELOG.md +18 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/CMakeLists.txt +1 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/PKG-INFO +12 -1
- {seqtree-1.1.1 → seqtree-1.2.0}/README.md +11 -0
- seqtree-1.2.0/RELEASE_AUDIT.md +12 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/include/seqtree/seqtree.hpp +29 -3
- {seqtree-1.1.1 → seqtree-1.2.0}/pyproject.toml +1 -1
- {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/gapblock.py +38 -6
- {seqtree-1.1.1 → seqtree-1.2.0}/src/_bindings.cpp +58 -8
- seqtree-1.2.0/src/alignment_max.cpp +177 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/count_unit.hpp +31 -9
- {seqtree-1.1.1 → seqtree-1.2.0}/src/engine_seqtm.cpp +2 -2
- {seqtree-1.1.1 → seqtree-1.2.0}/src/engines.hpp +1 -1
- {seqtree-1.1.1 → seqtree-1.2.0}/src/gapblock.cpp +228 -46
- {seqtree-1.1.1 → seqtree-1.2.0}/src/index.cpp +5 -3
- {seqtree-1.1.1 → seqtree-1.2.0}/src/searcher.cpp +5 -1
- {seqtree-1.1.1 → seqtree-1.2.0}/.gitattributes +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/.gitignore +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/LICENSE +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/include/seqtree/kmer_index.hpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/include/seqtree/parallel.hpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/include/seqtree/text_index.hpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/include/seqtree/types.hpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/__init__.py +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/control.py +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/distance.py +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/evalue.py +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/layout.py +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/pairwise.py +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/pmhc.py +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/pmhc_evalue.py +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/py.typed +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/seeds.py +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/text_evalue.py +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/atomic_write.hpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/blosum45.inc +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/blosum62.inc +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/blosum80.inc +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/codec.cpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/distance.cpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/engine_seqtrie.cpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/kmer_index.cpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/pairwise.cpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/pairwise_detail.hpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/pam100.inc +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/pam250.inc +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/positional_matrix.cpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/structural.inc +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/substitution_matrix.cpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/text_index.cpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/trie.cpp +0 -0
- {seqtree-1.1.1 → seqtree-1.2.0}/src/trie.hpp +0 -0
|
@@ -3,6 +3,24 @@
|
|
|
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.2.0] — 2026-10-17
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Exact exclusion masks matching hits without shrinking model populations or
|
|
11
|
+
supplied annotation counters, including withheld-label models.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Fixed query/reference partitions for native group audits.
|
|
16
|
+
- Bounded sparse grouped radius-hit output with deterministic ordering and explicit memory-cap failures.
|
|
17
|
+
- Optional grouped geometry compatible with the supported nanobind 2.5 floor.
|
|
18
|
+
|
|
19
|
+
- Generic float64 global-alignment target maxima with numeric annotation CSR,
|
|
20
|
+
group offsets, explicit matrix symbols and independent candidate edit budgets.
|
|
21
|
+
- Optional aggregate insertion/deletion limits for batched edit histograms;
|
|
22
|
+
omitted and redundant limits preserve the existing count paths.
|
|
23
|
+
|
|
6
24
|
## [1.1.0] — 2026-10-11
|
|
7
25
|
|
|
8
26
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: seqtree
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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>
|
|
@@ -289,3 +289,14 @@ This repo follows **git-flow**:
|
|
|
289
289
|
Roadmap (affine gaps, position-specific matrices, succinct memory packing) lives in
|
|
290
290
|
[docs/roadmap.rst](docs/roadmap.rst). Control-set E-values already ship — see the
|
|
291
291
|
[E-value guide](https://antigenomics.github.io/seqtree/evalue.html).
|
|
292
|
+
|
|
293
|
+
### Floating alignment reductions
|
|
294
|
+
|
|
295
|
+
`Index.alignment_max_batch` streams unit-cost search candidates through float64
|
|
296
|
+
global alignment and requested numeric-target maxima without a dense pair table.
|
|
297
|
+
It accepts explicit matrix symbols, small group-offset matrices and annotation CSR,
|
|
298
|
+
with separate alignment-selection and final scoring coefficients.
|
|
299
|
+
`Index.edit_histogram_batch(..., max_indels=...)` additionally supports an aggregate
|
|
300
|
+
insertion/deletion cap; its default preserves previous behavior. See the
|
|
301
|
+
[API reference](https://antigenomics.github.io/seqtree/api.html) for array contracts
|
|
302
|
+
and tie, count, exclusion and unsupported-value semantics.
|
|
@@ -257,3 +257,14 @@ This repo follows **git-flow**:
|
|
|
257
257
|
Roadmap (affine gaps, position-specific matrices, succinct memory packing) lives in
|
|
258
258
|
[docs/roadmap.rst](docs/roadmap.rst). Control-set E-values already ship — see the
|
|
259
259
|
[E-value guide](https://antigenomics.github.io/seqtree/evalue.html).
|
|
260
|
+
|
|
261
|
+
### Floating alignment reductions
|
|
262
|
+
|
|
263
|
+
`Index.alignment_max_batch` streams unit-cost search candidates through float64
|
|
264
|
+
global alignment and requested numeric-target maxima without a dense pair table.
|
|
265
|
+
It accepts explicit matrix symbols, small group-offset matrices and annotation CSR,
|
|
266
|
+
with separate alignment-selection and final scoring coefficients.
|
|
267
|
+
`Index.edit_histogram_batch(..., max_indels=...)` additionally supports an aggregate
|
|
268
|
+
insertion/deletion cap; its default preserves previous behavior. See the
|
|
269
|
+
[API reference](https://antigenomics.github.io/seqtree/api.html) for array contracts
|
|
270
|
+
and tie, count, exclusion and unsupported-value semantics.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Release audit — 17 October 2026
|
|
2
|
+
|
|
3
|
+
The requested generic batch/search APIs are implemented. No new native algorithm is needed for the audited downstream requirements.
|
|
4
|
+
|
|
5
|
+
- Native batch search and edit histograms; aggregate insertion/deletion caps.
|
|
6
|
+
- Float64 global-alignment target reductions, separate candidate/final scoring semantics, fixed populations under exact exclusion.
|
|
7
|
+
- Grouped geometry, fixed audit partitions, bounded deterministic sparse radius hits and optional-geometry compatibility at the supported nanobind floor.
|
|
8
|
+
- Shared CPU allocation checks; one-thread inline execution and explicit over-budget rejection.
|
|
9
|
+
|
|
10
|
+
Verification on `0852ff5`: **379 passed, 1 Linux-only platform skip** across `test_batch_threads.py`, `test_alignment_max.py`, `test_edit_histogram.py`, `test_grouped_optional_geometry.py` and `test_grouped_sparse_hits.py`. An additional 280 checks cover grouped populations, withholding, paired lanes, offsets, score bins and partitions. Native budget was two CPUs; numerical libraries used one thread. The committed `examples/01_gapped_search.py` runs and its per-query cutoff/search output was inspected. Sphinx builds with `-W`.
|
|
11
|
+
|
|
12
|
+
The local `dev` commits `0126e00`, `858056b` and `0852ff5` were three commits ahead of the remote at audit start. They are preserved and included when this audited `dev` is pushed. The changelog now covers these capabilities; public API documentation already describes the alignment reductions. [Issue 18](https://github.com/antigenomics/seqtree/issues/18) tracks integration into the release head and the remaining exact-head cross-platform/wheel gate. Publishing remains the maintainer's action; this audit creates no tag or release. Focused checks do not substitute for complete release CI.
|
|
@@ -105,6 +105,14 @@ private:
|
|
|
105
105
|
std::vector<uint8_t> masked_; // [width]
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
+
struct AlignmentMaxResult {
|
|
109
|
+
std::vector<double> scores, raw_similarity, normalized_similarity, pfit, informativeness;
|
|
110
|
+
std::vector<uint64_t> counts, modeled_counts, aa_counts, exact_counts, candidate_counts, nw_pairs;
|
|
111
|
+
std::vector<int64_t> best_ref_ids, best_lane_ids;
|
|
112
|
+
std::vector<uint32_t> gap_counts;
|
|
113
|
+
std::vector<uint8_t> query_supported;
|
|
114
|
+
};
|
|
115
|
+
|
|
108
116
|
// Immutable after build. Lock-free for concurrent reads; share one Index across
|
|
109
117
|
// threads, give each thread its own Searcher.
|
|
110
118
|
class Index {
|
|
@@ -129,7 +137,18 @@ public:
|
|
|
129
137
|
// Exact edit-count bins 0..max_total_edits; one reusable hit buffer per worker.
|
|
130
138
|
std::vector<std::vector<uint64_t>> edit_histogram_batch(
|
|
131
139
|
const std::vector<std::string>& queries, const SearchParams&,
|
|
132
|
-
int threads = 0, bool exclude_exact = false) const;
|
|
140
|
+
int threads = 0, bool exclude_exact = false, int max_indels = -1) const;
|
|
141
|
+
|
|
142
|
+
// Numeric requested-label maxima; float NW selection is independent of candidate scope.
|
|
143
|
+
AlignmentMaxResult alignment_max_batch(const std::vector<std::string>& queries,
|
|
144
|
+
const SearchParams&, const std::vector<std::vector<double>>& similarity, const std::string& symbols, int max_indels,
|
|
145
|
+
const std::vector<uint32_t>& query_target_offsets, const std::vector<uint32_t>& query_target_ids,
|
|
146
|
+
const std::vector<uint32_t>& reference_target_offsets, const std::vector<uint32_t>& reference_target_ids,
|
|
147
|
+
const std::vector<int32_t>& target_class_ids, const std::vector<uint32_t>& reference_class_offsets,
|
|
148
|
+
const std::vector<int32_t>& reference_class_ids, const std::vector<std::vector<int32_t>>& query_group_ids,
|
|
149
|
+
const std::vector<std::vector<int32_t>>& reference_group_ids, const std::vector<std::vector<std::vector<double>>>& group_offsets,
|
|
150
|
+
const std::vector<uint64_t>& reference_other_counts, const std::vector<uint64_t>& reference_multiplicities,
|
|
151
|
+
const std::vector<double>& coefficients, int threads=0, bool exclude_exact=false) const;
|
|
133
152
|
|
|
134
153
|
// Per-query count of seqtm collisions: how often a reference was re-reached via a
|
|
135
154
|
// different edit path during branch-and-bound (0 for the seqtrie engine / Hamming).
|
|
@@ -155,7 +174,7 @@ public:
|
|
|
155
174
|
~Searcher();
|
|
156
175
|
|
|
157
176
|
std::vector<Hit> search(std::string_view query, const SearchParams&);
|
|
158
|
-
void search_into(std::string_view query, const SearchParams&, std::vector<Hit>& out);
|
|
177
|
+
void search_into(std::string_view query, const SearchParams&, std::vector<Hit>& out, int max_indels = -1);
|
|
159
178
|
bool search_top(std::string_view query, const SearchParams&, Hit& out);
|
|
160
179
|
|
|
161
180
|
// seqtm collisions from the most recent search_into on this Searcher.
|
|
@@ -285,7 +304,9 @@ gapblock_group_audit_batch(const std::vector<std::string>& queries,
|
|
|
285
304
|
const std::vector<std::vector<int32_t>>& group_distances = {},
|
|
286
305
|
const std::vector<std::vector<int32_t>>& query_weights = {},
|
|
287
306
|
const std::vector<std::vector<int32_t>>& reference_weights = {},
|
|
288
|
-
const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt
|
|
307
|
+
const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt,
|
|
308
|
+
const std::vector<int32_t>& query_partitions = {},
|
|
309
|
+
const std::vector<int32_t>& reference_partitions = {});
|
|
289
310
|
|
|
290
311
|
std::vector<std::vector<Hit>> gapblock_topk_batch(const std::vector<std::string>& queries,
|
|
291
312
|
const std::vector<std::string>& refs, uint32_t k, Alphabet alphabet,
|
|
@@ -371,6 +392,10 @@ struct GroupedGeometry {
|
|
|
371
392
|
int32_t alignment_scale = 1;
|
|
372
393
|
uint32_t trim_left = 0, trim_right = 0;
|
|
373
394
|
int32_t score_quantum = 1;
|
|
395
|
+
bool return_hits = false;
|
|
396
|
+
size_t max_hit_bytes = 64*1024*1024;
|
|
397
|
+
std::optional<std::vector<int32_t>> query_target_ids;
|
|
398
|
+
std::optional<std::vector<int64_t>> query_reference_ids;
|
|
374
399
|
std::vector<std::vector<int32_t>> weights, group_distances, query_weights, reference_weights;
|
|
375
400
|
std::vector<int32_t> query_groups, reference_groups;
|
|
376
401
|
std::optional<PairedFeatureOffsets> features;
|
|
@@ -382,6 +407,7 @@ struct CountBuffer {
|
|
|
382
407
|
struct GroupedCountChunk {
|
|
383
408
|
size_t start = 0, stop = 0;
|
|
384
409
|
CountBuffer counts, sizes, exact_counts;
|
|
410
|
+
CountBuffer hits{{},{0,3}};
|
|
385
411
|
};
|
|
386
412
|
class GroupedCountBatch {
|
|
387
413
|
public:
|
|
@@ -745,24 +745,34 @@ def group_audit_batch(queries, refs, thresholds, matrix=None, gap_open=None, gap
|
|
|
745
745
|
gap_prior=None, alphabet="aa", threads=0, exclude_exact=False,
|
|
746
746
|
position_weights_by_length=None,
|
|
747
747
|
query_group_ids=None, reference_group_ids=None, group_distances=None,
|
|
748
|
-
query_position_weights=None, reference_position_weights=None, feature_offsets=None
|
|
748
|
+
query_position_weights=None, reference_position_weights=None, feature_offsets=None,
|
|
749
|
+
query_partition_ids=None, reference_partition_ids=None):
|
|
749
750
|
"""Stream component-ball count, full-score count and bounded nearest in one batch.
|
|
750
751
|
|
|
751
752
|
Geometry/offset options match ball_batch. Component excludes the nonnegative
|
|
752
753
|
group offset; full score includes it. For each query threshold, return three
|
|
753
754
|
lists: component count, full count and minimum full score among component hits.
|
|
754
755
|
Empty component balls have nearest=-1. Duplicate references count separately;
|
|
755
|
-
full encoded exact puncturing is identical.
|
|
756
|
+
full encoded exact puncturing is identical. Optional nonnegative partition IDs
|
|
757
|
+
restrict each query to references with the same ID. Both arrays are required;
|
|
758
|
+
unmatched partitions return zero counts and nearest=-1. Partition restriction
|
|
759
|
+
precedes scoring and is independent of additive group/feature distances. O(Q) output; no sparse hit objects.
|
|
756
760
|
"""
|
|
757
761
|
q, r = list(queries), list(refs)
|
|
758
762
|
thresholds = [_batch_integer(v, "threshold", -(1 << 31)) for v in thresholds]
|
|
759
763
|
if len(thresholds) != len(q):
|
|
760
764
|
raise ValueError("group audit thresholds must have one value per query")
|
|
765
|
+
if (query_partition_ids is None) != (reference_partition_ids is None):
|
|
766
|
+
raise ValueError("both query and reference partition IDs are required")
|
|
767
|
+
qp = [] if query_partition_ids is None else [_batch_integer(v, "query partition ID") for v in query_partition_ids]
|
|
768
|
+
rp = [] if reference_partition_ids is None else [_batch_integer(v, "reference partition ID") for v in reference_partition_ids]
|
|
769
|
+
if query_partition_ids is not None and (len(qp) != len(q) or len(rp) != len(r)):
|
|
770
|
+
raise ValueError("partition IDs must cover every query and reference")
|
|
761
771
|
go, ge, cube, width, threads = _batch_options((q, r), matrix, gap_open, gap_extend, gap_prior, threads)
|
|
762
772
|
return _gapblock_group_audit_batch(q, r, thresholds, alphabet, matrix, go, ge, cube, width,
|
|
763
773
|
threads, exclude_exact, _position_weights(position_weights_by_length, q, r),
|
|
764
774
|
*_group_options(query_group_ids, reference_group_ids, group_distances, q, r),
|
|
765
|
-
*_sequence_weights(query_position_weights, reference_position_weights, position_weights_by_length, q, r), feature_offsets)
|
|
775
|
+
*_sequence_weights(query_position_weights, reference_position_weights, position_weights_by_length, q, r), feature_offsets, qp, rp)
|
|
766
776
|
|
|
767
777
|
|
|
768
778
|
def ball_batch(queries, refs, thresholds, matrix=None, gap_open=None, gap_extend=1,
|
|
@@ -950,7 +960,8 @@ def _grouped_geometry(q, r, weights, query_groups, reference_groups, distances,
|
|
|
950
960
|
query_groups, reference_groups, distances, q, r)
|
|
951
961
|
geometry.query_weights, geometry.reference_weights = _sequence_weights(
|
|
952
962
|
query_weights, reference_weights, weights, q, r)
|
|
953
|
-
|
|
963
|
+
if features is not None:
|
|
964
|
+
geometry.features = features
|
|
954
965
|
return geometry
|
|
955
966
|
|
|
956
967
|
|
|
@@ -963,9 +974,21 @@ def grouped_count_batch(queries, refs, thresholds, *, reference_target_offsets,
|
|
|
963
974
|
feature_offsets=None, query_withheld_peptide_ids=None,
|
|
964
975
|
reference_peptide_offsets=None, reference_peptide_ids=None, score_quantum=1,
|
|
965
976
|
alignment_mode="gapblock", alignment_scale=1, trim_left=0, trim_right=0,
|
|
966
|
-
additional_feature_offsets=None
|
|
977
|
+
additional_feature_offsets=None, return_hits=False, query_target_ids=None,
|
|
978
|
+
query_reference_ids=None, max_hit_bytes=64*1024*1024):
|
|
967
979
|
"""Yield bounded label curves from one immutable prepared scoring population.
|
|
968
980
|
|
|
981
|
+
``return_hits=True`` instead yields ``chunk.hits[H,3]`` read-only uint32 rows
|
|
982
|
+
(global query index, reference index, raw distance) at thresholds[-1], ordered
|
|
983
|
+
by query then reference; counts/sizes/exact_counts are empty. Requires one
|
|
984
|
+
query_target_ids entry per query and scans only that target's CSR references.
|
|
985
|
+
Optional query_reference_ids keeps reference indices strictly greater than
|
|
986
|
+
each query's supplied index (-1 keeps all). Exact exclusion still compares
|
|
987
|
+
full sequences. Supports the single non-withheld population API only.
|
|
988
|
+
max_hit_bytes bounds actual hit output per chunk (default64MiB); exceeding it
|
|
989
|
+
raises without dropping hits. Row assembly plus final output temporarily uses
|
|
990
|
+
at most roughly three times that cap, excluding inputs and worker DP scratch.
|
|
991
|
+
|
|
969
992
|
``alignment_mode`` defaults to gapblock. Global/local reuse the existing affine
|
|
970
993
|
similarity recurrence and require a raw similarity matrix. For trimmed sequences
|
|
971
994
|
q/r, junction distance is alignment_scale * max(0, sum(diag(q)) + sum(diag(r))
|
|
@@ -997,7 +1020,7 @@ def grouped_count_batch(queries, refs, thresholds, *, reference_target_offsets,
|
|
|
997
1020
|
Each chunk owns read-only uint32 buffers: counts[C,1+2*T+P,K], sizes[C,1+2*T+P],
|
|
998
1021
|
exact_counts[C,T], with start/stop query offsets. numpy.asarray is zero-copy;
|
|
999
1022
|
no NumPy import/dependency is required. Exact counts retain raw multiplicity;
|
|
1000
|
-
exclude_exact
|
|
1023
|
+
exclude_exact masks hit curves by full sequence identity; model population sizes remain fixed.
|
|
1001
1024
|
Optional query_withheld_peptide_ids is one ID/query (-1 means none) and
|
|
1002
1025
|
requires full reference_peptide_offsets/reference_peptide_ids CSR. Each sorted
|
|
1003
1026
|
unique row includes all raw peptide labels, including sparse/unselected IDs in
|
|
@@ -1018,6 +1041,15 @@ def grouped_count_batch(queries, refs, thresholds, *, reference_target_offsets,
|
|
|
1018
1041
|
go, ge, cube, width, threads = _batch_options((q, r), matrix, gap_open, gap_extend, gap_prior, threads)
|
|
1019
1042
|
geometry = _grouped_geometry(q, r, position_weights_by_length, query_group_ids,
|
|
1020
1043
|
reference_group_ids, group_distances, query_position_weights, reference_position_weights, feature_offsets, score_quantum)
|
|
1044
|
+
if return_hits or query_target_ids is not None or query_reference_ids is not None:
|
|
1045
|
+
if not hasattr(geometry, "return_hits"):
|
|
1046
|
+
raise RuntimeError("sparse hits require native return_hits support")
|
|
1047
|
+
geometry.return_hits = bool(return_hits)
|
|
1048
|
+
geometry.max_hit_bytes = _batch_integer(max_hit_bytes, "max_hit_bytes", 12)
|
|
1049
|
+
if query_target_ids is not None:
|
|
1050
|
+
geometry.query_target_ids = [_batch_integer(x, "query target ID", 0) for x in query_target_ids]
|
|
1051
|
+
if query_reference_ids is not None:
|
|
1052
|
+
geometry.query_reference_ids = [_batch_integer(x, "query reference ID", -1) for x in query_reference_ids]
|
|
1021
1053
|
_additional_geometry(geometry, additional_feature_offsets)
|
|
1022
1054
|
_alignment_options(geometry, q, r, alignment_mode, alignment_scale, trim_left, trim_right,
|
|
1023
1055
|
matrix, gap_prior, position_weights_by_length, query_position_weights, reference_position_weights)
|
|
@@ -188,13 +188,13 @@ nb::list py_search_batch(const Index& idx, const std::vector<std::string>& queri
|
|
|
188
188
|
|
|
189
189
|
std::vector<std::vector<uint64_t>> py_edit_histogram_batch(
|
|
190
190
|
const Index& idx, const std::vector<std::string>& queries,
|
|
191
|
-
const PyParams& pp, int threads, bool exclude_exact) {
|
|
191
|
+
const PyParams& pp, int threads, bool exclude_exact, int max_indels) {
|
|
192
192
|
if (pp.max_total_edits < 1 || pp.max_total_edits > 65535)
|
|
193
193
|
throw nb::value_error("edit histogram requires max_total_edits in [1, 65535]");
|
|
194
194
|
auto mat = make_matrix(pp, idx.alphabet());
|
|
195
195
|
SearchParams cp = to_cpp(pp, mat ? &*mat : nullptr);
|
|
196
196
|
nb::gil_scoped_release release;
|
|
197
|
-
return idx.edit_histogram_batch(queries, cp, threads, exclude_exact);
|
|
197
|
+
return idx.edit_histogram_batch(queries, cp, threads, exclude_exact, max_indels);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
std::vector<uint64_t> py_collisions_batch(const Index& idx, const std::vector<std::string>& queries,
|
|
@@ -335,6 +335,26 @@ ArrayView view_of(const nb::object& owner, const std::vector<T>& v, char fmt) {
|
|
|
335
335
|
return ArrayView{owner, v.data(), v.size(), sizeof(T), {fmt, 0}};
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
nb::dict alignment_max_arrays(const nb::object& self) {
|
|
339
|
+
const auto& r=nb::cast<const AlignmentMaxResult&>(self); nb::dict d;
|
|
340
|
+
d["scores"]=view_of(self,r.scores,'d');
|
|
341
|
+
d["raw_similarity"]=view_of(self,r.raw_similarity,'d');
|
|
342
|
+
d["normalized_similarity"]=view_of(self,r.normalized_similarity,'d');
|
|
343
|
+
d["pfit"]=view_of(self,r.pfit,'d');
|
|
344
|
+
d["informativeness"]=view_of(self,r.informativeness,'d');
|
|
345
|
+
d["counts"]=view_of(self,r.counts,'Q');
|
|
346
|
+
d["modeled_counts"]=view_of(self,r.modeled_counts,'Q');
|
|
347
|
+
d["aa_counts"]=view_of(self,r.aa_counts,'Q');
|
|
348
|
+
d["exact_counts"]=view_of(self,r.exact_counts,'Q');
|
|
349
|
+
d["candidate_counts"]=view_of(self,r.candidate_counts,'Q');
|
|
350
|
+
d["nw_pairs"]=view_of(self,r.nw_pairs,'Q');
|
|
351
|
+
d["best_ref_ids"]=view_of(self,r.best_ref_ids,'q');
|
|
352
|
+
d["best_lane_ids"]=view_of(self,r.best_lane_ids,'q');
|
|
353
|
+
d["gap_counts"]=view_of(self,r.gap_counts,'I');
|
|
354
|
+
d["query_supported"]=view_of(self,r.query_supported,'B');
|
|
355
|
+
return d;
|
|
356
|
+
}
|
|
357
|
+
|
|
338
358
|
// One hit as a Python object. Built only for the query actually asked for, never for the whole
|
|
339
359
|
// batch -- the flat arrays are what a 445k-query run should read.
|
|
340
360
|
struct PyTextHit {
|
|
@@ -395,6 +415,7 @@ ScoreMatrix py_gapblock_matrix(const std::vector<std::string>& queries,
|
|
|
395
415
|
} // namespace
|
|
396
416
|
|
|
397
417
|
NB_MODULE(_core, m) {
|
|
418
|
+
m.attr("exact_exclusion_hits_only") = true;
|
|
398
419
|
m.doc() = "seqtree: fuzzy biological-sequence search (C++ core)";
|
|
399
420
|
|
|
400
421
|
nb::class_<SubstitutionMatrix>(m, "SubstitutionMatrix",
|
|
@@ -663,6 +684,9 @@ NB_MODULE(_core, m) {
|
|
|
663
684
|
return "Alignment(score=" + std::to_string(a.score) + ", ops='" + a.ops + "')";
|
|
664
685
|
});
|
|
665
686
|
|
|
687
|
+
nb::class_<AlignmentMaxResult>(m,"AlignmentMaxResult")
|
|
688
|
+
.def("arrays",&alignment_max_arrays,"Read-only zero-copy requested-target CSR buffers; views own their result.");
|
|
689
|
+
|
|
666
690
|
nb::class_<Index>(m, "Index",
|
|
667
691
|
"Immutable search index over a set of reference sequences. Build once, "
|
|
668
692
|
"then query concurrently; reference id is the position in ``refs``.")
|
|
@@ -696,11 +720,32 @@ NB_MODULE(_core, m) {
|
|
|
696
720
|
"cores. Returns one hit list per query, in input order.")
|
|
697
721
|
.def("edit_histogram_batch", &py_edit_histogram_batch,
|
|
698
722
|
nb::arg("queries"), nb::arg("params"), nb::arg("threads") = 0,
|
|
699
|
-
nb::arg("exclude_exact") = false,
|
|
723
|
+
nb::arg("exclude_exact") = false, nb::arg("max_indels") = -1,
|
|
700
724
|
"Exact reference counts by total n_subs+n_ins+n_dels, bins 0..max_total_edits. "
|
|
701
725
|
"Requires explicit positive max_total_edits and uncapped seqtm/auto all-hit search. "
|
|
726
|
+
"max_indels>=0 also caps total insertions+deletions; -1 preserves directional-only caps. "
|
|
702
727
|
"Duplicates count separately; exclude_exact retains bin 0 with count zero. threads<=0 uses the detected available CPU budget. Releases the GIL; "
|
|
703
728
|
"one reusable hit buffer per worker, no all-query hit vectors.")
|
|
729
|
+
.def("alignment_max_batch", [](const Index& idx,const std::vector<std::string>& queries,const PyParams& pp,
|
|
730
|
+
const std::vector<std::vector<double>>& sim,const std::string& symbols,int max_indels,
|
|
731
|
+
const std::vector<uint32_t>& qo,const std::vector<uint32_t>& qt,const std::vector<uint32_t>& ro,
|
|
732
|
+
const std::vector<uint32_t>& rt,const std::vector<int32_t>& tc,const std::vector<uint32_t>& co,
|
|
733
|
+
const std::vector<int32_t>& ci,const std::vector<std::vector<int32_t>>& qg,
|
|
734
|
+
const std::vector<std::vector<int32_t>>& rg,const std::vector<std::vector<std::vector<double>>>& offsets,
|
|
735
|
+
const std::vector<uint64_t>& other,const std::vector<uint64_t>& multiplicities,
|
|
736
|
+
const std::vector<double>& coefficients,int threads,bool exclude_exact) {
|
|
737
|
+
auto mat=make_matrix(pp,idx.alphabet()); auto cp=to_cpp(pp,mat?&*mat:nullptr);
|
|
738
|
+
nb::gil_scoped_release release;
|
|
739
|
+
return idx.alignment_max_batch(queries,cp,sim,symbols,max_indels,qo,qt,ro,rt,tc,co,ci,qg,rg,offsets,other,multiplicities,coefficients,threads,exclude_exact);
|
|
740
|
+
}, nb::arg("queries"),nb::arg("params"),nb::arg("similarity"),nb::arg("similarity_symbols"),nb::arg("max_indels"),nb::kw_only(),
|
|
741
|
+
nb::arg("query_target_offsets"),nb::arg("query_target_ids"),nb::arg("reference_target_offsets"),nb::arg("reference_target_ids"),
|
|
742
|
+
nb::arg("target_class_ids"),nb::arg("reference_class_offsets"),nb::arg("reference_class_ids"),
|
|
743
|
+
nb::arg("query_group_ids"),nb::arg("reference_group_ids"),nb::arg("group_offsets"),nb::arg("reference_other_counts"),
|
|
744
|
+
nb::arg("reference_multiplicities"),nb::arg("coefficients"),nb::arg("threads")=0,nb::arg("exclude_exact")=false,
|
|
745
|
+
"Fused numeric requested-label max of cloglog(offset+c3*(rawNW-maxself)+gapcoef*g)*finite information. "
|
|
746
|
+
"Candidate scope is independent of float64 global NW; selection gap=max(abs(similarity)); "
|
|
747
|
+
"strict ties use M/X/Y for M and X, M/Y/X for Y, terminal M/X/Y. Group ID -1 is unsupported, not imputed. "
|
|
748
|
+
"Target filtering precedes NW. Use bounded query chunks against the same immutable Index. GIL released.")
|
|
704
749
|
.def("align", &py_align, nb::arg("ref_id"), nb::arg("query"), nb::arg("params"),
|
|
705
750
|
"Compute a global alignment between ``query`` and a reference, on demand.")
|
|
706
751
|
.def("collisions_batch", &py_collisions_batch, nb::arg("queries"), nb::arg("params"),
|
|
@@ -839,9 +884,14 @@ NB_MODULE(_core, m) {
|
|
|
839
884
|
nb::class_<GroupedCountChunk>(m,"GroupedCountChunk")
|
|
840
885
|
.def_ro("start",&GroupedCountChunk::start).def_ro("stop",&GroupedCountChunk::stop)
|
|
841
886
|
.def_ro("counts",&GroupedCountChunk::counts).def_ro("sizes",&GroupedCountChunk::sizes)
|
|
842
|
-
.def_ro("exact_counts",&GroupedCountChunk::exact_counts)
|
|
887
|
+
.def_ro("exact_counts",&GroupedCountChunk::exact_counts)
|
|
888
|
+
.def_ro("hits",&GroupedCountChunk::hits);
|
|
843
889
|
nb::class_<GroupedGeometry>(m,"_GroupedGeometry")
|
|
844
890
|
.def(nb::init<>()).def_rw("weights",&GroupedGeometry::weights)
|
|
891
|
+
.def_rw("return_hits",&GroupedGeometry::return_hits)
|
|
892
|
+
.def_rw("max_hit_bytes",&GroupedGeometry::max_hit_bytes)
|
|
893
|
+
.def_rw("query_target_ids",&GroupedGeometry::query_target_ids)
|
|
894
|
+
.def_rw("query_reference_ids",&GroupedGeometry::query_reference_ids)
|
|
845
895
|
.def_rw("additional_features",&GroupedGeometry::additional_features)
|
|
846
896
|
.def_rw("gap_prior",&GroupedGeometry::gap_prior)
|
|
847
897
|
.def_rw("alignment_mode",&GroupedGeometry::alignment_mode)
|
|
@@ -895,7 +945,7 @@ NB_MODULE(_core, m) {
|
|
|
895
945
|
nb::arg("reference_target_offsets"),nb::arg("reference_target_ids"),
|
|
896
946
|
nb::arg("target_peptide_ids"),nb::arg("reference_exclusive_peptide_ids"),
|
|
897
947
|
nb::arg("query_gene_ids"),nb::arg("reference_gene_ids"),nb::arg("chunk_size"),
|
|
898
|
-
nb::arg("alphabet"),nb::arg("matrix"),nb::arg("gap_open"),nb::arg("gap_extend"),
|
|
948
|
+
nb::arg("alphabet"),nb::arg("matrix").none(),nb::arg("gap_open"),nb::arg("gap_extend"),
|
|
899
949
|
nb::arg("prior"),nb::arg("prior_width"),nb::arg("threads"),nb::arg("exclude_exact"),
|
|
900
950
|
nb::arg("alpha"),nb::arg("beta")=std::nullopt,
|
|
901
951
|
nb::arg("queries_beta")=std::vector<std::string>{},nb::arg("refs_beta")=std::vector<std::string>{},
|
|
@@ -923,11 +973,11 @@ NB_MODULE(_core, m) {
|
|
|
923
973
|
return gapblock_count_mass_batch(queries, refs, thresholds, alph, matrix ? &*matrix : nullptr, go, ge, prior, width, threads, exclude_exact, weights, query_groups, reference_groups, group_distances, query_weights, reference_weights, feature_offsets, shared_thresholds);
|
|
924
974
|
}, nb::arg("queries"), nb::arg("refs"), nb::arg("thresholds"), nb::arg("alphabet")="aa", nb::arg("matrix")=std::nullopt, nb::arg("gap_open")=1, nb::arg("gap_extend")=1, nb::arg("prior")=std::vector<int32_t>{}, nb::arg("prior_width")=0, nb::arg("threads")=0, nb::arg("exclude_exact")=false, nb::arg("position_weights_by_length")=std::vector<std::vector<int32_t>>{}, nb::arg("query_group_ids")=std::vector<int32_t>{}, nb::arg("reference_group_ids")=std::vector<int32_t>{}, nb::arg("group_distances")=std::vector<std::vector<int32_t>>{}, nb::arg("query_position_weights")=std::vector<std::vector<int32_t>>{}, nb::arg("reference_position_weights")=std::vector<std::vector<int32_t>>{}, nb::arg("feature_offsets")=std::nullopt, nb::arg("shared_thresholds")=false, "Counts and integer linear mass in one bounded traversal, GIL released.");
|
|
925
975
|
|
|
926
|
-
m.def("gapblock_group_audit_batch", [](const std::vector<std::string>& queries, const std::vector<std::string>& refs, const std::vector<int32_t>& thresholds, const std::string& alphabet, const std::optional<SubstitutionMatrix>& matrix, int32_t go, int32_t ge, const std::vector<int32_t>& prior, uint32_t width, int threads, bool exclude_exact, const std::vector<std::vector<int32_t>>& weights, const std::vector<int32_t>& query_groups, const std::vector<int32_t>& reference_groups, const std::vector<std::vector<int32_t>>& group_distances, const std::vector<std::vector<int32_t>>& query_weights, const std::vector<std::vector<int32_t>>& reference_weights, const std::optional<PairedFeatureOffsets>& feature_offsets) {
|
|
976
|
+
m.def("gapblock_group_audit_batch", [](const std::vector<std::string>& queries, const std::vector<std::string>& refs, const std::vector<int32_t>& thresholds, const std::string& alphabet, const std::optional<SubstitutionMatrix>& matrix, int32_t go, int32_t ge, const std::vector<int32_t>& prior, uint32_t width, int threads, bool exclude_exact, const std::vector<std::vector<int32_t>>& weights, const std::vector<int32_t>& query_groups, const std::vector<int32_t>& reference_groups, const std::vector<std::vector<int32_t>>& group_distances, const std::vector<std::vector<int32_t>>& query_weights, const std::vector<std::vector<int32_t>>& reference_weights, const std::optional<PairedFeatureOffsets>& feature_offsets, const std::vector<int32_t>& query_partitions, const std::vector<int32_t>& reference_partitions) {
|
|
927
977
|
const auto alph = parse_alphabet(alphabet);
|
|
928
978
|
nb::gil_scoped_release release;
|
|
929
|
-
return gapblock_group_audit_batch(queries, refs, thresholds, alph, matrix ? &*matrix : nullptr, go, ge, prior, width, threads, exclude_exact, weights, query_groups, reference_groups, group_distances, query_weights, reference_weights, feature_offsets);
|
|
930
|
-
}, nb::arg("queries"), nb::arg("refs"), nb::arg("thresholds"), nb::arg("alphabet")="aa", nb::arg("matrix")=std::nullopt, nb::arg("gap_open")=1, nb::arg("gap_extend")=1, nb::arg("prior")=std::vector<int32_t>{}, nb::arg("prior_width")=0, nb::arg("threads")=0, nb::arg("exclude_exact")=false, nb::arg("position_weights_by_length")=std::vector<std::vector<int32_t>>{}, nb::arg("query_group_ids")=std::vector<int32_t>{}, nb::arg("reference_group_ids")=std::vector<int32_t>{}, nb::arg("group_distances")=std::vector<std::vector<int32_t>>{}, nb::arg("query_position_weights")=std::vector<std::vector<int32_t>>{}, nb::arg("reference_position_weights")=std::vector<std::vector<int32_t>>{}, nb::arg("feature_offsets")=std::nullopt, "Streaming component/full ball counts and minimum full score within the component ball, GIL released.");
|
|
979
|
+
return gapblock_group_audit_batch(queries, refs, thresholds, alph, matrix ? &*matrix : nullptr, go, ge, prior, width, threads, exclude_exact, weights, query_groups, reference_groups, group_distances, query_weights, reference_weights, feature_offsets, query_partitions, reference_partitions);
|
|
980
|
+
}, nb::arg("queries"), nb::arg("refs"), nb::arg("thresholds"), nb::arg("alphabet")="aa", nb::arg("matrix")=std::nullopt, nb::arg("gap_open")=1, nb::arg("gap_extend")=1, nb::arg("prior")=std::vector<int32_t>{}, nb::arg("prior_width")=0, nb::arg("threads")=0, nb::arg("exclude_exact")=false, nb::arg("position_weights_by_length")=std::vector<std::vector<int32_t>>{}, nb::arg("query_group_ids")=std::vector<int32_t>{}, nb::arg("reference_group_ids")=std::vector<int32_t>{}, nb::arg("group_distances")=std::vector<std::vector<int32_t>>{}, nb::arg("query_position_weights")=std::vector<std::vector<int32_t>>{}, nb::arg("reference_position_weights")=std::vector<std::vector<int32_t>>{}, nb::arg("feature_offsets")=std::nullopt, nb::arg("query_partition_ids")=std::vector<int32_t>{}, nb::arg("reference_partition_ids")=std::vector<int32_t>{}, "Streaming component/full ball counts and minimum full score within the component ball, GIL released.");
|
|
931
981
|
|
|
932
982
|
m.def("gapblock_ball_batch", [](const std::vector<std::string>& queries, const std::vector<std::string>& refs, const std::vector<int32_t>& thresholds, const std::string& alphabet, const std::optional<SubstitutionMatrix>& matrix, int32_t go, int32_t ge, const std::vector<int32_t>& prior, uint32_t width, int threads, bool exclude_exact, const std::vector<std::vector<int32_t>>& weights, const std::vector<int32_t>& query_groups, const std::vector<int32_t>& reference_groups, const std::vector<std::vector<int32_t>>& group_distances, const std::vector<std::vector<int32_t>>& query_weights, const std::vector<std::vector<int32_t>>& reference_weights, const std::optional<PairedFeatureOffsets>& feature_offsets) {
|
|
933
983
|
const auto alph = parse_alphabet(alphabet);
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#include "seqtree/seqtree.hpp"
|
|
2
|
+
#include "seqtree/parallel.hpp"
|
|
3
|
+
#include "count_unit.hpp"
|
|
4
|
+
#include <algorithm>
|
|
5
|
+
#include <array>
|
|
6
|
+
#include <cmath>
|
|
7
|
+
#include <limits>
|
|
8
|
+
#include <stdexcept>
|
|
9
|
+
#include <unordered_map>
|
|
10
|
+
|
|
11
|
+
namespace seqtree {
|
|
12
|
+
namespace {
|
|
13
|
+
struct Cell { double score=-std::numeric_limits<double>::infinity(), raw=0; uint32_t gaps=0; };
|
|
14
|
+
Cell best(Cell a, const Cell& b, const Cell& c) {
|
|
15
|
+
if (b.score>a.score) a=b;
|
|
16
|
+
if (c.score>a.score) a=c;
|
|
17
|
+
return a;
|
|
18
|
+
}
|
|
19
|
+
using Rows=std::array<std::vector<Cell>,6>;
|
|
20
|
+
// Existing strict NW ties: M/X/Y for M and X, M/Y/X for Y; terminal M/X/Y.
|
|
21
|
+
Cell nw(const std::vector<uint8_t>& q,const std::vector<uint8_t>& r,
|
|
22
|
+
const std::vector<std::vector<double>>& sim,double gap,Rows& rows) {
|
|
23
|
+
for (auto& row:rows) row.assign(r.size()+1,Cell{});
|
|
24
|
+
auto& mp=rows[0]; auto& xp=rows[1]; auto& yp=rows[2];
|
|
25
|
+
auto& mc=rows[3]; auto& xc=rows[4]; auto& yc=rows[5];
|
|
26
|
+
mp[0]={0,0,0};
|
|
27
|
+
for (size_t j=1;j<=r.size();++j) yp[j]={-gap*double(j),0,uint32_t(j)};
|
|
28
|
+
for (size_t i=1;i<=q.size();++i) {
|
|
29
|
+
mc[0]=yc[0]=Cell{}; xc[0]={-gap*double(i),0,uint32_t(i)};
|
|
30
|
+
for (size_t j=1;j<=r.size();++j) {
|
|
31
|
+
mc[j]=best(mp[j-1],xp[j-1],yp[j-1]);
|
|
32
|
+
const double s=sim[q[i-1]][r[j-1]]; mc[j].score+=s; mc[j].raw+=s;
|
|
33
|
+
xc[j]=best(mp[j],xp[j],yp[j]); xc[j].score-=gap; ++xc[j].gaps;
|
|
34
|
+
yc[j]=best(mc[j-1],yc[j-1],xc[j-1]); yc[j].score-=gap; ++yc[j].gaps;
|
|
35
|
+
}
|
|
36
|
+
mp.swap(mc); xp.swap(xc); yp.swap(yc);
|
|
37
|
+
}
|
|
38
|
+
return best(mp.back(),xp.back(),yp.back());
|
|
39
|
+
}
|
|
40
|
+
void require(bool ok,const char* message) { if (!ok) throw std::invalid_argument(message); }
|
|
41
|
+
void csr(const std::vector<uint32_t>& offsets,size_t rows,size_t entries) {
|
|
42
|
+
require(offsets.size()==rows+1 && offsets.front()==0 && offsets.back()==entries &&
|
|
43
|
+
std::is_sorted(offsets.begin(),offsets.end()),"invalid CSR offsets");
|
|
44
|
+
}
|
|
45
|
+
void add(uint64_t& a,uint64_t b) {
|
|
46
|
+
if (b>std::numeric_limits<uint64_t>::max()-a) throw std::overflow_error("annotation count overflow");
|
|
47
|
+
a+=b;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
AlignmentMaxResult Index::alignment_max_batch(const std::vector<std::string>& queries,
|
|
52
|
+
const SearchParams& p,const std::vector<std::vector<double>>& similarity,const std::string& symbols,int max_indels,
|
|
53
|
+
const std::vector<uint32_t>& qo,const std::vector<uint32_t>& qt,
|
|
54
|
+
const std::vector<uint32_t>& ro,const std::vector<uint32_t>& rt,const std::vector<int32_t>& tc,
|
|
55
|
+
const std::vector<uint32_t>& co,const std::vector<int32_t>& ci,
|
|
56
|
+
const std::vector<std::vector<int32_t>>& qg,const std::vector<std::vector<int32_t>>& rg,
|
|
57
|
+
const std::vector<std::vector<std::vector<double>>>& offsets,
|
|
58
|
+
const std::vector<uint64_t>& other,const std::vector<uint64_t>& multiplicities,
|
|
59
|
+
const std::vector<double>& coefficients,int threads,bool exclude_exact) const {
|
|
60
|
+
const size_t Q=queries.size(),R=size(),L=rt.size(),T=tc.size(),M=qt.size();
|
|
61
|
+
require(p.engine!=Engine::SeqTrie && p.mode==Mode::AllHits && !p.max_hits && !p.matrix && !p.pos_matrix &&
|
|
62
|
+
p.gap_open==1 && p.gap_extend==1 && !p.max_score_penalty,"candidate scope requires uncapped unit-cost seqtm");
|
|
63
|
+
require(max_indels>=0 && max_indels<=65535,"max_indels must be in [0,65535]");
|
|
64
|
+
require(coefficients.size()==3 && std::all_of(coefficients.begin(),coefficients.end(),[](double x){return std::isfinite(x);}),"three finite coefficients required");
|
|
65
|
+
require(!symbols.empty() && similarity.size()==symbols.size(),"similarity dimension does not match symbols");
|
|
66
|
+
std::array<int,256> code; code.fill(-1); double gap=0;
|
|
67
|
+
for (size_t a=0;a<symbols.size();++a) {
|
|
68
|
+
const auto c=codec().encode(symbols[a]);
|
|
69
|
+
require(c!=Codec::kInvalid && code[c]<0,"invalid or duplicate similarity symbol"); code[c]=int(a);
|
|
70
|
+
require(similarity[a].size()==symbols.size(),"similarity matrix must be square");
|
|
71
|
+
for (double x:similarity[a]) { require(std::isfinite(x),"similarity values must be finite"); gap=std::max(gap,std::abs(x)); }
|
|
72
|
+
}
|
|
73
|
+
require(gap>0,"NW selection gap max(abs(similarity)) must be positive");
|
|
74
|
+
csr(qo,Q,M); csr(ro,R,L); csr(co,R,ci.size());
|
|
75
|
+
require(other.size()==L && multiplicities.size()==L,"annotation arrays have inconsistent lengths");
|
|
76
|
+
require(qg.size()==offsets.size() && rg.size()==offsets.size(),"group offset components disagree");
|
|
77
|
+
for (size_t c=0;c<offsets.size();++c) {
|
|
78
|
+
require(qg[c].size()==Q && rg[c].size()==L,"group ID lengths disagree");
|
|
79
|
+
const size_t columns=offsets[c].empty()?0:offsets[c][0].size();
|
|
80
|
+
for (const auto& row:offsets[c]) require(row.size()==columns && std::all_of(row.begin(),row.end(),[](double x){return std::isfinite(x);}),"group offsets must be finite rectangular matrices");
|
|
81
|
+
for (int32_t x:qg[c]) require(x>=-1 && (x<0 || size_t(x)<offsets[c].size()),"query group ID out of range");
|
|
82
|
+
for (int32_t x:rg[c]) require(x>=-1 && (x<0 || size_t(x)<columns),"reference group ID out of range");
|
|
83
|
+
}
|
|
84
|
+
std::vector<size_t> seen(T,std::numeric_limits<size_t>::max());
|
|
85
|
+
for (size_t i=0;i<Q;++i) for (size_t k=qo[i];k<qo[i+1];++k) {
|
|
86
|
+
require(qt[k]<T && seen[qt[k]]!=i,"invalid or duplicate requested target"); seen[qt[k]]=i;
|
|
87
|
+
}
|
|
88
|
+
for (int32_t x:tc) require(x>=0,"target class IDs must be nonnegative");
|
|
89
|
+
std::vector<uint8_t> modeled(L,1);
|
|
90
|
+
for (size_t r=0;r<R;++r) {
|
|
91
|
+
require(co[r]<co[r+1],"each reference requires at least one class role");
|
|
92
|
+
std::vector<int32_t> labels(ci.begin()+co[r],ci.begin()+co[r+1]); std::sort(labels.begin(),labels.end());
|
|
93
|
+
require(labels.front()>=0 && std::adjacent_find(labels.begin(),labels.end())==labels.end(),"reference class roles must be unique and nonnegative");
|
|
94
|
+
for (size_t a=ro[r];a<ro[r+1];++a) {
|
|
95
|
+
require(rt[a]<T && std::binary_search(labels.begin(),labels.end(),tc[rt[a]]),"annotation target class missing from reference roles");
|
|
96
|
+
require(multiplicities[a]>0 && other[a]<ci.size() && other[a]<=R,"invalid annotation multiplicity or other-class count");
|
|
97
|
+
for (size_t c=0;c<rg.size();++c) if (rg[c][a]<0) modeled[a]=0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
auto encode=[&](std::string_view s) {
|
|
101
|
+
require(s.size()<=size_t(std::numeric_limits<int>::max()),"sequence exceeds native search length");
|
|
102
|
+
std::vector<uint8_t> out; out.reserve(s.size());
|
|
103
|
+
for (char a:s) { const auto c=codec().encode(a); require(c!=Codec::kInvalid && code[c]>=0,"sequence symbol absent from similarity matrix"); out.push_back(uint8_t(code[c])); }
|
|
104
|
+
return out;
|
|
105
|
+
};
|
|
106
|
+
auto self=[&](const std::vector<uint8_t>& s) {
|
|
107
|
+
double value=0; for (auto a:s) value+=similarity[a][a];
|
|
108
|
+
if (!std::isfinite(value)) throw std::overflow_error("self similarity overflow"); return value;
|
|
109
|
+
};
|
|
110
|
+
auto key=[](const std::vector<uint8_t>& s) { return std::string(s.begin(),s.end()); };
|
|
111
|
+
std::vector<std::vector<uint8_t>> refs(R),qs(Q); std::vector<double> rself(R),qself(Q);
|
|
112
|
+
std::unordered_map<std::string,uint32_t> identities;
|
|
113
|
+
for (size_t r=0;r<R;++r) { refs[r]=encode(ref_seq(uint32_t(r))); rself[r]=self(refs[r]); require(identities.emplace(key(refs[r]),uint32_t(r)).second,"alignment max index references must be unique sequences"); }
|
|
114
|
+
std::vector<int64_t> exact_ref(Q,-1);
|
|
115
|
+
for (size_t i=0;i<Q;++i) { qs[i]=encode(queries[i]); qself[i]=self(qs[i]); if (auto found=identities.find(key(qs[i]));found!=identities.end()) exact_ref[i]=found->second; }
|
|
116
|
+
AlignmentMaxResult out; const double nan=std::numeric_limits<double>::quiet_NaN();
|
|
117
|
+
out.scores.assign(M,nan); out.raw_similarity.assign(M,nan); out.normalized_similarity.assign(M,nan);
|
|
118
|
+
out.pfit.assign(M,nan); out.informativeness.assign(M,nan); out.gap_counts.assign(M,0);
|
|
119
|
+
out.best_ref_ids.assign(M,-1); out.best_lane_ids.assign(M,-1);
|
|
120
|
+
out.counts.assign(M,0); out.modeled_counts.assign(M,0); out.aa_counts.assign(M,0); out.exact_counts.assign(M,0);
|
|
121
|
+
out.candidate_counts.assign(Q,0); out.nw_pairs.assign(Q,0); out.query_supported.assign(Q,1);
|
|
122
|
+
struct Worker { Searcher search; std::vector<Hit> hits; Rows rows; std::vector<int64_t> slots; std::vector<uint64_t> aa_seen;
|
|
123
|
+
Worker(const Index& index,size_t targets):search(index),slots(targets,-1),aa_seen(targets,0) {} };
|
|
124
|
+
parallel_for(Q,threads,[&]{return Worker(*this,T);},[&](size_t i,Worker& w) {
|
|
125
|
+
for (size_t c=0;c<qg.size();++c) if (qg[c][i]<0) out.query_supported[i]=0;
|
|
126
|
+
for (size_t k=qo[i];k<qo[i+1];++k) w.slots[qt[k]]=int64_t(k);
|
|
127
|
+
if (count_unit::bounded_eligible(p,max_indels) && !queries[i].empty() && queries[i].size()<=63) {
|
|
128
|
+
count_unit::Pattern pattern(queries[i],codec()); w.hits.clear();
|
|
129
|
+
for (uint32_t r=0;r<R;++r) {
|
|
130
|
+
const int d=count_unit::bounded_distance(pattern,ref_seq(r),codec(),p,max_indels);
|
|
131
|
+
// Only ref_id is consumed below; no operation trace is reconstructed.
|
|
132
|
+
if (d<=p.max_total_edits) w.hits.push_back(Hit{r,d,0,0,0});
|
|
133
|
+
}
|
|
134
|
+
} else w.search.search_into(queries[i],p,w.hits,max_indels);
|
|
135
|
+
out.candidate_counts[i]=w.hits.size();
|
|
136
|
+
const int64_t er=exact_ref[i];
|
|
137
|
+
const uint64_t N=ci.size();
|
|
138
|
+
for (const auto& hit:w.hits) {
|
|
139
|
+
const uint32_t r=hit.ref_id; const bool exact=int64_t(r)==er; bool align=false;
|
|
140
|
+
for (size_t a=ro[r];a<ro[r+1];++a) {
|
|
141
|
+
const uint32_t t=rt[a]; const int64_t k=w.slots[t]; if (k<0) continue;
|
|
142
|
+
if (exact) add(out.exact_counts[size_t(k)],multiplicities[a]);
|
|
143
|
+
if (exclude_exact && exact) continue;
|
|
144
|
+
add(out.counts[size_t(k)],multiplicities[a]);
|
|
145
|
+
if (w.aa_seen[t]!=uint64_t(r)+1) { ++out.aa_counts[size_t(k)]; w.aa_seen[t]=uint64_t(r)+1; }
|
|
146
|
+
if (modeled[a] && out.query_supported[i]) { add(out.modeled_counts[size_t(k)],multiplicities[a]); align=true; }
|
|
147
|
+
}
|
|
148
|
+
if (!align) continue; // Filter requested targets and model availability BEFORE NW.
|
|
149
|
+
const Cell features=nw(qs[i],refs[r],similarity,gap,w.rows); ++out.nw_pairs[i];
|
|
150
|
+
const double normalized=features.raw-std::max(qself[i],rself[r]);
|
|
151
|
+
if (!std::isfinite(features.score) || !std::isfinite(normalized)) throw std::overflow_error("NW similarity overflow");
|
|
152
|
+
for (size_t a=ro[r];a<ro[r+1];++a) {
|
|
153
|
+
const uint32_t t=rt[a]; const int64_t slot=w.slots[t]; if (slot<0 || !modeled[a]) continue;
|
|
154
|
+
const size_t k=size_t(slot);
|
|
155
|
+
const uint64_t numerator=other[a]+1;
|
|
156
|
+
require(N>=numerator,"reference-role denominator is too small");
|
|
157
|
+
const double information=-std::log10(double(numerator)/double(N));
|
|
158
|
+
double eta=coefficients[0];
|
|
159
|
+
for (size_t c=0;c<offsets.size();++c) eta+=offsets[c][size_t(qg[c][i])][size_t(rg[c][a])];
|
|
160
|
+
eta+=coefficients[1]*normalized+coefficients[2]*features.gaps;
|
|
161
|
+
if (!std::isfinite(eta)) throw std::overflow_error("linear predictor overflow");
|
|
162
|
+
const double probability=-std::expm1(-std::exp(eta)),score=probability*information;
|
|
163
|
+
if (out.best_lane_ids[k]<0 || score>out.scores[k] || (score==out.scores[k] && int64_t(a)<out.best_lane_ids[k])) {
|
|
164
|
+
out.scores[k]=score; out.best_ref_ids[k]=r; out.best_lane_ids[k]=int64_t(a);
|
|
165
|
+
out.raw_similarity[k]=features.raw; out.normalized_similarity[k]=normalized; out.gap_counts[k]=features.gaps;
|
|
166
|
+
out.pfit[k]=probability; out.informativeness[k]=information;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (size_t k=qo[i];k<qo[i+1];++k) {
|
|
171
|
+
if (out.query_supported[i] && !out.counts[k]) out.scores[k]=0;
|
|
172
|
+
w.slots[qt[k]]=-1; w.aa_seen[qt[k]]=0;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
} // namespace seqtree
|