seqtree 1.2.0__tar.gz → 1.3.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.
Files changed (53) hide show
  1. {seqtree-1.2.0 → seqtree-1.3.0}/CHANGELOG.md +9 -0
  2. {seqtree-1.2.0 → seqtree-1.3.0}/PKG-INFO +1 -1
  3. {seqtree-1.2.0 → seqtree-1.3.0}/include/seqtree/seqtree.hpp +15 -0
  4. {seqtree-1.2.0 → seqtree-1.3.0}/pyproject.toml +1 -1
  5. {seqtree-1.2.0 → seqtree-1.3.0}/src/_bindings.cpp +66 -0
  6. {seqtree-1.2.0 → seqtree-1.3.0}/src/index.cpp +128 -0
  7. {seqtree-1.2.0 → seqtree-1.3.0}/.gitattributes +0 -0
  8. {seqtree-1.2.0 → seqtree-1.3.0}/.gitignore +0 -0
  9. {seqtree-1.2.0 → seqtree-1.3.0}/CMakeLists.txt +0 -0
  10. {seqtree-1.2.0 → seqtree-1.3.0}/LICENSE +0 -0
  11. {seqtree-1.2.0 → seqtree-1.3.0}/README.md +0 -0
  12. {seqtree-1.2.0 → seqtree-1.3.0}/RELEASE_AUDIT.md +0 -0
  13. {seqtree-1.2.0 → seqtree-1.3.0}/include/seqtree/kmer_index.hpp +0 -0
  14. {seqtree-1.2.0 → seqtree-1.3.0}/include/seqtree/parallel.hpp +0 -0
  15. {seqtree-1.2.0 → seqtree-1.3.0}/include/seqtree/text_index.hpp +0 -0
  16. {seqtree-1.2.0 → seqtree-1.3.0}/include/seqtree/types.hpp +0 -0
  17. {seqtree-1.2.0 → seqtree-1.3.0}/python/seqtree/__init__.py +0 -0
  18. {seqtree-1.2.0 → seqtree-1.3.0}/python/seqtree/control.py +0 -0
  19. {seqtree-1.2.0 → seqtree-1.3.0}/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
  20. {seqtree-1.2.0 → seqtree-1.3.0}/python/seqtree/distance.py +0 -0
  21. {seqtree-1.2.0 → seqtree-1.3.0}/python/seqtree/evalue.py +0 -0
  22. {seqtree-1.2.0 → seqtree-1.3.0}/python/seqtree/gapblock.py +0 -0
  23. {seqtree-1.2.0 → seqtree-1.3.0}/python/seqtree/layout.py +0 -0
  24. {seqtree-1.2.0 → seqtree-1.3.0}/python/seqtree/pairwise.py +0 -0
  25. {seqtree-1.2.0 → seqtree-1.3.0}/python/seqtree/pmhc.py +0 -0
  26. {seqtree-1.2.0 → seqtree-1.3.0}/python/seqtree/pmhc_evalue.py +0 -0
  27. {seqtree-1.2.0 → seqtree-1.3.0}/python/seqtree/py.typed +0 -0
  28. {seqtree-1.2.0 → seqtree-1.3.0}/python/seqtree/seeds.py +0 -0
  29. {seqtree-1.2.0 → seqtree-1.3.0}/python/seqtree/text_evalue.py +0 -0
  30. {seqtree-1.2.0 → seqtree-1.3.0}/src/alignment_max.cpp +0 -0
  31. {seqtree-1.2.0 → seqtree-1.3.0}/src/atomic_write.hpp +0 -0
  32. {seqtree-1.2.0 → seqtree-1.3.0}/src/blosum45.inc +0 -0
  33. {seqtree-1.2.0 → seqtree-1.3.0}/src/blosum62.inc +0 -0
  34. {seqtree-1.2.0 → seqtree-1.3.0}/src/blosum80.inc +0 -0
  35. {seqtree-1.2.0 → seqtree-1.3.0}/src/codec.cpp +0 -0
  36. {seqtree-1.2.0 → seqtree-1.3.0}/src/count_unit.hpp +0 -0
  37. {seqtree-1.2.0 → seqtree-1.3.0}/src/distance.cpp +0 -0
  38. {seqtree-1.2.0 → seqtree-1.3.0}/src/engine_seqtm.cpp +0 -0
  39. {seqtree-1.2.0 → seqtree-1.3.0}/src/engine_seqtrie.cpp +0 -0
  40. {seqtree-1.2.0 → seqtree-1.3.0}/src/engines.hpp +0 -0
  41. {seqtree-1.2.0 → seqtree-1.3.0}/src/gapblock.cpp +0 -0
  42. {seqtree-1.2.0 → seqtree-1.3.0}/src/kmer_index.cpp +0 -0
  43. {seqtree-1.2.0 → seqtree-1.3.0}/src/pairwise.cpp +0 -0
  44. {seqtree-1.2.0 → seqtree-1.3.0}/src/pairwise_detail.hpp +0 -0
  45. {seqtree-1.2.0 → seqtree-1.3.0}/src/pam100.inc +0 -0
  46. {seqtree-1.2.0 → seqtree-1.3.0}/src/pam250.inc +0 -0
  47. {seqtree-1.2.0 → seqtree-1.3.0}/src/positional_matrix.cpp +0 -0
  48. {seqtree-1.2.0 → seqtree-1.3.0}/src/searcher.cpp +0 -0
  49. {seqtree-1.2.0 → seqtree-1.3.0}/src/structural.inc +0 -0
  50. {seqtree-1.2.0 → seqtree-1.3.0}/src/substitution_matrix.cpp +0 -0
  51. {seqtree-1.2.0 → seqtree-1.3.0}/src/text_index.cpp +0 -0
  52. {seqtree-1.2.0 → seqtree-1.3.0}/src/trie.cpp +0 -0
  53. {seqtree-1.2.0 → seqtree-1.3.0}/src/trie.hpp +0 -0
@@ -3,6 +3,15 @@
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.0] — 2026-10-19
7
+
8
+ ### Added
9
+
10
+ - Bounded complete-query hit consumers for single and coordinated two-index
11
+ searches, with owned numeric buffers, explicit byte budgets and worker cleanup.
12
+ - Optional empty query axes in coordinated search retain original query offsets
13
+ and supported marginal evidence without creating a query for the missing axis.
14
+
6
15
  ## [1.2.0] — 2026-10-17
7
16
 
8
17
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: seqtree
3
- Version: 1.2.0
3
+ Version: 1.3.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>
@@ -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.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"
@@ -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