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.
Files changed (53) hide show
  1. {seqtree-1.1.1 → seqtree-1.2.0}/CHANGELOG.md +18 -0
  2. {seqtree-1.1.1 → seqtree-1.2.0}/CMakeLists.txt +1 -0
  3. {seqtree-1.1.1 → seqtree-1.2.0}/PKG-INFO +12 -1
  4. {seqtree-1.1.1 → seqtree-1.2.0}/README.md +11 -0
  5. seqtree-1.2.0/RELEASE_AUDIT.md +12 -0
  6. {seqtree-1.1.1 → seqtree-1.2.0}/include/seqtree/seqtree.hpp +29 -3
  7. {seqtree-1.1.1 → seqtree-1.2.0}/pyproject.toml +1 -1
  8. {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/gapblock.py +38 -6
  9. {seqtree-1.1.1 → seqtree-1.2.0}/src/_bindings.cpp +58 -8
  10. seqtree-1.2.0/src/alignment_max.cpp +177 -0
  11. {seqtree-1.1.1 → seqtree-1.2.0}/src/count_unit.hpp +31 -9
  12. {seqtree-1.1.1 → seqtree-1.2.0}/src/engine_seqtm.cpp +2 -2
  13. {seqtree-1.1.1 → seqtree-1.2.0}/src/engines.hpp +1 -1
  14. {seqtree-1.1.1 → seqtree-1.2.0}/src/gapblock.cpp +228 -46
  15. {seqtree-1.1.1 → seqtree-1.2.0}/src/index.cpp +5 -3
  16. {seqtree-1.1.1 → seqtree-1.2.0}/src/searcher.cpp +5 -1
  17. {seqtree-1.1.1 → seqtree-1.2.0}/.gitattributes +0 -0
  18. {seqtree-1.1.1 → seqtree-1.2.0}/.gitignore +0 -0
  19. {seqtree-1.1.1 → seqtree-1.2.0}/LICENSE +0 -0
  20. {seqtree-1.1.1 → seqtree-1.2.0}/include/seqtree/kmer_index.hpp +0 -0
  21. {seqtree-1.1.1 → seqtree-1.2.0}/include/seqtree/parallel.hpp +0 -0
  22. {seqtree-1.1.1 → seqtree-1.2.0}/include/seqtree/text_index.hpp +0 -0
  23. {seqtree-1.1.1 → seqtree-1.2.0}/include/seqtree/types.hpp +0 -0
  24. {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/__init__.py +0 -0
  25. {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/control.py +0 -0
  26. {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
  27. {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/distance.py +0 -0
  28. {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/evalue.py +0 -0
  29. {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/layout.py +0 -0
  30. {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/pairwise.py +0 -0
  31. {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/pmhc.py +0 -0
  32. {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/pmhc_evalue.py +0 -0
  33. {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/py.typed +0 -0
  34. {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/seeds.py +0 -0
  35. {seqtree-1.1.1 → seqtree-1.2.0}/python/seqtree/text_evalue.py +0 -0
  36. {seqtree-1.1.1 → seqtree-1.2.0}/src/atomic_write.hpp +0 -0
  37. {seqtree-1.1.1 → seqtree-1.2.0}/src/blosum45.inc +0 -0
  38. {seqtree-1.1.1 → seqtree-1.2.0}/src/blosum62.inc +0 -0
  39. {seqtree-1.1.1 → seqtree-1.2.0}/src/blosum80.inc +0 -0
  40. {seqtree-1.1.1 → seqtree-1.2.0}/src/codec.cpp +0 -0
  41. {seqtree-1.1.1 → seqtree-1.2.0}/src/distance.cpp +0 -0
  42. {seqtree-1.1.1 → seqtree-1.2.0}/src/engine_seqtrie.cpp +0 -0
  43. {seqtree-1.1.1 → seqtree-1.2.0}/src/kmer_index.cpp +0 -0
  44. {seqtree-1.1.1 → seqtree-1.2.0}/src/pairwise.cpp +0 -0
  45. {seqtree-1.1.1 → seqtree-1.2.0}/src/pairwise_detail.hpp +0 -0
  46. {seqtree-1.1.1 → seqtree-1.2.0}/src/pam100.inc +0 -0
  47. {seqtree-1.1.1 → seqtree-1.2.0}/src/pam250.inc +0 -0
  48. {seqtree-1.1.1 → seqtree-1.2.0}/src/positional_matrix.cpp +0 -0
  49. {seqtree-1.1.1 → seqtree-1.2.0}/src/structural.inc +0 -0
  50. {seqtree-1.1.1 → seqtree-1.2.0}/src/substitution_matrix.cpp +0 -0
  51. {seqtree-1.1.1 → seqtree-1.2.0}/src/text_index.cpp +0 -0
  52. {seqtree-1.1.1 → seqtree-1.2.0}/src/trie.cpp +0 -0
  53. {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
@@ -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.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:
@@ -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.2.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)
@@ -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