seqtree 1.2.0__tar.gz → 1.3.1__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.
Files changed (53) hide show
  1. {seqtree-1.2.0 → seqtree-1.3.1}/CHANGELOG.md +18 -0
  2. {seqtree-1.2.0 → seqtree-1.3.1}/PKG-INFO +1 -1
  3. {seqtree-1.2.0 → seqtree-1.3.1}/include/seqtree/seqtree.hpp +15 -0
  4. {seqtree-1.2.0 → seqtree-1.3.1}/pyproject.toml +1 -1
  5. {seqtree-1.2.0 → seqtree-1.3.1}/python/seqtree/gapblock.py +20 -14
  6. {seqtree-1.2.0 → seqtree-1.3.1}/src/_bindings.cpp +66 -0
  7. {seqtree-1.2.0 → seqtree-1.3.1}/src/index.cpp +128 -0
  8. {seqtree-1.2.0 → seqtree-1.3.1}/.gitattributes +0 -0
  9. {seqtree-1.2.0 → seqtree-1.3.1}/.gitignore +0 -0
  10. {seqtree-1.2.0 → seqtree-1.3.1}/CMakeLists.txt +0 -0
  11. {seqtree-1.2.0 → seqtree-1.3.1}/LICENSE +0 -0
  12. {seqtree-1.2.0 → seqtree-1.3.1}/README.md +0 -0
  13. {seqtree-1.2.0 → seqtree-1.3.1}/RELEASE_AUDIT.md +0 -0
  14. {seqtree-1.2.0 → seqtree-1.3.1}/include/seqtree/kmer_index.hpp +0 -0
  15. {seqtree-1.2.0 → seqtree-1.3.1}/include/seqtree/parallel.hpp +0 -0
  16. {seqtree-1.2.0 → seqtree-1.3.1}/include/seqtree/text_index.hpp +0 -0
  17. {seqtree-1.2.0 → seqtree-1.3.1}/include/seqtree/types.hpp +0 -0
  18. {seqtree-1.2.0 → seqtree-1.3.1}/python/seqtree/__init__.py +0 -0
  19. {seqtree-1.2.0 → seqtree-1.3.1}/python/seqtree/control.py +0 -0
  20. {seqtree-1.2.0 → seqtree-1.3.1}/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
  21. {seqtree-1.2.0 → seqtree-1.3.1}/python/seqtree/distance.py +0 -0
  22. {seqtree-1.2.0 → seqtree-1.3.1}/python/seqtree/evalue.py +0 -0
  23. {seqtree-1.2.0 → seqtree-1.3.1}/python/seqtree/layout.py +0 -0
  24. {seqtree-1.2.0 → seqtree-1.3.1}/python/seqtree/pairwise.py +0 -0
  25. {seqtree-1.2.0 → seqtree-1.3.1}/python/seqtree/pmhc.py +0 -0
  26. {seqtree-1.2.0 → seqtree-1.3.1}/python/seqtree/pmhc_evalue.py +0 -0
  27. {seqtree-1.2.0 → seqtree-1.3.1}/python/seqtree/py.typed +0 -0
  28. {seqtree-1.2.0 → seqtree-1.3.1}/python/seqtree/seeds.py +0 -0
  29. {seqtree-1.2.0 → seqtree-1.3.1}/python/seqtree/text_evalue.py +0 -0
  30. {seqtree-1.2.0 → seqtree-1.3.1}/src/alignment_max.cpp +0 -0
  31. {seqtree-1.2.0 → seqtree-1.3.1}/src/atomic_write.hpp +0 -0
  32. {seqtree-1.2.0 → seqtree-1.3.1}/src/blosum45.inc +0 -0
  33. {seqtree-1.2.0 → seqtree-1.3.1}/src/blosum62.inc +0 -0
  34. {seqtree-1.2.0 → seqtree-1.3.1}/src/blosum80.inc +0 -0
  35. {seqtree-1.2.0 → seqtree-1.3.1}/src/codec.cpp +0 -0
  36. {seqtree-1.2.0 → seqtree-1.3.1}/src/count_unit.hpp +0 -0
  37. {seqtree-1.2.0 → seqtree-1.3.1}/src/distance.cpp +0 -0
  38. {seqtree-1.2.0 → seqtree-1.3.1}/src/engine_seqtm.cpp +0 -0
  39. {seqtree-1.2.0 → seqtree-1.3.1}/src/engine_seqtrie.cpp +0 -0
  40. {seqtree-1.2.0 → seqtree-1.3.1}/src/engines.hpp +0 -0
  41. {seqtree-1.2.0 → seqtree-1.3.1}/src/gapblock.cpp +0 -0
  42. {seqtree-1.2.0 → seqtree-1.3.1}/src/kmer_index.cpp +0 -0
  43. {seqtree-1.2.0 → seqtree-1.3.1}/src/pairwise.cpp +0 -0
  44. {seqtree-1.2.0 → seqtree-1.3.1}/src/pairwise_detail.hpp +0 -0
  45. {seqtree-1.2.0 → seqtree-1.3.1}/src/pam100.inc +0 -0
  46. {seqtree-1.2.0 → seqtree-1.3.1}/src/pam250.inc +0 -0
  47. {seqtree-1.2.0 → seqtree-1.3.1}/src/positional_matrix.cpp +0 -0
  48. {seqtree-1.2.0 → seqtree-1.3.1}/src/searcher.cpp +0 -0
  49. {seqtree-1.2.0 → seqtree-1.3.1}/src/structural.inc +0 -0
  50. {seqtree-1.2.0 → seqtree-1.3.1}/src/substitution_matrix.cpp +0 -0
  51. {seqtree-1.2.0 → seqtree-1.3.1}/src/text_index.cpp +0 -0
  52. {seqtree-1.2.0 → seqtree-1.3.1}/src/trie.cpp +0 -0
  53. {seqtree-1.2.0 → seqtree-1.3.1}/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.3.1] — 2026-10-19
7
+
8
+ ### Changed
9
+
10
+ - Gap-prior lookup preparation evaluates pure callbacks only for input lengths,
11
+ avoiding cubic callback work for absent lengths while preserving exact scores,
12
+ count reductions and lane-specific priors. Equal-length/empty axes do not
13
+ evaluate unused priors. The dense native lookup layout is unchanged.
14
+
15
+ ## [1.3.0] — 2026-10-19
16
+
17
+ ### Added
18
+
19
+ - Bounded complete-query hit consumers for single and coordinated two-index
20
+ searches, with owned numeric buffers, explicit byte budgets and worker cleanup.
21
+ - Optional empty query axes in coordinated search retain original query offsets
22
+ and supported marginal evidence without creating a query for the missing axis.
23
+
6
24
  ## [1.2.0] — 2026-10-17
7
25
 
8
26
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: seqtree
3
- Version: 1.2.0
3
+ Version: 1.3.1
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>
@@ -2,6 +2,7 @@
2
2
  #include "seqtree/types.hpp"
3
3
 
4
4
  #include <cstdint>
5
+ #include <functional>
5
6
  #include <memory>
6
7
  #include <optional>
7
8
  #include <string>
@@ -13,6 +14,7 @@ namespace seqtree {
13
14
 
14
15
  struct Trie; // internal, src/trie.hpp
15
16
  struct Scratch; // internal, src/engines.hpp
17
+ struct CountBuffer;
16
18
 
17
19
  // Maps characters to small integer codes (<=32) and back. Case-insensitive on
18
20
  // input. Invalid characters encode to kInvalid.
@@ -134,6 +136,19 @@ public:
134
136
  std::vector<std::vector<Hit>> search_batch(const std::vector<std::string>& queries,
135
137
  const SearchParams&, int threads = 0) const;
136
138
 
139
+ // One pool, serialized owned primitive blocks; each query is emitted completely.
140
+ // Columns: query index, ref_id, score, n_subs, n_ins, n_dels (all nonnegative).
141
+ void search_batch_consume(const std::vector<std::string>& queries, const SearchParams&,
142
+ const std::function<void(size_t, size_t, CountBuffer&&)>& consume,
143
+ int threads = 0, size_t chunk_size = 32, size_t max_hit_bytes = 64*1024*1024) const;
144
+
145
+ // Coordinate two indexes in one pool; both owned buffers cover the same queries.
146
+ void search_paired_batch_consume(const Index& other,
147
+ const std::vector<std::string>& queries, const std::vector<std::string>& other_queries,
148
+ const SearchParams&,
149
+ const std::function<void(size_t, size_t, CountBuffer&&, CountBuffer&&)>& consume,
150
+ int threads = 0, size_t chunk_size = 32, size_t max_hit_bytes = 64*1024*1024, bool skip_empty = false) const;
151
+
137
152
  // Exact edit-count bins 0..max_total_edits; one reusable hit buffer per worker.
138
153
  std::vector<std::vector<uint64_t>> edit_histogram_batch(
139
154
  const std::vector<std::string>& queries, const SearchParams&,
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "seqtree"
7
- version = "1.2.0"
7
+ version = "1.3.1"
8
8
  description = "Fast fuzzy search over biological sequences (C++ core, Python bindings)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -499,19 +499,22 @@ def gapblock_score(
499
499
  return best + gap_cost(d, gap_open, gap_extend), best_i
500
500
 
501
501
 
502
- def _prior_cube(prior: GapPrior, width: int) -> list[int]:
502
+ def _prior_cube(prior: GapPrior, width: int, lengths=None) -> list[int]:
503
503
  """Flatten ``prior`` to ``[m][d][i]`` with stride ``width + 1``, for the C++ kernel.
504
504
 
505
505
  Only reachable cells are filled: ``d >= 1`` (the prior never applies to equal lengths) and
506
506
  ``i <= m - d`` (the block start indexes the shorter sequence). The rest stay zero and are
507
- never read.
507
+ never read. Input lengths restrict callback evaluation to possible full-length pairs;
508
+ the dense native layout is unchanged. None retains the full-width internal oracle.
508
509
  """
509
510
  w1 = width + 1
510
511
  cube = [0] * (w1 * w1 * w1)
511
- for m in range(w1):
512
- for d in range(1, m + 1):
512
+ lengths = range(w1) if lengths is None else sorted(set(lengths))
513
+ for row, m in enumerate(lengths):
514
+ for n in reversed(lengths[:row]):
515
+ d = m - n
513
516
  base = (m * w1 + d) * w1
514
- for i in range(m - d + 1):
517
+ for i in range(n + 1):
515
518
  v = int(prior(i, d, m))
516
519
  if v < 0:
517
520
  raise ValueError(f"gap_prior({i}, {d}, {m}) = {v} is negative")
@@ -561,7 +564,8 @@ def score_matrix(
561
564
  leaving this at 1 with a real matrix makes gaps ~14x cheaper than substitutions.
562
565
  gap_extend: Cost per additional gap column.
563
566
  gap_prior: :data:`GapPrior`, materialized once into a lookup cube and then read from C++.
564
- ``None`` lets the score alone choose the block position.
567
+ Evaluated only for unequal input lengths and valid block starts; it must be
568
+ a pure nonnegative function. ``None`` lets the score alone choose the block position.
565
569
  alphabet: ``"aa"``, ``"nt"``, or ``"iupac"``. Symbols outside it raise.
566
570
  threads: Worker threads; ``0`` means one per core. Rows are disjoint, so this scales.
567
571
 
@@ -586,8 +590,9 @@ def score_matrix(
586
590
  gap_open = _default_gap_open(matrix)
587
591
  if gap_open < 0 or gap_extend < 0:
588
592
  raise ValueError("gap_open and gap_extend must be >= 0")
589
- width = max((len(s) for s in q + r), default=0)
590
- cube = _prior_cube(gap_prior, width) if gap_prior is not None else []
593
+ lengths = {len(s) for s in q + r}
594
+ width = max(lengths, default=0)
595
+ cube = _prior_cube(gap_prior, width, lengths) if gap_prior is not None else []
591
596
  return _gapblock_matrix(q, r, alphabet, matrix, gap_open, gap_extend, cube, width, threads,
592
597
  _position_weights(position_weights_by_length, q, r),
593
598
  *_group_options(query_group_ids, reference_group_ids, group_distances, q, r),
@@ -611,8 +616,9 @@ def _batch_options(axes, matrix, gap_open, gap_extend, gap_prior, threads):
611
616
  go = _batch_integer(_default_gap_open(matrix) if gap_open is None else gap_open, "gap_open")
612
617
  ge = _batch_integer(gap_extend, "gap_extend")
613
618
  threads = _batch_integer(threads, "threads")
614
- width = max((len(s) for axis in axes for s in axis), default=0)
615
- cube = _prior_cube(gap_prior, width) if gap_prior is not None else []
619
+ lengths = {len(s) for axis in axes for s in axis}
620
+ width = max(lengths, default=0)
621
+ cube = _prior_cube(gap_prior, width, lengths) if gap_prior is not None else []
616
622
  return go, ge, cube, width, threads
617
623
 
618
624
 
@@ -915,11 +921,11 @@ def _additional_geometry(geometry, features):
915
921
  geometry.additional_features = terms
916
922
 
917
923
 
918
- def _lane_prior(geometry, prior, width):
924
+ def _lane_prior(geometry, prior, width, queries, refs):
919
925
  if prior is not None:
920
926
  if not hasattr(geometry, "gap_prior"):
921
927
  raise RuntimeError("lane priors require native grouped gap_prior support")
922
- geometry.gap_prior = _prior_cube(prior, width)
928
+ geometry.gap_prior = _prior_cube(prior, width, {len(s) for s in queries + refs})
923
929
 
924
930
 
925
931
  def _alignment_options(geometry, q, r, mode, scale, left, right, matrix, prior,
@@ -1099,8 +1105,8 @@ def grouped_paired_sum_count_batch(queries_a, queries_b, refs_a, refs_b, thresho
1099
1105
  reference_position_weights_beta, feature_offsets_beta, score_quantum_beta)
1100
1106
  _additional_geometry(alpha, additional_feature_offsets_alpha)
1101
1107
  _additional_geometry(beta, additional_feature_offsets_beta)
1102
- _lane_prior(alpha, gap_prior_alpha, width)
1103
- _lane_prior(beta, gap_prior_beta, width)
1108
+ _lane_prior(alpha, gap_prior_alpha, width, qa, ra)
1109
+ _lane_prior(beta, gap_prior_beta, width, qb, rb)
1104
1110
  _alignment_options(alpha, qa, ra, alignment_mode, alignment_scale, trim_left, trim_right,
1105
1111
  matrix, gap_prior if gap_prior_alpha is None else gap_prior_alpha, position_weights_by_length_alpha, query_position_weights_alpha,
1106
1112
  reference_position_weights_alpha)
@@ -12,6 +12,7 @@
12
12
  #include <nanobind/stl/vector.h>
13
13
 
14
14
  #include <algorithm>
15
+ #include <exception>
15
16
  #include <optional>
16
17
  #include <string>
17
18
  #include <vector>
@@ -186,6 +187,53 @@ nb::list py_search_batch(const Index& idx, const std::vector<std::string>& queri
186
187
  return out;
187
188
  }
188
189
 
190
+ void py_search_batch_consume(const Index& idx, const std::vector<std::string>& queries,
191
+ const PyParams& pp, nb::object consume, int threads, size_t chunk_size, size_t max_hit_bytes) {
192
+ if (!PyCallable_Check(consume.ptr())) throw nb::value_error("consumer must be callable");
193
+ // A callback may mutate the original Python parameters; native workers read
194
+ // only these owned matrix/positional snapshots until the whole pool joins.
195
+ const PyParams owned = pp;
196
+ auto mat = make_matrix(owned, idx.alphabet());
197
+ SearchParams cp = to_cpp(owned, mat ? &*mat : nullptr);
198
+ std::exception_ptr error;
199
+ {
200
+ nb::gil_scoped_release release;
201
+ try {
202
+ idx.search_batch_consume(queries, cp,
203
+ [&](size_t start, size_t stop, CountBuffer&& buffer) {
204
+ nb::gil_scoped_acquire acquire;
205
+ consume(start, stop, nb::cast(std::move(buffer)));
206
+ }, threads, chunk_size, max_hit_bytes);
207
+ } catch (...) { error = std::current_exception(); }
208
+ }
209
+ // Restore the caller's GIL before Python exceptions are restored/destroyed.
210
+ if (error) std::rethrow_exception(error);
211
+ }
212
+
213
+ void py_search_paired_batch_consume(const Index& idx, const Index& other,
214
+ const std::vector<std::string>& queries, const std::vector<std::string>& other_queries,
215
+ const PyParams& pp, nb::object consume, int threads, size_t chunk_size, size_t max_hit_bytes, bool skip_empty) {
216
+ if (!PyCallable_Check(consume.ptr())) throw nb::value_error("consumer must be callable");
217
+ // A callback may mutate the original Python parameters; native workers read
218
+ // only these owned matrix/positional snapshots until the whole pool joins.
219
+ const PyParams owned = pp;
220
+ auto mat = make_matrix(owned, idx.alphabet());
221
+ SearchParams cp = to_cpp(owned, mat ? &*mat : nullptr);
222
+ std::exception_ptr error;
223
+ {
224
+ nb::gil_scoped_release release;
225
+ try {
226
+ idx.search_paired_batch_consume(other, queries, other_queries, cp,
227
+ [&](size_t start, size_t stop, CountBuffer&& buffer, CountBuffer&& other_buffer) {
228
+ nb::gil_scoped_acquire acquire;
229
+ consume(start, stop, nb::cast(std::move(buffer)), nb::cast(std::move(other_buffer)));
230
+ }, threads, chunk_size, max_hit_bytes, skip_empty);
231
+ } catch (...) { error = std::current_exception(); }
232
+ }
233
+ // Restore the caller's GIL before Python exceptions are restored/destroyed.
234
+ if (error) std::rethrow_exception(error);
235
+ }
236
+
189
237
  std::vector<std::vector<uint64_t>> py_edit_histogram_batch(
190
238
  const Index& idx, const std::vector<std::string>& queries,
191
239
  const PyParams& pp, int threads, bool exclude_exact, int max_indels) {
@@ -718,6 +766,24 @@ NB_MODULE(_core, m) {
718
766
  nb::arg("threads") = 0,
719
767
  "Search many queries in parallel (releases the GIL). ``threads=0`` uses all "
720
768
  "cores. Returns one hit list per query, in input order.")
769
+ .def("search_batch_consume", &py_search_batch_consume,
770
+ nb::arg("queries"), nb::arg("params"), nb::arg("consume"), nb::arg("threads") = 0,
771
+ nb::arg("chunk_size") = 32, nb::arg("max_hit_bytes") = 64*1024*1024,
772
+ "One population/pool, serialized consume(start, stop, owned_buffer) callbacks. "
773
+ "Read-only uint32 rows: query_index, ref_id, score, n_subs, n_ins, n_dels. "
774
+ "Every query is complete; block order may vary across threads. Output bytes per "
775
+ "block never exceed max_hit_bytes; an oversized query raises, never truncates. "
776
+ "Native worker scratch also holds the current block of hits. Consumer-retained "
777
+ "buffers remain owned; callers control their retention. Parameters are snapshotted.")
778
+ .def("search_paired_batch_consume", &py_search_paired_batch_consume,
779
+ nb::arg("other"), nb::arg("queries"), nb::arg("other_queries"),
780
+ nb::arg("params"), nb::arg("consume"), nb::arg("threads") = 0,
781
+ nb::arg("chunk_size") = 32, nb::arg("max_hit_bytes") = 64*1024*1024, nb::arg("skip_empty") = false,
782
+ "One pool with two Searchers; consume(start, stop, owned_buffer, other_owned_buffer). "
783
+ "Both six-column buffers cover the same complete query block. Combined output "
784
+ "bytes are bounded by max_hit_bytes; block order may vary. Same-alphabet indexes "
785
+ "and equal-length query populations required. Parameters are snapshotted. "
786
+ "skip_empty=True emits no hits for empty query axes; other axes remain searched.")
721
787
  .def("edit_histogram_batch", &py_edit_histogram_batch,
722
788
  nb::arg("queries"), nb::arg("params"), nb::arg("threads") = 0,
723
789
  nb::arg("exclude_exact") = false, nb::arg("max_indels") = -1,
@@ -5,9 +5,12 @@
5
5
  #include "count_unit.hpp"
6
6
 
7
7
  #include <algorithm>
8
+ #include <atomic>
8
9
  #include <cstring>
9
10
  #include <fstream>
11
+ #include <iterator>
10
12
  #include <limits>
13
+ #include <mutex>
11
14
  #include <stdexcept>
12
15
  #include <vector>
13
16
 
@@ -188,6 +191,131 @@ std::vector<std::vector<Hit>> Index::search_batch(const std::vector<std::string>
188
191
  return results;
189
192
  }
190
193
 
194
+ void Index::search_batch_consume(const std::vector<std::string>& queries,
195
+ const SearchParams& p,
196
+ const std::function<void(size_t, size_t, CountBuffer&&)>& consume,
197
+ int threads, size_t chunk_size, size_t max_hit_bytes) const {
198
+ constexpr size_t columns = 6;
199
+ const size_t max_rows = max_hit_bytes / (columns * sizeof(uint32_t));
200
+ if (!chunk_size || !max_rows || !consume)
201
+ throw std::invalid_argument("positive chunk_size/max_hit_bytes and a consumer required");
202
+ if (queries.size() > uint64_t(std::numeric_limits<uint32_t>::max()) + 1)
203
+ throw std::overflow_error("query indices exceed the uint32 output buffer");
204
+ // Bound both the primitive output and the per-worker Hit vectors. A single
205
+ // query may still exceed the explicit output cap; detect that without truncation.
206
+ chunk_size = std::min(chunk_size, std::max(size_t(1), max_rows / std::max(size_t(1), size_t(size()))));
207
+ const size_t blocks = queries.size() / chunk_size + (queries.size() % chunk_size != 0);
208
+ std::atomic<bool> cancelled{false};
209
+ std::mutex callback_mutex;
210
+ struct Local { Searcher searcher; std::vector<std::vector<Hit>> hits; };
211
+ parallel_for(blocks, threads,
212
+ [&] { return Local{Searcher(*this), std::vector<std::vector<Hit>>(chunk_size)}; },
213
+ [&](size_t block, Local& worker) {
214
+ if (cancelled.load()) return;
215
+ try {
216
+ const size_t start = block * chunk_size;
217
+ const size_t stop = start + std::min(chunk_size, queries.size() - start);
218
+ size_t rows = 0;
219
+ for (size_t i = start; i < stop; ++i) {
220
+ if (cancelled.load()) return;
221
+ auto& hits = worker.hits[i-start];
222
+ worker.searcher.search_into(queries[i], p, hits);
223
+ if (hits.size() > max_rows - rows)
224
+ throw std::overflow_error("complete query hits exceed max_hit_bytes; increase the explicit buffer limit");
225
+ rows += hits.size();
226
+ }
227
+ CountBuffer buffer;
228
+ buffer.shape = {rows, columns};
229
+ buffer.data.resize(rows * columns);
230
+ size_t offset = 0;
231
+ for (size_t i = start; i < stop; ++i)
232
+ for (const auto& hit : worker.hits[i-start]) {
233
+ const uint32_t values[] = {uint32_t(i), hit.ref_id, uint32_t(hit.score),
234
+ hit.n_subs, hit.n_ins, hit.n_dels};
235
+ std::copy(std::begin(values), std::end(values), buffer.data.begin() + offset);
236
+ offset += columns;
237
+ }
238
+ std::lock_guard<std::mutex> lock(callback_mutex);
239
+ if (!cancelled.load()) {
240
+ try { consume(start, stop, std::move(buffer)); }
241
+ catch (...) { cancelled.store(true); throw; }
242
+ }
243
+ } catch (...) {
244
+ cancelled.store(true);
245
+ throw;
246
+ }
247
+ });
248
+ }
249
+
250
+ void Index::search_paired_batch_consume(const Index& other,
251
+ const std::vector<std::string>& queries, const std::vector<std::string>& other_queries,
252
+ const SearchParams& p,
253
+ const std::function<void(size_t, size_t, CountBuffer&&, CountBuffer&&)>& consume,
254
+ int threads, size_t chunk_size, size_t max_hit_bytes, bool skip_empty) const {
255
+ if (queries.size() != other_queries.size())
256
+ throw std::invalid_argument("paired query populations must have equal lengths");
257
+ if (alphabet() != other.alphabet())
258
+ throw std::invalid_argument("paired indexes must use the same alphabet");
259
+ constexpr size_t columns = 6;
260
+ const size_t max_rows = max_hit_bytes / (columns * sizeof(uint32_t));
261
+ if (!chunk_size || !max_rows || !consume)
262
+ throw std::invalid_argument("positive chunk_size/max_hit_bytes and a consumer required");
263
+ if (queries.size() > uint64_t(std::numeric_limits<uint32_t>::max()) + 1)
264
+ throw std::overflow_error("query indices exceed the uint32 output buffer");
265
+ // Bound both the primitive output and the per-worker Hit vectors. A single
266
+ // query may still exceed the explicit output cap; detect that without truncation.
267
+ chunk_size = std::min(chunk_size, std::max(size_t(1), max_rows / std::max(size_t(1), size_t(size()) + size_t(other.size()))));
268
+ const size_t blocks = queries.size() / chunk_size + (queries.size() % chunk_size != 0);
269
+ std::atomic<bool> cancelled{false};
270
+ std::mutex callback_mutex;
271
+ struct Local { Searcher searcher, other_searcher; std::vector<std::vector<Hit>> hits, other_hits; };
272
+ parallel_for(blocks, threads,
273
+ [&] { return Local{Searcher(*this), Searcher(other), std::vector<std::vector<Hit>>(chunk_size), std::vector<std::vector<Hit>>(chunk_size)}; },
274
+ [&](size_t block, Local& worker) {
275
+ if (cancelled.load()) return;
276
+ try {
277
+ const size_t start = block * chunk_size;
278
+ const size_t stop = start + std::min(chunk_size, queries.size() - start);
279
+ size_t rows = 0;
280
+ for (size_t i = start; i < stop; ++i) {
281
+ if (cancelled.load()) return;
282
+ auto& hits = worker.hits[i-start];
283
+ if (skip_empty && queries[i].empty()) hits.clear();
284
+ else worker.searcher.search_into(queries[i], p, hits);
285
+ auto& other_hits = worker.other_hits[i-start];
286
+ if (skip_empty && other_queries[i].empty()) other_hits.clear();
287
+ else worker.other_searcher.search_into(other_queries[i], p, other_hits);
288
+ if (hits.size() > max_rows - rows || other_hits.size() > max_rows - rows - hits.size())
289
+ throw std::overflow_error("complete query hits exceed max_hit_bytes; increase the explicit buffer limit");
290
+ rows += hits.size() + other_hits.size();
291
+ }
292
+ auto pack = [&](const auto& groups) {
293
+ CountBuffer buffer;
294
+ size_t count = 0;
295
+ for (size_t i = start; i < stop; ++i) count += groups[i-start].size();
296
+ buffer.shape = {count, columns};
297
+ buffer.data.reserve(count * columns);
298
+ for (size_t i = start; i < stop; ++i)
299
+ for (const auto& hit : groups[i-start]) {
300
+ const uint32_t values[] = {uint32_t(i), hit.ref_id, uint32_t(hit.score),
301
+ hit.n_subs, hit.n_ins, hit.n_dels};
302
+ buffer.data.insert(buffer.data.end(), std::begin(values), std::end(values));
303
+ }
304
+ return buffer;
305
+ };
306
+ auto buffer = pack(worker.hits), other_buffer = pack(worker.other_hits);
307
+ std::lock_guard<std::mutex> lock(callback_mutex);
308
+ if (!cancelled.load()) {
309
+ try { consume(start, stop, std::move(buffer), std::move(other_buffer)); }
310
+ catch (...) { cancelled.store(true); throw; }
311
+ }
312
+ } catch (...) {
313
+ cancelled.store(true);
314
+ throw;
315
+ }
316
+ });
317
+ }
318
+
191
319
  std::vector<std::vector<uint64_t>> Index::edit_histogram_batch(
192
320
  const std::vector<std::string>& queries, const SearchParams& p,
193
321
  int threads, bool exclude_exact, int max_indels) const {
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