seqtree 1.1.1__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.1.1 → seqtree-1.3.0}/CHANGELOG.md +27 -0
  2. {seqtree-1.1.1 → seqtree-1.3.0}/CMakeLists.txt +1 -0
  3. {seqtree-1.1.1 → seqtree-1.3.0}/PKG-INFO +12 -1
  4. {seqtree-1.1.1 → seqtree-1.3.0}/README.md +11 -0
  5. seqtree-1.3.0/RELEASE_AUDIT.md +12 -0
  6. {seqtree-1.1.1 → seqtree-1.3.0}/include/seqtree/seqtree.hpp +44 -3
  7. {seqtree-1.1.1 → seqtree-1.3.0}/pyproject.toml +1 -1
  8. {seqtree-1.1.1 → seqtree-1.3.0}/python/seqtree/gapblock.py +38 -6
  9. {seqtree-1.1.1 → seqtree-1.3.0}/src/_bindings.cpp +124 -8
  10. seqtree-1.3.0/src/alignment_max.cpp +177 -0
  11. {seqtree-1.1.1 → seqtree-1.3.0}/src/count_unit.hpp +31 -9
  12. {seqtree-1.1.1 → seqtree-1.3.0}/src/engine_seqtm.cpp +2 -2
  13. {seqtree-1.1.1 → seqtree-1.3.0}/src/engines.hpp +1 -1
  14. {seqtree-1.1.1 → seqtree-1.3.0}/src/gapblock.cpp +228 -46
  15. {seqtree-1.1.1 → seqtree-1.3.0}/src/index.cpp +133 -3
  16. {seqtree-1.1.1 → seqtree-1.3.0}/src/searcher.cpp +5 -1
  17. {seqtree-1.1.1 → seqtree-1.3.0}/.gitattributes +0 -0
  18. {seqtree-1.1.1 → seqtree-1.3.0}/.gitignore +0 -0
  19. {seqtree-1.1.1 → seqtree-1.3.0}/LICENSE +0 -0
  20. {seqtree-1.1.1 → seqtree-1.3.0}/include/seqtree/kmer_index.hpp +0 -0
  21. {seqtree-1.1.1 → seqtree-1.3.0}/include/seqtree/parallel.hpp +0 -0
  22. {seqtree-1.1.1 → seqtree-1.3.0}/include/seqtree/text_index.hpp +0 -0
  23. {seqtree-1.1.1 → seqtree-1.3.0}/include/seqtree/types.hpp +0 -0
  24. {seqtree-1.1.1 → seqtree-1.3.0}/python/seqtree/__init__.py +0 -0
  25. {seqtree-1.1.1 → seqtree-1.3.0}/python/seqtree/control.py +0 -0
  26. {seqtree-1.1.1 → seqtree-1.3.0}/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
  27. {seqtree-1.1.1 → seqtree-1.3.0}/python/seqtree/distance.py +0 -0
  28. {seqtree-1.1.1 → seqtree-1.3.0}/python/seqtree/evalue.py +0 -0
  29. {seqtree-1.1.1 → seqtree-1.3.0}/python/seqtree/layout.py +0 -0
  30. {seqtree-1.1.1 → seqtree-1.3.0}/python/seqtree/pairwise.py +0 -0
  31. {seqtree-1.1.1 → seqtree-1.3.0}/python/seqtree/pmhc.py +0 -0
  32. {seqtree-1.1.1 → seqtree-1.3.0}/python/seqtree/pmhc_evalue.py +0 -0
  33. {seqtree-1.1.1 → seqtree-1.3.0}/python/seqtree/py.typed +0 -0
  34. {seqtree-1.1.1 → seqtree-1.3.0}/python/seqtree/seeds.py +0 -0
  35. {seqtree-1.1.1 → seqtree-1.3.0}/python/seqtree/text_evalue.py +0 -0
  36. {seqtree-1.1.1 → seqtree-1.3.0}/src/atomic_write.hpp +0 -0
  37. {seqtree-1.1.1 → seqtree-1.3.0}/src/blosum45.inc +0 -0
  38. {seqtree-1.1.1 → seqtree-1.3.0}/src/blosum62.inc +0 -0
  39. {seqtree-1.1.1 → seqtree-1.3.0}/src/blosum80.inc +0 -0
  40. {seqtree-1.1.1 → seqtree-1.3.0}/src/codec.cpp +0 -0
  41. {seqtree-1.1.1 → seqtree-1.3.0}/src/distance.cpp +0 -0
  42. {seqtree-1.1.1 → seqtree-1.3.0}/src/engine_seqtrie.cpp +0 -0
  43. {seqtree-1.1.1 → seqtree-1.3.0}/src/kmer_index.cpp +0 -0
  44. {seqtree-1.1.1 → seqtree-1.3.0}/src/pairwise.cpp +0 -0
  45. {seqtree-1.1.1 → seqtree-1.3.0}/src/pairwise_detail.hpp +0 -0
  46. {seqtree-1.1.1 → seqtree-1.3.0}/src/pam100.inc +0 -0
  47. {seqtree-1.1.1 → seqtree-1.3.0}/src/pam250.inc +0 -0
  48. {seqtree-1.1.1 → seqtree-1.3.0}/src/positional_matrix.cpp +0 -0
  49. {seqtree-1.1.1 → seqtree-1.3.0}/src/structural.inc +0 -0
  50. {seqtree-1.1.1 → seqtree-1.3.0}/src/substitution_matrix.cpp +0 -0
  51. {seqtree-1.1.1 → seqtree-1.3.0}/src/text_index.cpp +0 -0
  52. {seqtree-1.1.1 → seqtree-1.3.0}/src/trie.cpp +0 -0
  53. {seqtree-1.1.1 → seqtree-1.3.0}/src/trie.hpp +0 -0
@@ -3,6 +3,33 @@
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
+
15
+ ## [1.2.0] — 2026-10-17
16
+
17
+ ### Fixed
18
+
19
+ - Exact exclusion masks matching hits without shrinking model populations or
20
+ supplied annotation counters, including withheld-label models.
21
+
22
+ ### Added
23
+
24
+ - Fixed query/reference partitions for native group audits.
25
+ - Bounded sparse grouped radius-hit output with deterministic ordering and explicit memory-cap failures.
26
+ - Optional grouped geometry compatible with the supported nanobind 2.5 floor.
27
+
28
+ - Generic float64 global-alignment target maxima with numeric annotation CSR,
29
+ group offsets, explicit matrix symbols and independent candidate edit budgets.
30
+ - Optional aggregate insertion/deletion limits for batched edit histograms;
31
+ omitted and redundant limits preserve the existing count paths.
32
+
6
33
  ## [1.1.0] — 2026-10-11
7
34
 
8
35
  ### Added
@@ -30,6 +30,7 @@ add_library(seqtree_core STATIC
30
30
  src/searcher.cpp
31
31
  src/gapblock.cpp
32
32
  src/pairwise.cpp
33
+ src/alignment_max.cpp
33
34
  src/distance.cpp
34
35
  )
35
36
  target_include_directories(seqtree_core PUBLIC include PRIVATE src)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: seqtree
3
- Version: 1.1.1
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>
@@ -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.
@@ -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.
@@ -105,6 +107,14 @@ private:
105
107
  std::vector<uint8_t> masked_; // [width]
106
108
  };
107
109
 
110
+ struct AlignmentMaxResult {
111
+ std::vector<double> scores, raw_similarity, normalized_similarity, pfit, informativeness;
112
+ std::vector<uint64_t> counts, modeled_counts, aa_counts, exact_counts, candidate_counts, nw_pairs;
113
+ std::vector<int64_t> best_ref_ids, best_lane_ids;
114
+ std::vector<uint32_t> gap_counts;
115
+ std::vector<uint8_t> query_supported;
116
+ };
117
+
108
118
  // Immutable after build. Lock-free for concurrent reads; share one Index across
109
119
  // threads, give each thread its own Searcher.
110
120
  class Index {
@@ -126,10 +136,34 @@ public:
126
136
  std::vector<std::vector<Hit>> search_batch(const std::vector<std::string>& queries,
127
137
  const SearchParams&, int threads = 0) const;
128
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
+
129
152
  // Exact edit-count bins 0..max_total_edits; one reusable hit buffer per worker.
130
153
  std::vector<std::vector<uint64_t>> edit_histogram_batch(
131
154
  const std::vector<std::string>& queries, const SearchParams&,
132
- int threads = 0, bool exclude_exact = false) const;
155
+ int threads = 0, bool exclude_exact = false, int max_indels = -1) const;
156
+
157
+ // Numeric requested-label maxima; float NW selection is independent of candidate scope.
158
+ AlignmentMaxResult alignment_max_batch(const std::vector<std::string>& queries,
159
+ const SearchParams&, const std::vector<std::vector<double>>& similarity, const std::string& symbols, int max_indels,
160
+ const std::vector<uint32_t>& query_target_offsets, const std::vector<uint32_t>& query_target_ids,
161
+ const std::vector<uint32_t>& reference_target_offsets, const std::vector<uint32_t>& reference_target_ids,
162
+ const std::vector<int32_t>& target_class_ids, const std::vector<uint32_t>& reference_class_offsets,
163
+ const std::vector<int32_t>& reference_class_ids, const std::vector<std::vector<int32_t>>& query_group_ids,
164
+ const std::vector<std::vector<int32_t>>& reference_group_ids, const std::vector<std::vector<std::vector<double>>>& group_offsets,
165
+ const std::vector<uint64_t>& reference_other_counts, const std::vector<uint64_t>& reference_multiplicities,
166
+ const std::vector<double>& coefficients, int threads=0, bool exclude_exact=false) const;
133
167
 
134
168
  // Per-query count of seqtm collisions: how often a reference was re-reached via a
135
169
  // different edit path during branch-and-bound (0 for the seqtrie engine / Hamming).
@@ -155,7 +189,7 @@ public:
155
189
  ~Searcher();
156
190
 
157
191
  std::vector<Hit> search(std::string_view query, const SearchParams&);
158
- void search_into(std::string_view query, const SearchParams&, std::vector<Hit>& out);
192
+ void search_into(std::string_view query, const SearchParams&, std::vector<Hit>& out, int max_indels = -1);
159
193
  bool search_top(std::string_view query, const SearchParams&, Hit& out);
160
194
 
161
195
  // seqtm collisions from the most recent search_into on this Searcher.
@@ -285,7 +319,9 @@ gapblock_group_audit_batch(const std::vector<std::string>& queries,
285
319
  const std::vector<std::vector<int32_t>>& group_distances = {},
286
320
  const std::vector<std::vector<int32_t>>& query_weights = {},
287
321
  const std::vector<std::vector<int32_t>>& reference_weights = {},
288
- const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt);
322
+ const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt,
323
+ const std::vector<int32_t>& query_partitions = {},
324
+ const std::vector<int32_t>& reference_partitions = {});
289
325
 
290
326
  std::vector<std::vector<Hit>> gapblock_topk_batch(const std::vector<std::string>& queries,
291
327
  const std::vector<std::string>& refs, uint32_t k, Alphabet alphabet,
@@ -371,6 +407,10 @@ struct GroupedGeometry {
371
407
  int32_t alignment_scale = 1;
372
408
  uint32_t trim_left = 0, trim_right = 0;
373
409
  int32_t score_quantum = 1;
410
+ bool return_hits = false;
411
+ size_t max_hit_bytes = 64*1024*1024;
412
+ std::optional<std::vector<int32_t>> query_target_ids;
413
+ std::optional<std::vector<int64_t>> query_reference_ids;
374
414
  std::vector<std::vector<int32_t>> weights, group_distances, query_weights, reference_weights;
375
415
  std::vector<int32_t> query_groups, reference_groups;
376
416
  std::optional<PairedFeatureOffsets> features;
@@ -382,6 +422,7 @@ struct CountBuffer {
382
422
  struct GroupedCountChunk {
383
423
  size_t start = 0, stop = 0;
384
424
  CountBuffer counts, sizes, exact_counts;
425
+ CountBuffer hits{{},{0,3}};
385
426
  };
386
427
  class GroupedCountBatch {
387
428
  public:
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "seqtree"
7
- version = "1.1.1"
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"
@@ -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. O(Q) output; no sparse hit objects.
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
- geometry.features = features
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 punctures every curve and population size by full sequence identity.
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)
@@ -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,15 +187,62 @@ 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
- const PyParams& pp, int threads, bool exclude_exact) {
239
+ const PyParams& pp, int threads, bool exclude_exact, int max_indels) {
192
240
  if (pp.max_total_edits < 1 || pp.max_total_edits > 65535)
193
241
  throw nb::value_error("edit histogram requires max_total_edits in [1, 65535]");
194
242
  auto mat = make_matrix(pp, idx.alphabet());
195
243
  SearchParams cp = to_cpp(pp, mat ? &*mat : nullptr);
196
244
  nb::gil_scoped_release release;
197
- return idx.edit_histogram_batch(queries, cp, threads, exclude_exact);
245
+ return idx.edit_histogram_batch(queries, cp, threads, exclude_exact, max_indels);
198
246
  }
199
247
 
200
248
  std::vector<uint64_t> py_collisions_batch(const Index& idx, const std::vector<std::string>& queries,
@@ -335,6 +383,26 @@ ArrayView view_of(const nb::object& owner, const std::vector<T>& v, char fmt) {
335
383
  return ArrayView{owner, v.data(), v.size(), sizeof(T), {fmt, 0}};
336
384
  }
337
385
 
386
+ nb::dict alignment_max_arrays(const nb::object& self) {
387
+ const auto& r=nb::cast<const AlignmentMaxResult&>(self); nb::dict d;
388
+ d["scores"]=view_of(self,r.scores,'d');
389
+ d["raw_similarity"]=view_of(self,r.raw_similarity,'d');
390
+ d["normalized_similarity"]=view_of(self,r.normalized_similarity,'d');
391
+ d["pfit"]=view_of(self,r.pfit,'d');
392
+ d["informativeness"]=view_of(self,r.informativeness,'d');
393
+ d["counts"]=view_of(self,r.counts,'Q');
394
+ d["modeled_counts"]=view_of(self,r.modeled_counts,'Q');
395
+ d["aa_counts"]=view_of(self,r.aa_counts,'Q');
396
+ d["exact_counts"]=view_of(self,r.exact_counts,'Q');
397
+ d["candidate_counts"]=view_of(self,r.candidate_counts,'Q');
398
+ d["nw_pairs"]=view_of(self,r.nw_pairs,'Q');
399
+ d["best_ref_ids"]=view_of(self,r.best_ref_ids,'q');
400
+ d["best_lane_ids"]=view_of(self,r.best_lane_ids,'q');
401
+ d["gap_counts"]=view_of(self,r.gap_counts,'I');
402
+ d["query_supported"]=view_of(self,r.query_supported,'B');
403
+ return d;
404
+ }
405
+
338
406
  // One hit as a Python object. Built only for the query actually asked for, never for the whole
339
407
  // batch -- the flat arrays are what a 445k-query run should read.
340
408
  struct PyTextHit {
@@ -395,6 +463,7 @@ ScoreMatrix py_gapblock_matrix(const std::vector<std::string>& queries,
395
463
  } // namespace
396
464
 
397
465
  NB_MODULE(_core, m) {
466
+ m.attr("exact_exclusion_hits_only") = true;
398
467
  m.doc() = "seqtree: fuzzy biological-sequence search (C++ core)";
399
468
 
400
469
  nb::class_<SubstitutionMatrix>(m, "SubstitutionMatrix",
@@ -663,6 +732,9 @@ NB_MODULE(_core, m) {
663
732
  return "Alignment(score=" + std::to_string(a.score) + ", ops='" + a.ops + "')";
664
733
  });
665
734
 
735
+ nb::class_<AlignmentMaxResult>(m,"AlignmentMaxResult")
736
+ .def("arrays",&alignment_max_arrays,"Read-only zero-copy requested-target CSR buffers; views own their result.");
737
+
666
738
  nb::class_<Index>(m, "Index",
667
739
  "Immutable search index over a set of reference sequences. Build once, "
668
740
  "then query concurrently; reference id is the position in ``refs``.")
@@ -694,13 +766,52 @@ NB_MODULE(_core, m) {
694
766
  nb::arg("threads") = 0,
695
767
  "Search many queries in parallel (releases the GIL). ``threads=0`` uses all "
696
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.")
697
787
  .def("edit_histogram_batch", &py_edit_histogram_batch,
698
788
  nb::arg("queries"), nb::arg("params"), nb::arg("threads") = 0,
699
- nb::arg("exclude_exact") = false,
789
+ nb::arg("exclude_exact") = false, nb::arg("max_indels") = -1,
700
790
  "Exact reference counts by total n_subs+n_ins+n_dels, bins 0..max_total_edits. "
701
791
  "Requires explicit positive max_total_edits and uncapped seqtm/auto all-hit search. "
792
+ "max_indels>=0 also caps total insertions+deletions; -1 preserves directional-only caps. "
702
793
  "Duplicates count separately; exclude_exact retains bin 0 with count zero. threads<=0 uses the detected available CPU budget. Releases the GIL; "
703
794
  "one reusable hit buffer per worker, no all-query hit vectors.")
795
+ .def("alignment_max_batch", [](const Index& idx,const std::vector<std::string>& queries,const PyParams& pp,
796
+ const std::vector<std::vector<double>>& sim,const std::string& symbols,int max_indels,
797
+ const std::vector<uint32_t>& qo,const std::vector<uint32_t>& qt,const std::vector<uint32_t>& ro,
798
+ const std::vector<uint32_t>& rt,const std::vector<int32_t>& tc,const std::vector<uint32_t>& co,
799
+ const std::vector<int32_t>& ci,const std::vector<std::vector<int32_t>>& qg,
800
+ const std::vector<std::vector<int32_t>>& rg,const std::vector<std::vector<std::vector<double>>>& offsets,
801
+ const std::vector<uint64_t>& other,const std::vector<uint64_t>& multiplicities,
802
+ const std::vector<double>& coefficients,int threads,bool exclude_exact) {
803
+ auto mat=make_matrix(pp,idx.alphabet()); auto cp=to_cpp(pp,mat?&*mat:nullptr);
804
+ nb::gil_scoped_release release;
805
+ 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);
806
+ }, nb::arg("queries"),nb::arg("params"),nb::arg("similarity"),nb::arg("similarity_symbols"),nb::arg("max_indels"),nb::kw_only(),
807
+ nb::arg("query_target_offsets"),nb::arg("query_target_ids"),nb::arg("reference_target_offsets"),nb::arg("reference_target_ids"),
808
+ nb::arg("target_class_ids"),nb::arg("reference_class_offsets"),nb::arg("reference_class_ids"),
809
+ nb::arg("query_group_ids"),nb::arg("reference_group_ids"),nb::arg("group_offsets"),nb::arg("reference_other_counts"),
810
+ nb::arg("reference_multiplicities"),nb::arg("coefficients"),nb::arg("threads")=0,nb::arg("exclude_exact")=false,
811
+ "Fused numeric requested-label max of cloglog(offset+c3*(rawNW-maxself)+gapcoef*g)*finite information. "
812
+ "Candidate scope is independent of float64 global NW; selection gap=max(abs(similarity)); "
813
+ "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. "
814
+ "Target filtering precedes NW. Use bounded query chunks against the same immutable Index. GIL released.")
704
815
  .def("align", &py_align, nb::arg("ref_id"), nb::arg("query"), nb::arg("params"),
705
816
  "Compute a global alignment between ``query`` and a reference, on demand.")
706
817
  .def("collisions_batch", &py_collisions_batch, nb::arg("queries"), nb::arg("params"),
@@ -839,9 +950,14 @@ NB_MODULE(_core, m) {
839
950
  nb::class_<GroupedCountChunk>(m,"GroupedCountChunk")
840
951
  .def_ro("start",&GroupedCountChunk::start).def_ro("stop",&GroupedCountChunk::stop)
841
952
  .def_ro("counts",&GroupedCountChunk::counts).def_ro("sizes",&GroupedCountChunk::sizes)
842
- .def_ro("exact_counts",&GroupedCountChunk::exact_counts);
953
+ .def_ro("exact_counts",&GroupedCountChunk::exact_counts)
954
+ .def_ro("hits",&GroupedCountChunk::hits);
843
955
  nb::class_<GroupedGeometry>(m,"_GroupedGeometry")
844
956
  .def(nb::init<>()).def_rw("weights",&GroupedGeometry::weights)
957
+ .def_rw("return_hits",&GroupedGeometry::return_hits)
958
+ .def_rw("max_hit_bytes",&GroupedGeometry::max_hit_bytes)
959
+ .def_rw("query_target_ids",&GroupedGeometry::query_target_ids)
960
+ .def_rw("query_reference_ids",&GroupedGeometry::query_reference_ids)
845
961
  .def_rw("additional_features",&GroupedGeometry::additional_features)
846
962
  .def_rw("gap_prior",&GroupedGeometry::gap_prior)
847
963
  .def_rw("alignment_mode",&GroupedGeometry::alignment_mode)
@@ -895,7 +1011,7 @@ NB_MODULE(_core, m) {
895
1011
  nb::arg("reference_target_offsets"),nb::arg("reference_target_ids"),
896
1012
  nb::arg("target_peptide_ids"),nb::arg("reference_exclusive_peptide_ids"),
897
1013
  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"),
1014
+ nb::arg("alphabet"),nb::arg("matrix").none(),nb::arg("gap_open"),nb::arg("gap_extend"),
899
1015
  nb::arg("prior"),nb::arg("prior_width"),nb::arg("threads"),nb::arg("exclude_exact"),
900
1016
  nb::arg("alpha"),nb::arg("beta")=std::nullopt,
901
1017
  nb::arg("queries_beta")=std::vector<std::string>{},nb::arg("refs_beta")=std::vector<std::string>{},
@@ -923,11 +1039,11 @@ NB_MODULE(_core, m) {
923
1039
  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
1040
  }, 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
1041
 
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) {
1042
+ 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
1043
  const auto alph = parse_alphabet(alphabet);
928
1044
  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.");
1045
+ 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);
1046
+ }, 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
1047
 
932
1048
  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
1049
  const auto alph = parse_alphabet(alphabet);