seqtree 1.1.0__tar.gz → 1.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {seqtree-1.1.0 → seqtree-1.1.1}/PKG-INFO +1 -1
  2. {seqtree-1.1.0 → seqtree-1.1.1}/include/seqtree/seqtree.hpp +72 -6
  3. {seqtree-1.1.0 → seqtree-1.1.1}/pyproject.toml +1 -1
  4. {seqtree-1.1.0 → seqtree-1.1.1}/python/seqtree/gapblock.py +248 -17
  5. {seqtree-1.1.0 → seqtree-1.1.1}/src/_bindings.cpp +117 -18
  6. {seqtree-1.1.0 → seqtree-1.1.1}/src/count_unit.hpp +9 -3
  7. seqtree-1.1.1/src/gapblock.cpp +1028 -0
  8. {seqtree-1.1.0 → seqtree-1.1.1}/src/pairwise.cpp +14 -3
  9. seqtree-1.1.1/src/pairwise_detail.hpp +8 -0
  10. seqtree-1.1.0/src/gapblock.cpp +0 -499
  11. {seqtree-1.1.0 → seqtree-1.1.1}/.gitattributes +0 -0
  12. {seqtree-1.1.0 → seqtree-1.1.1}/.gitignore +0 -0
  13. {seqtree-1.1.0 → seqtree-1.1.1}/CHANGELOG.md +0 -0
  14. {seqtree-1.1.0 → seqtree-1.1.1}/CMakeLists.txt +0 -0
  15. {seqtree-1.1.0 → seqtree-1.1.1}/LICENSE +0 -0
  16. {seqtree-1.1.0 → seqtree-1.1.1}/README.md +0 -0
  17. {seqtree-1.1.0 → seqtree-1.1.1}/include/seqtree/kmer_index.hpp +0 -0
  18. {seqtree-1.1.0 → seqtree-1.1.1}/include/seqtree/parallel.hpp +0 -0
  19. {seqtree-1.1.0 → seqtree-1.1.1}/include/seqtree/text_index.hpp +0 -0
  20. {seqtree-1.1.0 → seqtree-1.1.1}/include/seqtree/types.hpp +0 -0
  21. {seqtree-1.1.0 → seqtree-1.1.1}/python/seqtree/__init__.py +0 -0
  22. {seqtree-1.1.0 → seqtree-1.1.1}/python/seqtree/control.py +0 -0
  23. {seqtree-1.1.0 → seqtree-1.1.1}/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
  24. {seqtree-1.1.0 → seqtree-1.1.1}/python/seqtree/distance.py +0 -0
  25. {seqtree-1.1.0 → seqtree-1.1.1}/python/seqtree/evalue.py +0 -0
  26. {seqtree-1.1.0 → seqtree-1.1.1}/python/seqtree/layout.py +0 -0
  27. {seqtree-1.1.0 → seqtree-1.1.1}/python/seqtree/pairwise.py +0 -0
  28. {seqtree-1.1.0 → seqtree-1.1.1}/python/seqtree/pmhc.py +0 -0
  29. {seqtree-1.1.0 → seqtree-1.1.1}/python/seqtree/pmhc_evalue.py +0 -0
  30. {seqtree-1.1.0 → seqtree-1.1.1}/python/seqtree/py.typed +0 -0
  31. {seqtree-1.1.0 → seqtree-1.1.1}/python/seqtree/seeds.py +0 -0
  32. {seqtree-1.1.0 → seqtree-1.1.1}/python/seqtree/text_evalue.py +0 -0
  33. {seqtree-1.1.0 → seqtree-1.1.1}/src/atomic_write.hpp +0 -0
  34. {seqtree-1.1.0 → seqtree-1.1.1}/src/blosum45.inc +0 -0
  35. {seqtree-1.1.0 → seqtree-1.1.1}/src/blosum62.inc +0 -0
  36. {seqtree-1.1.0 → seqtree-1.1.1}/src/blosum80.inc +0 -0
  37. {seqtree-1.1.0 → seqtree-1.1.1}/src/codec.cpp +0 -0
  38. {seqtree-1.1.0 → seqtree-1.1.1}/src/distance.cpp +0 -0
  39. {seqtree-1.1.0 → seqtree-1.1.1}/src/engine_seqtm.cpp +0 -0
  40. {seqtree-1.1.0 → seqtree-1.1.1}/src/engine_seqtrie.cpp +0 -0
  41. {seqtree-1.1.0 → seqtree-1.1.1}/src/engines.hpp +0 -0
  42. {seqtree-1.1.0 → seqtree-1.1.1}/src/index.cpp +0 -0
  43. {seqtree-1.1.0 → seqtree-1.1.1}/src/kmer_index.cpp +0 -0
  44. {seqtree-1.1.0 → seqtree-1.1.1}/src/pam100.inc +0 -0
  45. {seqtree-1.1.0 → seqtree-1.1.1}/src/pam250.inc +0 -0
  46. {seqtree-1.1.0 → seqtree-1.1.1}/src/positional_matrix.cpp +0 -0
  47. {seqtree-1.1.0 → seqtree-1.1.1}/src/searcher.cpp +0 -0
  48. {seqtree-1.1.0 → seqtree-1.1.1}/src/structural.inc +0 -0
  49. {seqtree-1.1.0 → seqtree-1.1.1}/src/substitution_matrix.cpp +0 -0
  50. {seqtree-1.1.0 → seqtree-1.1.1}/src/text_index.cpp +0 -0
  51. {seqtree-1.1.0 → seqtree-1.1.1}/src/trie.cpp +0 -0
  52. {seqtree-1.1.0 → seqtree-1.1.1}/src/trie.hpp +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: seqtree
3
- Version: 1.1.0
3
+ Version: 1.1.1
4
4
  Summary: Fast fuzzy search over biological sequences (C++ core, Python bindings)
5
5
  Keywords: sequence-search,fuzzy-matching,CDR3,immunology,bioinformatics,trie
6
6
  Author-Email: ISALGO laboratory <mikhail.shugay@gmail.com>
@@ -3,6 +3,7 @@
3
3
 
4
4
  #include <cstdint>
5
5
  #include <memory>
6
+ #include <optional>
6
7
  #include <string>
7
8
  #include <string_view>
8
9
  #include <tuple>
@@ -228,6 +229,20 @@ std::vector<int32_t> align_dist_matrix(const std::vector<std::string>& queries,
228
229
  // Both axes are required; these rows cannot combine with length profiles.
229
230
  // Optional group IDs index a nonnegative rectangular penalty table added per pair.
230
231
  // Exact exclusion remains encoded junction identity, independent of these groups.
232
+ // Factored nonnegative offsets: paired feature codes, one shared code cost table.
233
+ // Round weight * sum(max(head cost, tail cost)) / depth to nearest, ties even,
234
+ // then multiply by scale. Input descriptors are copied into immutable batch state.
235
+ struct PairedFeatureOffsets {
236
+ std::vector<std::vector<int32_t>> query_codes, reference_codes, costs;
237
+ double weight;
238
+ int32_t scale;
239
+ PairedFeatureOffsets(std::vector<std::vector<int32_t>> q,
240
+ std::vector<std::vector<int32_t>> r,
241
+ std::vector<std::vector<int32_t>> c, double w = 1.0, int32_t s = 1)
242
+ : query_codes(std::move(q)), reference_codes(std::move(r)), costs(std::move(c)),
243
+ weight(w), scale(s) {}
244
+ };
245
+
231
246
  std::vector<int32_t> gapblock_matrix(const std::vector<std::string>& queries,
232
247
  const std::vector<std::string>& refs, Alphabet,
233
248
  const SubstitutionMatrix* matrix, int32_t gap_open,
@@ -238,7 +253,8 @@ std::vector<int32_t> gapblock_matrix(const std::vector<std::string>& queries,
238
253
  const std::vector<int32_t>& reference_groups = {},
239
254
  const std::vector<std::vector<int32_t>>& group_distances = {},
240
255
  const std::vector<std::vector<int32_t>>& query_weights = {},
241
- const std::vector<std::vector<int32_t>>& reference_weights = {});
256
+ const std::vector<std::vector<int32_t>>& reference_weights = {},
257
+ const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt);
242
258
 
243
259
  // Exhaustive ball rows in reference order; hit edit fields are unset (zero).
244
260
  std::vector<std::vector<Hit>> gapblock_ball_batch(const std::vector<std::string>& queries,
@@ -250,7 +266,8 @@ std::vector<std::vector<Hit>> gapblock_ball_batch(const std::vector<std::string>
250
266
  const std::vector<int32_t>& reference_groups = {},
251
267
  const std::vector<std::vector<int32_t>>& group_distances = {},
252
268
  const std::vector<std::vector<int32_t>>& query_weights = {},
253
- const std::vector<std::vector<int32_t>>& reference_weights = {});
269
+ const std::vector<std::vector<int32_t>>& reference_weights = {},
270
+ const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt);
254
271
 
255
272
  // Exhaustive bounded reductions using the same gapblock cell. Top-k ties use
256
273
  // (penalty, ref_id); paired penalty is max(lane scores). Exact exclusion uses
@@ -267,7 +284,8 @@ gapblock_group_audit_batch(const std::vector<std::string>& queries,
267
284
  const std::vector<int32_t>& reference_groups = {},
268
285
  const std::vector<std::vector<int32_t>>& group_distances = {},
269
286
  const std::vector<std::vector<int32_t>>& query_weights = {},
270
- const std::vector<std::vector<int32_t>>& reference_weights = {});
287
+ const std::vector<std::vector<int32_t>>& reference_weights = {},
288
+ const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt);
271
289
 
272
290
  std::vector<std::vector<Hit>> gapblock_topk_batch(const std::vector<std::string>& queries,
273
291
  const std::vector<std::string>& refs, uint32_t k, Alphabet alphabet,
@@ -287,7 +305,8 @@ std::vector<std::vector<uint64_t>> gapblock_count_batch(const std::vector<std::s
287
305
  const std::vector<int32_t>& reference_groups = {},
288
306
  const std::vector<std::vector<int32_t>>& group_distances = {},
289
307
  const std::vector<std::vector<int32_t>>& query_weights = {},
290
- const std::vector<std::vector<int32_t>>& reference_weights = {});
308
+ const std::vector<std::vector<int32_t>>& reference_weights = {},
309
+ const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt, bool shared_thresholds = false);
291
310
  std::pair<std::vector<std::vector<uint64_t>>, std::vector<std::vector<uint64_t>>> gapblock_count_mass_batch(const std::vector<std::string>& queries,
292
311
  const std::vector<std::string>& refs, const std::vector<std::vector<int32_t>>& thresholds,
293
312
  Alphabet alphabet, const SubstitutionMatrix* matrix, int32_t go, int32_t ge,
@@ -297,7 +316,8 @@ std::pair<std::vector<std::vector<uint64_t>>, std::vector<std::vector<uint64_t>>
297
316
  const std::vector<int32_t>& reference_groups = {},
298
317
  const std::vector<std::vector<int32_t>>& group_distances = {},
299
318
  const std::vector<std::vector<int32_t>>& query_weights = {},
300
- const std::vector<std::vector<int32_t>>& reference_weights = {});
319
+ const std::vector<std::vector<int32_t>>& reference_weights = {},
320
+ const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt, bool shared_thresholds = false);
301
321
  std::vector<std::vector<uint64_t>> gapblock_paired_count_batch(
302
322
  const std::vector<std::string>& qa, const std::vector<std::string>& qb,
303
323
  const std::vector<std::string>& ra, const std::vector<std::string>& rb,
@@ -319,7 +339,9 @@ std::pair<std::vector<std::vector<uint64_t>>, std::vector<std::vector<uint64_t>>
319
339
  const std::vector<std::vector<int32_t>>& query_weights_alpha = {},
320
340
  const std::vector<std::vector<int32_t>>& query_weights_beta = {},
321
341
  const std::vector<std::vector<int32_t>>& reference_weights_alpha = {},
322
- const std::vector<std::vector<int32_t>>& reference_weights_beta = {});
342
+ const std::vector<std::vector<int32_t>>& reference_weights_beta = {},
343
+ const std::optional<PairedFeatureOffsets>& feature_offsets_alpha = std::nullopt,
344
+ const std::optional<PairedFeatureOffsets>& feature_offsets_beta = std::nullopt, bool shared_thresholds = false);
323
345
 
324
346
  // ---------------------------------------------------------------------------------------
325
347
  // Plain string edit distances on raw characters, unit costs. Deliberately alphabet-agnostic:
@@ -341,4 +363,48 @@ std::vector<int32_t> hamming_matrix(const std::vector<std::string>& a,
341
363
  std::vector<int32_t> levenshtein_matrix(const std::vector<std::string>& a,
342
364
  const std::vector<std::string>& b, int threads = 0);
343
365
 
366
+ // Generic label histograms over one prepared native query/reference population.
367
+ struct GroupedGeometry {
368
+ std::vector<PairedFeatureOffsets> additional_features;
369
+ std::optional<std::vector<int32_t>> gap_prior;
370
+ std::string alignment_mode = "gapblock";
371
+ int32_t alignment_scale = 1;
372
+ uint32_t trim_left = 0, trim_right = 0;
373
+ int32_t score_quantum = 1;
374
+ std::vector<std::vector<int32_t>> weights, group_distances, query_weights, reference_weights;
375
+ std::vector<int32_t> query_groups, reference_groups;
376
+ std::optional<PairedFeatureOffsets> features;
377
+ };
378
+ struct CountBuffer {
379
+ std::vector<uint32_t> data;
380
+ std::vector<size_t> shape;
381
+ };
382
+ struct GroupedCountChunk {
383
+ size_t start = 0, stop = 0;
384
+ CountBuffer counts, sizes, exact_counts;
385
+ };
386
+ class GroupedCountBatch {
387
+ public:
388
+ GroupedCountBatch(const std::vector<std::string>& qa, const std::vector<std::string>& ra,
389
+ std::vector<int32_t> thresholds, std::vector<uint32_t> target_offsets,
390
+ std::vector<uint32_t> target_ids, std::vector<int32_t> target_peptides,
391
+ std::vector<int32_t> exclusive_peptides, std::vector<int32_t> query_genes,
392
+ std::vector<int32_t> reference_genes, size_t chunk_size, Alphabet alphabet,
393
+ const SubstitutionMatrix* matrix, int32_t go, int32_t ge, std::vector<int32_t> prior,
394
+ uint32_t width, int threads, bool exclude_exact, const GroupedGeometry& alpha,
395
+ const std::optional<GroupedGeometry>& beta = std::nullopt,
396
+ const std::vector<std::string>& qb = {}, const std::vector<std::string>& rb = {},
397
+ std::optional<std::vector<int32_t>> query_withheld_peptides = std::nullopt,
398
+ std::vector<uint32_t> reference_peptide_offsets = {},
399
+ std::vector<int32_t> reference_peptide_ids = {});
400
+ std::optional<GroupedCountChunk> next();
401
+ size_t query_count() const;
402
+ size_t reference_count() const;
403
+ size_t target_count() const;
404
+ size_t peptide_count() const;
405
+ private:
406
+ struct Impl;
407
+ std::shared_ptr<Impl> impl_;
408
+ };
409
+
344
410
  } // namespace seqtree
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "seqtree"
7
- version = "1.1.0"
7
+ version = "1.1.1"
8
8
  description = "Fast fuzzy search over biological sequences (C++ core, Python bindings)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -40,7 +40,7 @@ import collections
40
40
  import math
41
41
  from collections.abc import Callable, Iterable, Sequence
42
42
 
43
- from ._core import Index, ScoreMatrix, SearchParams, SubstitutionMatrix
43
+ from ._core import Index, ScoreMatrix, SearchParams, SubstitutionMatrix, PairedFeatureOffsets
44
44
  from ._core import gapblock_matrix as _gapblock_matrix
45
45
  from ._core import gapblock_topk_batch as _gapblock_topk_batch
46
46
  from ._core import gapblock_count_batch as _gapblock_count_batch
@@ -51,10 +51,12 @@ from ._core import gapblock_paired_topk_batch as _gapblock_paired_topk_batch
51
51
  from ._core import gapblock_paired_count_batch as _gapblock_paired_count_batch
52
52
  from ._core import gapblock_paired_sum_count_batch as _gapblock_paired_sum_count_batch
53
53
 
54
+ from ._core import gapblock_grouped_count_batch as _gapblock_grouped_count_batch, _GroupedGeometry
55
+
54
56
  __all__ = [
55
57
  "gapblock_score", "score_matrix", "ball_batch", "group_audit_batch", "topk_batch", "count_batch", "paired_topk_batch", "paired_count_batch", "paired_sum_count_batch", "deletion_variants", "central_prior", "profile_prior",
56
58
  "frame_prior", "positions_prior", "embed_in_frame", "gap_cost", "GapBlockIndex", "ScoreMatrix",
57
- "IslandProfile",
59
+ "IslandProfile", "PairedFeatureOffsets", "grouped_count_batch", "grouped_paired_sum_count_batch",
58
60
  ]
59
61
 
60
62
  #: A prior value large enough that no layout carrying it can ever win, yet small enough that
@@ -528,7 +530,7 @@ def score_matrix(
528
530
  threads: int = 0,
529
531
  position_weights_by_length=None,
530
532
  query_group_ids=None, reference_group_ids=None, group_distances=None,
531
- query_position_weights=None, reference_position_weights=None,
533
+ query_position_weights=None, reference_position_weights=None, feature_offsets=None,
532
534
  ) -> ScoreMatrix:
533
535
  """Gap-block penalty of every query against every reference, in C++ with the GIL released.
534
536
 
@@ -552,6 +554,8 @@ def score_matrix(
552
554
  reference_group_ids: Optional integer IDs, one per reference.
553
555
  group_distances: Nonempty rectangular nonnegative integer penalty table.
554
556
  Added to each pair score in the same units; no symmetry/zero diagonal imposed.
557
+ feature_offsets: Optional :class:`PairedFeatureOffsets` descriptor. Adds
558
+ paired-code costs averaged at shared depth, ties-even rounded, then scaled.
555
559
  matrix: Substitution penalties; ``None`` means unit cost.
556
560
  gap_open: Block-opening cost. Defaults to ``2 * matrix.scale()``. See the module note:
557
561
  leaving this at 1 with a real matrix makes gaps ~14x cheaper than substitutions.
@@ -587,7 +591,7 @@ def score_matrix(
587
591
  return _gapblock_matrix(q, r, alphabet, matrix, gap_open, gap_extend, cube, width, threads,
588
592
  _position_weights(position_weights_by_length, q, r),
589
593
  *_group_options(query_group_ids, reference_group_ids, group_distances, q, r),
590
- *_sequence_weights(query_position_weights, reference_position_weights, position_weights_by_length, q, r))
594
+ *_sequence_weights(query_position_weights, reference_position_weights, position_weights_by_length, q, r), feature_offsets)
591
595
 
592
596
 
593
597
  def _batch_integer(value, name, lower=0, upper=(1 << 31) - 1):
@@ -668,7 +672,11 @@ def _sequence_weights(query_weights, reference_weights, by_length, queries, refs
668
672
  return validate(query_weights, queries), validate(reference_weights, refs)
669
673
 
670
674
 
671
- def _threshold_rows(thresholds, n_queries):
675
+ def _threshold_rows(thresholds, n_queries, shared_thresholds=False):
676
+ if not isinstance(shared_thresholds, bool):
677
+ raise TypeError("shared_thresholds must be a boolean")
678
+ if shared_thresholds:
679
+ return [[_batch_integer(value, "threshold", -(1 << 31)) for value in thresholds]]
672
680
  rows = [list(row) for row in thresholds]
673
681
  if len(rows) != n_queries:
674
682
  raise ValueError("thresholds must have one row per query")
@@ -702,13 +710,15 @@ def topk_batch(queries, refs, k=10, matrix=None, gap_open=None, gap_extend=1,
702
710
  def count_batch(queries, refs, thresholds, matrix=None, gap_open=None, gap_extend=1,
703
711
  gap_prior=None, alphabet="aa", threads=0, exclude_exact=False, position_weights_by_length=None,
704
712
  query_group_ids=None, reference_group_ids=None, group_distances=None, linear_mass=False,
705
- query_position_weights=None, reference_position_weights=None):
713
+ query_position_weights=None, reference_position_weights=None, feature_offsets=None, shared_thresholds=False):
706
714
  """Count all reference scores <= each query's requested integer thresholds.
707
715
 
708
716
  Optional positional weights and additive group distances match :func:`score_matrix`.
709
717
  Exact exclusion remains full encoded identity, irrespective of group distance.
710
718
  ``thresholds`` has one row per query; rows may be empty, unsorted or contain
711
- duplicates. Return integer counts in their original order, including ALL
719
+ duplicates. With ``shared_thresholds=True``, supply one flat cutoff list;
720
+ it is validated and sorted once for the entire batch. Return integer counts
721
+ in their original order, including ALL
712
722
  score ties. Negative cutoffs count zero. Same scorer/exclusion as
713
723
  :func:`topk_batch`; one native batch with O(Q*S) output, no dense score matrix.
714
724
  Worker scratch is O(max_sequence_length + maximum_threshold_row_length).
@@ -722,20 +732,20 @@ def count_batch(queries, refs, thresholds, matrix=None, gap_open=None, gap_exten
722
732
  if not isinstance(linear_mass, bool):
723
733
  raise TypeError("linear_mass must be a boolean")
724
734
  q, r = list(queries), list(refs)
725
- thresholds = _threshold_rows(thresholds, len(q))
735
+ thresholds = _threshold_rows(thresholds, len(q), shared_thresholds)
726
736
  go, ge, cube, width, threads = _batch_options((q, r), matrix, gap_open, gap_extend, gap_prior, threads)
727
737
  reduction = _gapblock_count_mass_batch if linear_mass else _gapblock_count_batch
728
738
  return reduction(q, r, thresholds, alphabet, matrix, go, ge, cube, width, threads, exclude_exact,
729
739
  _position_weights(position_weights_by_length, q, r),
730
740
  *_group_options(query_group_ids, reference_group_ids, group_distances, q, r),
731
- *_sequence_weights(query_position_weights, reference_position_weights, position_weights_by_length, q, r))
741
+ *_sequence_weights(query_position_weights, reference_position_weights, position_weights_by_length, q, r), feature_offsets, shared_thresholds)
732
742
 
733
743
 
734
744
  def group_audit_batch(queries, refs, thresholds, matrix=None, gap_open=None, gap_extend=1,
735
745
  gap_prior=None, alphabet="aa", threads=0, exclude_exact=False,
736
746
  position_weights_by_length=None,
737
747
  query_group_ids=None, reference_group_ids=None, group_distances=None,
738
- query_position_weights=None, reference_position_weights=None):
748
+ query_position_weights=None, reference_position_weights=None, feature_offsets=None):
739
749
  """Stream component-ball count, full-score count and bounded nearest in one batch.
740
750
 
741
751
  Geometry/offset options match ball_batch. Component excludes the nonnegative
@@ -752,14 +762,14 @@ def group_audit_batch(queries, refs, thresholds, matrix=None, gap_open=None, gap
752
762
  return _gapblock_group_audit_batch(q, r, thresholds, alphabet, matrix, go, ge, cube, width,
753
763
  threads, exclude_exact, _position_weights(position_weights_by_length, q, r),
754
764
  *_group_options(query_group_ids, reference_group_ids, group_distances, q, r),
755
- *_sequence_weights(query_position_weights, reference_position_weights, position_weights_by_length, q, r))
765
+ *_sequence_weights(query_position_weights, reference_position_weights, position_weights_by_length, q, r), feature_offsets)
756
766
 
757
767
 
758
768
  def ball_batch(queries, refs, thresholds, matrix=None, gap_open=None, gap_extend=1,
759
769
  gap_prior=None, alphabet="aa", threads=0, exclude_exact=False,
760
770
  position_weights_by_length=None,
761
771
  query_group_ids=None, reference_group_ids=None, group_distances=None,
762
- query_position_weights=None, reference_position_weights=None):
772
+ query_position_weights=None, reference_position_weights=None, feature_offsets=None):
763
773
  """Exhaustive bounded ball rows, reference order, unset edit fields; no Q*N matrix.
764
774
 
765
775
  One integer cutoff per query. Negative cutoffs yield empty rows. Optional integer
@@ -776,7 +786,7 @@ def ball_batch(queries, refs, thresholds, matrix=None, gap_open=None, gap_extend
776
786
  return _gapblock_ball_batch(q, r, thresholds, alphabet, matrix, go, ge, cube, width,
777
787
  threads, exclude_exact, _position_weights(position_weights_by_length, q, r),
778
788
  *_group_options(query_group_ids, reference_group_ids, group_distances, q, r),
779
- *_sequence_weights(query_position_weights, reference_position_weights, position_weights_by_length, q, r))
789
+ *_sequence_weights(query_position_weights, reference_position_weights, position_weights_by_length, q, r), feature_offsets)
780
790
 
781
791
 
782
792
  def paired_topk_batch(queries_a, queries_b, refs_a, refs_b, k=10, matrix=None,
@@ -823,7 +833,8 @@ def paired_sum_count_batch(queries_a, queries_b, refs_a, refs_b, thresholds, mat
823
833
  reference_group_ids_alpha=None, reference_group_ids_beta=None,
824
834
  group_distances_alpha=None, group_distances_beta=None,
825
835
  query_position_weights_alpha=None, query_position_weights_beta=None,
826
- reference_position_weights_alpha=None, reference_position_weights_beta=None):
836
+ reference_position_weights_alpha=None, reference_position_weights_beta=None,
837
+ feature_offsets_alpha=None, feature_offsets_beta=None, shared_thresholds=False):
827
838
  """Return (counts, linear masses) for linked rows under summed lane distances.
828
839
 
829
840
  Shared matrix/gap costs/prior and separate full-coordinate lane weights and
@@ -833,12 +844,13 @@ def paired_sum_count_batch(queries_a, queries_b, refs_a, refs_b, thresholds, mat
833
844
  duplicate reference observations and input threshold order. Exact exclusion
834
845
  removes a row only when both encoded full strings match. One GIL-released
835
846
  native traversal, O(Q*S) outputs and O(max_length + max_threshold_row) worker
836
- scratch; no matrix/edges. The existing paired-max API is unchanged.
847
+ scratch; no matrix/edges. ``shared_thresholds=True`` accepts one flat cutoff
848
+ list validated and sorted once for all queries. The paired-max API is unchanged.
837
849
  """
838
850
  qa, qb, ra, rb = map(list, (queries_a, queries_b, refs_a, refs_b))
839
851
  if len(qa) != len(qb) or len(ra) != len(rb):
840
852
  raise ValueError("paired query/reference axes must agree")
841
- thresholds = _threshold_rows(thresholds, len(qa))
853
+ thresholds = _threshold_rows(thresholds, len(qa), shared_thresholds)
842
854
  go, ge, cube, width, threads = _batch_options((qa, qb, ra, rb), matrix, gap_open, gap_extend, gap_prior, threads)
843
855
  ag = _group_options(query_group_ids_alpha, reference_group_ids_alpha, group_distances_alpha, qa, ra)
844
856
  bg = _group_options(query_group_ids_beta, reference_group_ids_beta, group_distances_beta, qb, rb)
@@ -849,7 +861,226 @@ def paired_sum_count_batch(queries_a, queries_b, refs_a, refs_b, thresholds, mat
849
861
  _position_weights(position_weights_by_length_alpha, qa, ra),
850
862
  _position_weights(position_weights_by_length_beta, qb, rb),
851
863
  ag[0], bg[0], ag[1], bg[1], ag[2], bg[2],
852
- aw[0], bw[0], aw[1], bw[1])
864
+ aw[0], bw[0], aw[1], bw[1], feature_offsets_alpha, feature_offsets_beta, shared_thresholds)
865
+
866
+
867
+ def _grouped_metadata(q, r, thresholds, reference_target_offsets, reference_target_ids,
868
+ target_peptide_ids, reference_exclusive_peptide_ids,
869
+ query_gene_ids, reference_gene_ids, chunk_size):
870
+ cuts = [_batch_integer(v, "threshold", -(1 << 31)) for v in thresholds]
871
+ offsets = [_batch_integer(v, "target offset", upper=(1 << 32)-1) for v in reference_target_offsets]
872
+ labels = [_batch_integer(v, "target ID", upper=(1 << 32)-1) for v in reference_target_ids]
873
+ peptides = [_batch_integer(v, "peptide ID") for v in target_peptide_ids]
874
+ exclusive = [_batch_integer(v, "exclusive peptide ID", -1) for v in reference_exclusive_peptide_ids]
875
+ qgenes = [_batch_integer(v, "query gene ID") for v in query_gene_ids]
876
+ rgenes = [_batch_integer(v, "reference gene ID") for v in reference_gene_ids]
877
+ chunk_size = _batch_integer(chunk_size, "chunk_size", 1)
878
+ return cuts, offsets, labels, peptides, exclusive, qgenes, rgenes, chunk_size
879
+
880
+
881
+ def _withheld_metadata(q, query_withheld_peptide_ids, reference_peptide_offsets, reference_peptide_ids):
882
+ if query_withheld_peptide_ids is None:
883
+ if reference_peptide_offsets is not None or reference_peptide_ids is not None:
884
+ raise ValueError("reference peptide memberships require query withheld peptide IDs")
885
+ return None, [], []
886
+ if reference_peptide_offsets is None or reference_peptide_ids is None:
887
+ raise ValueError("withheld peptide IDs require full reference peptide CSR")
888
+ withheld = [_batch_integer(v, "withheld peptide ID", -1) for v in query_withheld_peptide_ids]
889
+ offsets = [_batch_integer(v, "peptide offset", upper=(1 << 32)-1) for v in reference_peptide_offsets]
890
+ peptides = [_batch_integer(v, "reference peptide ID") for v in reference_peptide_ids]
891
+ if len(withheld) != len(q):
892
+ raise ValueError("withheld peptide IDs must match query axis")
893
+ return withheld, offsets, peptides
894
+
895
+
896
+ def _additional_geometry(geometry, features):
897
+ if features is None:
898
+ return
899
+ terms = list(features)
900
+ if any(not isinstance(term, PairedFeatureOffsets) for term in terms):
901
+ raise ValueError("additional_feature_offsets must contain PairedFeatureOffsets")
902
+ if terms:
903
+ if not hasattr(geometry, "additional_features"):
904
+ raise RuntimeError("additional factored offsets require native additional_features support")
905
+ geometry.additional_features = terms
906
+
907
+
908
+ def _lane_prior(geometry, prior, width):
909
+ if prior is not None:
910
+ if not hasattr(geometry, "gap_prior"):
911
+ raise RuntimeError("lane priors require native grouped gap_prior support")
912
+ geometry.gap_prior = _prior_cube(prior, width)
913
+
914
+
915
+ def _alignment_options(geometry, q, r, mode, scale, left, right, matrix, prior,
916
+ *weights):
917
+ if mode not in ("gapblock", "global", "local"):
918
+ raise ValueError("alignment_mode must be gapblock, global or local")
919
+ scale = _batch_integer(scale, "alignment scale", 1)
920
+ left = _batch_integer(left, "trim_left")
921
+ right = _batch_integer(right, "trim_right")
922
+ if mode == "gapblock":
923
+ if scale != 1 or left or right:
924
+ raise ValueError("alignment scale/trims require global or local mode")
925
+ return
926
+ if matrix is None:
927
+ raise ValueError("affine alignment requires a similarity matrix")
928
+ if prior is not None or any(w is not None for w in weights):
929
+ raise ValueError("affine alignment does not accept position weights or gap prior")
930
+ if any(left + right > len(sequence) for axis in (q, r) for sequence in axis):
931
+ raise ValueError("alignment trims exceed sequence length")
932
+ if not hasattr(geometry, "alignment_mode"):
933
+ raise RuntimeError("affine grouped scoring requires native alignment_mode support")
934
+ geometry.alignment_mode = mode
935
+ geometry.alignment_scale = scale
936
+ geometry.trim_left = left
937
+ geometry.trim_right = right
938
+
939
+
940
+ def _grouped_geometry(q, r, weights, query_groups, reference_groups, distances,
941
+ query_weights, reference_weights, features, score_quantum=1):
942
+ quantum = _batch_integer(score_quantum, "score quantum", 1)
943
+ geometry = _GroupedGeometry()
944
+ if quantum != 1:
945
+ if not hasattr(geometry, "score_quantum"):
946
+ raise RuntimeError("alignment quantization requires native score_quantum support")
947
+ geometry.score_quantum = quantum
948
+ geometry.weights = _position_weights(weights, q, r)
949
+ geometry.query_groups, geometry.reference_groups, geometry.group_distances = _group_options(
950
+ query_groups, reference_groups, distances, q, r)
951
+ geometry.query_weights, geometry.reference_weights = _sequence_weights(
952
+ query_weights, reference_weights, weights, q, r)
953
+ geometry.features = features
954
+ return geometry
955
+
956
+
957
+ def grouped_count_batch(queries, refs, thresholds, *, reference_target_offsets,
958
+ reference_target_ids, target_peptide_ids, reference_exclusive_peptide_ids,
959
+ query_gene_ids, reference_gene_ids, chunk_size=64, matrix=None, gap_open=None,
960
+ gap_extend=1, gap_prior=None, alphabet="aa", threads=0, exclude_exact=False,
961
+ position_weights_by_length=None, query_group_ids=None, reference_group_ids=None,
962
+ group_distances=None, query_position_weights=None, reference_position_weights=None,
963
+ feature_offsets=None, query_withheld_peptide_ids=None,
964
+ reference_peptide_offsets=None, reference_peptide_ids=None, score_quantum=1,
965
+ alignment_mode="gapblock", alignment_scale=1, trim_left=0, trim_right=0,
966
+ additional_feature_offsets=None):
967
+ """Yield bounded label curves from one immutable prepared scoring population.
968
+
969
+ ``alignment_mode`` defaults to gapblock. Global/local reuse the existing affine
970
+ similarity recurrence and require a raw similarity matrix. For trimmed sequences
971
+ q/r, junction distance is alignment_scale * max(0, sum(diag(q)) + sum(diag(r))
972
+ - 2*similarity(q,r)), using raw diagonal sums rather than self-alignment scores.
973
+ Nonnegative trim_left/trim_right remove residues for scoring only; exact
974
+ exclusion and offsets retain full input identities. Gap costs are raw similarity
975
+ magnitudes in these modes. Position weights and gap_prior are rejected.
976
+ Paired lanes compute distances independently before summation. Scratch rows are
977
+ reused per worker; no dense query/reference score matrix is produced.
978
+
979
+ ``additional_feature_offsets`` is an optional iterable of PairedFeatureOffsets.
980
+ Every term is independently ties-even rounded/scaled, then added to the primary
981
+ feature and group offsets. All descriptors are snapshotted/validated once.
982
+ Combined score/offset bounds must fit int32 per lane. In the paired API, use
983
+ additional_feature_offsets_alpha/beta; gap_prior_alpha/beta independently
984
+ override the shared gap_prior. None inherits the shared prior for that lane.
985
+
986
+ ``score_quantum`` is a positive int32. Floor the raw alignment score to a
987
+ multiple of this quantum before adding group/feature offsets. Default1 leaves
988
+ every score unchanged. Paired lanes floor independently before their sum.
989
+ This option applies only to grouped APIs.
990
+
991
+ Cutoffs are one strictly increasing flat integer list. References are a caller's
992
+ deduplicated search-key axis; CSR offsets/IDs assign each key to zero or more
993
+ target IDs. Per-reference exclusive peptide IDs come from the full label set,
994
+ with -1 for shared/unselected labels; target_peptide_ids maps each target.
995
+ Canonical gene IDs define the auxiliary same-gene subset independently of
996
+ allele costs. Channels are whole, targets, same-gene targets, exclusive peptides.
997
+ Each chunk owns read-only uint32 buffers: counts[C,1+2*T+P,K], sizes[C,1+2*T+P],
998
+ exact_counts[C,T], with start/stop query offsets. numpy.asarray is zero-copy;
999
+ no NumPy import/dependency is required. Exact counts retain raw multiplicity;
1000
+ exclude_exact punctures every curve and population size by full sequence identity.
1001
+ Optional query_withheld_peptide_ids is one ID/query (-1 means none) and
1002
+ requires full reference_peptide_offsets/reference_peptide_ids CSR. Each sorted
1003
+ unique row includes all raw peptide labels, including sparse/unselected IDs in
1004
+ the target_peptide_ids namespace. Extra IDs beyond output peptide channels are
1005
+ valid. Selected target parents and static exclusives must agree with full CSR.
1006
+ Withholding removes every membership of that peptide, drops keys with no
1007
+ survivors, and recomputes exclusive surviving peptides before exact exclusion.
1008
+ Raw exact counts include only surviving targets. Full CSR without query IDs is
1009
+ rejected; None preserves the existing traversal. Encoding/geometry validation
1010
+ occurs once; each query/reference is scored once
1011
+ under the existing kernel, with GIL-released bounded workers. Memory is bounded
1012
+ by chunk_size, labels and cutoffs, never Q*labels. Retained chunks own their data.
1013
+ """
1014
+ q, r = list(queries), list(refs)
1015
+ metadata = _grouped_metadata(q, r, thresholds, reference_target_offsets,
1016
+ reference_target_ids, target_peptide_ids, reference_exclusive_peptide_ids,
1017
+ query_gene_ids, reference_gene_ids, chunk_size)
1018
+ go, ge, cube, width, threads = _batch_options((q, r), matrix, gap_open, gap_extend, gap_prior, threads)
1019
+ geometry = _grouped_geometry(q, r, position_weights_by_length, query_group_ids,
1020
+ reference_group_ids, group_distances, query_position_weights, reference_position_weights, feature_offsets, score_quantum)
1021
+ _additional_geometry(geometry, additional_feature_offsets)
1022
+ _alignment_options(geometry, q, r, alignment_mode, alignment_scale, trim_left, trim_right,
1023
+ matrix, gap_prior, position_weights_by_length, query_position_weights, reference_position_weights)
1024
+ withheld = _withheld_metadata(q, query_withheld_peptide_ids,
1025
+ reference_peptide_offsets, reference_peptide_ids)
1026
+ return _gapblock_grouped_count_batch(q, r, *metadata, alphabet, matrix, go, ge, cube, width,
1027
+ threads, exclude_exact, geometry,
1028
+ query_withheld_peptide_ids=withheld[0],
1029
+ reference_peptide_offsets=withheld[1],reference_peptide_ids=withheld[2])
1030
+
1031
+
1032
+ def grouped_paired_sum_count_batch(queries_a, queries_b, refs_a, refs_b, thresholds, *,
1033
+ reference_target_offsets, reference_target_ids, target_peptide_ids,
1034
+ reference_exclusive_peptide_ids, query_gene_ids, reference_gene_ids,
1035
+ chunk_size=64, matrix=None, gap_open=None, gap_extend=1, gap_prior=None,
1036
+ alphabet="aa", threads=0, exclude_exact=False,
1037
+ position_weights_by_length_alpha=None, position_weights_by_length_beta=None,
1038
+ query_group_ids_alpha=None, query_group_ids_beta=None,
1039
+ reference_group_ids_alpha=None, reference_group_ids_beta=None,
1040
+ group_distances_alpha=None, group_distances_beta=None,
1041
+ query_position_weights_alpha=None, query_position_weights_beta=None,
1042
+ reference_position_weights_alpha=None, reference_position_weights_beta=None,
1043
+ feature_offsets_alpha=None, feature_offsets_beta=None,
1044
+ query_withheld_peptide_ids=None, reference_peptide_offsets=None, reference_peptide_ids=None, score_quantum_alpha=1, score_quantum_beta=1,
1045
+ alignment_mode="gapblock", alignment_scale=1, trim_left=0, trim_right=0,
1046
+ additional_feature_offsets_alpha=None, additional_feature_offsets_beta=None,
1047
+ gap_prior_alpha=None, gap_prior_beta=None):
1048
+ """Grouped buffers under existing linked sum geometry; see grouped_count_batch.
1049
+
1050
+ Composite full-key exclusion requires both lanes equal. Canonical paired gene
1051
+ identities are caller-encoded tuple IDs, with equality requiring both genes.
1052
+ Shared matrix/prior and lane-specific weights/group/feature offsets retain the
1053
+ paired_sum_count_batch geometry. Counts, sizes and raw exact counts use uint32.
1054
+ """
1055
+ qa, qb, ra, rb = map(list, (queries_a, queries_b, refs_a, refs_b))
1056
+ if len(qa) != len(qb) or len(ra) != len(rb):
1057
+ raise ValueError("paired query/reference axes must agree")
1058
+ metadata = _grouped_metadata(qa, ra, thresholds, reference_target_offsets,
1059
+ reference_target_ids, target_peptide_ids, reference_exclusive_peptide_ids,
1060
+ query_gene_ids, reference_gene_ids, chunk_size)
1061
+ go, ge, cube, width, threads = _batch_options((qa, qb, ra, rb), matrix, gap_open, gap_extend, gap_prior, threads)
1062
+ alpha = _grouped_geometry(qa, ra, position_weights_by_length_alpha, query_group_ids_alpha,
1063
+ reference_group_ids_alpha, group_distances_alpha, query_position_weights_alpha,
1064
+ reference_position_weights_alpha, feature_offsets_alpha, score_quantum_alpha)
1065
+ beta = _grouped_geometry(qb, rb, position_weights_by_length_beta, query_group_ids_beta,
1066
+ reference_group_ids_beta, group_distances_beta, query_position_weights_beta,
1067
+ reference_position_weights_beta, feature_offsets_beta, score_quantum_beta)
1068
+ _additional_geometry(alpha, additional_feature_offsets_alpha)
1069
+ _additional_geometry(beta, additional_feature_offsets_beta)
1070
+ _lane_prior(alpha, gap_prior_alpha, width)
1071
+ _lane_prior(beta, gap_prior_beta, width)
1072
+ _alignment_options(alpha, qa, ra, alignment_mode, alignment_scale, trim_left, trim_right,
1073
+ matrix, gap_prior if gap_prior_alpha is None else gap_prior_alpha, position_weights_by_length_alpha, query_position_weights_alpha,
1074
+ reference_position_weights_alpha)
1075
+ _alignment_options(beta, qb, rb, alignment_mode, alignment_scale, trim_left, trim_right,
1076
+ matrix, gap_prior if gap_prior_beta is None else gap_prior_beta, position_weights_by_length_beta, query_position_weights_beta,
1077
+ reference_position_weights_beta)
1078
+ withheld = _withheld_metadata(qa, query_withheld_peptide_ids,
1079
+ reference_peptide_offsets, reference_peptide_ids)
1080
+ return _gapblock_grouped_count_batch(qa, ra, *metadata, alphabet, matrix, go, ge, cube, width,
1081
+ threads, exclude_exact, alpha, beta, qb, rb,
1082
+ query_withheld_peptide_ids=withheld[0],
1083
+ reference_peptide_offsets=withheld[1],reference_peptide_ids=withheld[2])
853
1084
 
854
1085
 
855
1086
  class GapBlockIndex: