seqtree 1.3.0__tar.gz → 1.3.2__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.3.0 → seqtree-1.3.2}/CHANGELOG.md +9 -0
  2. {seqtree-1.3.0 → seqtree-1.3.2}/PKG-INFO +1 -1
  3. {seqtree-1.3.0 → seqtree-1.3.2}/pyproject.toml +1 -1
  4. {seqtree-1.3.0 → seqtree-1.3.2}/python/seqtree/control.py +46 -17
  5. {seqtree-1.3.0 → seqtree-1.3.2}/python/seqtree/gapblock.py +20 -14
  6. {seqtree-1.3.0 → seqtree-1.3.2}/.gitattributes +0 -0
  7. {seqtree-1.3.0 → seqtree-1.3.2}/.gitignore +0 -0
  8. {seqtree-1.3.0 → seqtree-1.3.2}/CMakeLists.txt +0 -0
  9. {seqtree-1.3.0 → seqtree-1.3.2}/LICENSE +0 -0
  10. {seqtree-1.3.0 → seqtree-1.3.2}/README.md +0 -0
  11. {seqtree-1.3.0 → seqtree-1.3.2}/RELEASE_AUDIT.md +0 -0
  12. {seqtree-1.3.0 → seqtree-1.3.2}/include/seqtree/kmer_index.hpp +0 -0
  13. {seqtree-1.3.0 → seqtree-1.3.2}/include/seqtree/parallel.hpp +0 -0
  14. {seqtree-1.3.0 → seqtree-1.3.2}/include/seqtree/seqtree.hpp +0 -0
  15. {seqtree-1.3.0 → seqtree-1.3.2}/include/seqtree/text_index.hpp +0 -0
  16. {seqtree-1.3.0 → seqtree-1.3.2}/include/seqtree/types.hpp +0 -0
  17. {seqtree-1.3.0 → seqtree-1.3.2}/python/seqtree/__init__.py +0 -0
  18. {seqtree-1.3.0 → seqtree-1.3.2}/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
  19. {seqtree-1.3.0 → seqtree-1.3.2}/python/seqtree/distance.py +0 -0
  20. {seqtree-1.3.0 → seqtree-1.3.2}/python/seqtree/evalue.py +0 -0
  21. {seqtree-1.3.0 → seqtree-1.3.2}/python/seqtree/layout.py +0 -0
  22. {seqtree-1.3.0 → seqtree-1.3.2}/python/seqtree/pairwise.py +0 -0
  23. {seqtree-1.3.0 → seqtree-1.3.2}/python/seqtree/pmhc.py +0 -0
  24. {seqtree-1.3.0 → seqtree-1.3.2}/python/seqtree/pmhc_evalue.py +0 -0
  25. {seqtree-1.3.0 → seqtree-1.3.2}/python/seqtree/py.typed +0 -0
  26. {seqtree-1.3.0 → seqtree-1.3.2}/python/seqtree/seeds.py +0 -0
  27. {seqtree-1.3.0 → seqtree-1.3.2}/python/seqtree/text_evalue.py +0 -0
  28. {seqtree-1.3.0 → seqtree-1.3.2}/src/_bindings.cpp +0 -0
  29. {seqtree-1.3.0 → seqtree-1.3.2}/src/alignment_max.cpp +0 -0
  30. {seqtree-1.3.0 → seqtree-1.3.2}/src/atomic_write.hpp +0 -0
  31. {seqtree-1.3.0 → seqtree-1.3.2}/src/blosum45.inc +0 -0
  32. {seqtree-1.3.0 → seqtree-1.3.2}/src/blosum62.inc +0 -0
  33. {seqtree-1.3.0 → seqtree-1.3.2}/src/blosum80.inc +0 -0
  34. {seqtree-1.3.0 → seqtree-1.3.2}/src/codec.cpp +0 -0
  35. {seqtree-1.3.0 → seqtree-1.3.2}/src/count_unit.hpp +0 -0
  36. {seqtree-1.3.0 → seqtree-1.3.2}/src/distance.cpp +0 -0
  37. {seqtree-1.3.0 → seqtree-1.3.2}/src/engine_seqtm.cpp +0 -0
  38. {seqtree-1.3.0 → seqtree-1.3.2}/src/engine_seqtrie.cpp +0 -0
  39. {seqtree-1.3.0 → seqtree-1.3.2}/src/engines.hpp +0 -0
  40. {seqtree-1.3.0 → seqtree-1.3.2}/src/gapblock.cpp +0 -0
  41. {seqtree-1.3.0 → seqtree-1.3.2}/src/index.cpp +0 -0
  42. {seqtree-1.3.0 → seqtree-1.3.2}/src/kmer_index.cpp +0 -0
  43. {seqtree-1.3.0 → seqtree-1.3.2}/src/pairwise.cpp +0 -0
  44. {seqtree-1.3.0 → seqtree-1.3.2}/src/pairwise_detail.hpp +0 -0
  45. {seqtree-1.3.0 → seqtree-1.3.2}/src/pam100.inc +0 -0
  46. {seqtree-1.3.0 → seqtree-1.3.2}/src/pam250.inc +0 -0
  47. {seqtree-1.3.0 → seqtree-1.3.2}/src/positional_matrix.cpp +0 -0
  48. {seqtree-1.3.0 → seqtree-1.3.2}/src/searcher.cpp +0 -0
  49. {seqtree-1.3.0 → seqtree-1.3.2}/src/structural.inc +0 -0
  50. {seqtree-1.3.0 → seqtree-1.3.2}/src/substitution_matrix.cpp +0 -0
  51. {seqtree-1.3.0 → seqtree-1.3.2}/src/text_index.cpp +0 -0
  52. {seqtree-1.3.0 → seqtree-1.3.2}/src/trie.cpp +0 -0
  53. {seqtree-1.3.0 → seqtree-1.3.2}/src/trie.hpp +0 -0
@@ -3,6 +3,15 @@
3
3
  All notable changes to `seqtree`. Dates are release dates. From 1.0.0 the project follows semantic
4
4
  versioning: breaking changes need a **major** bump.
5
5
 
6
+ ## [1.3.1] — 2026-10-19
7
+
8
+ ### Changed
9
+
10
+ - Gap-prior lookup preparation evaluates pure callbacks only for input lengths,
11
+ avoiding cubic callback work for absent lengths while preserving exact scores,
12
+ count reductions and lane-specific priors. Equal-length/empty axes do not
13
+ evaluate unused priors. The dense native lookup layout is unchanged.
14
+
6
15
  ## [1.3.0] — 2026-10-19
7
16
 
8
17
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: seqtree
3
- Version: 1.3.0
3
+ Version: 1.3.2
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>
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "seqtree"
7
- version = "1.3.0"
7
+ version = "1.3.2"
8
8
  description = "Fast fuzzy search over biological sequences (C++ core, Python bindings)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -122,24 +122,53 @@ def _prune_superseded(cache_dir, name, size, keep):
122
122
  pass
123
123
 
124
124
 
125
- def _build_lock(cache):
126
- """An inter-process lock around build-and-save, if ``filelock`` happens to be installed.
127
-
128
- This is an *optimisation*, not the fix. ``Index.save`` writes to a temporary and renames it
129
- into place, so a reader can never observe a half-written index whether or not this lock is
130
- taken. What the lock buys is work: without it, a cold-cache fan-out of N workers has every
131
- worker build the same 250k-clonotype index (seconds of CPU, 45 MB written) and N-1 of those
132
- are thrown away. With it, one builds and the rest wait and load.
133
-
134
- seqtree has no runtime dependencies, so ``filelock`` is used only if something else already
135
- put it there -- ``huggingface_hub`` pulls it in, and that is installed whenever a control is
136
- downloaded rather than read from the bundled asset.
125
+ @contextlib.contextmanager
126
+ def _build_lock(cache, timeout=600):
127
+ """Serialize cold builds without optional dependencies; process death releases the lock.
128
+
129
+ Keep the guard file: deleting it could let waiters lock different inodes.
137
130
  """
138
- try:
139
- from filelock import FileLock
140
- except ImportError:
141
- return contextlib.nullcontext()
142
- return FileLock(cache + ".lock", timeout=600)
131
+ import errno
132
+ import time
133
+
134
+ with open(cache + ".lock", "a+b") as handle:
135
+ if os.name == "nt":
136
+ import msvcrt
137
+ if handle.seek(0, 2) == 0:
138
+ handle.write(b"\0")
139
+ handle.flush()
140
+
141
+ def acquire():
142
+ handle.seek(0)
143
+ msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
144
+
145
+ def release():
146
+ handle.seek(0)
147
+ msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
148
+ else:
149
+ import fcntl
150
+
151
+ def acquire():
152
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
153
+
154
+ def release():
155
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
156
+
157
+ deadline = time.monotonic() + timeout
158
+ while True:
159
+ try:
160
+ acquire()
161
+ break
162
+ except OSError as exc:
163
+ if exc.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
164
+ raise
165
+ if time.monotonic() >= deadline:
166
+ raise TimeoutError(f"Another control build owns {cache}.lock after {timeout:g}s") from exc
167
+ time.sleep(0.05)
168
+ try:
169
+ yield
170
+ finally:
171
+ release()
143
172
 
144
173
 
145
174
  def _load_cached(cache):
@@ -499,19 +499,22 @@ def gapblock_score(
499
499
  return best + gap_cost(d, gap_open, gap_extend), best_i
500
500
 
501
501
 
502
- def _prior_cube(prior: GapPrior, width: int) -> list[int]:
502
+ def _prior_cube(prior: GapPrior, width: int, lengths=None) -> list[int]:
503
503
  """Flatten ``prior`` to ``[m][d][i]`` with stride ``width + 1``, for the C++ kernel.
504
504
 
505
505
  Only reachable cells are filled: ``d >= 1`` (the prior never applies to equal lengths) and
506
506
  ``i <= m - d`` (the block start indexes the shorter sequence). The rest stay zero and are
507
- never read.
507
+ never read. Input lengths restrict callback evaluation to possible full-length pairs;
508
+ the dense native layout is unchanged. None retains the full-width internal oracle.
508
509
  """
509
510
  w1 = width + 1
510
511
  cube = [0] * (w1 * w1 * w1)
511
- for m in range(w1):
512
- for d in range(1, m + 1):
512
+ lengths = range(w1) if lengths is None else sorted(set(lengths))
513
+ for row, m in enumerate(lengths):
514
+ for n in reversed(lengths[:row]):
515
+ d = m - n
513
516
  base = (m * w1 + d) * w1
514
- for i in range(m - d + 1):
517
+ for i in range(n + 1):
515
518
  v = int(prior(i, d, m))
516
519
  if v < 0:
517
520
  raise ValueError(f"gap_prior({i}, {d}, {m}) = {v} is negative")
@@ -561,7 +564,8 @@ def score_matrix(
561
564
  leaving this at 1 with a real matrix makes gaps ~14x cheaper than substitutions.
562
565
  gap_extend: Cost per additional gap column.
563
566
  gap_prior: :data:`GapPrior`, materialized once into a lookup cube and then read from C++.
564
- ``None`` lets the score alone choose the block position.
567
+ Evaluated only for unequal input lengths and valid block starts; it must be
568
+ a pure nonnegative function. ``None`` lets the score alone choose the block position.
565
569
  alphabet: ``"aa"``, ``"nt"``, or ``"iupac"``. Symbols outside it raise.
566
570
  threads: Worker threads; ``0`` means one per core. Rows are disjoint, so this scales.
567
571
 
@@ -586,8 +590,9 @@ def score_matrix(
586
590
  gap_open = _default_gap_open(matrix)
587
591
  if gap_open < 0 or gap_extend < 0:
588
592
  raise ValueError("gap_open and gap_extend must be >= 0")
589
- width = max((len(s) for s in q + r), default=0)
590
- cube = _prior_cube(gap_prior, width) if gap_prior is not None else []
593
+ lengths = {len(s) for s in q + r}
594
+ width = max(lengths, default=0)
595
+ cube = _prior_cube(gap_prior, width, lengths) if gap_prior is not None else []
591
596
  return _gapblock_matrix(q, r, alphabet, matrix, gap_open, gap_extend, cube, width, threads,
592
597
  _position_weights(position_weights_by_length, q, r),
593
598
  *_group_options(query_group_ids, reference_group_ids, group_distances, q, r),
@@ -611,8 +616,9 @@ def _batch_options(axes, matrix, gap_open, gap_extend, gap_prior, threads):
611
616
  go = _batch_integer(_default_gap_open(matrix) if gap_open is None else gap_open, "gap_open")
612
617
  ge = _batch_integer(gap_extend, "gap_extend")
613
618
  threads = _batch_integer(threads, "threads")
614
- width = max((len(s) for axis in axes for s in axis), default=0)
615
- cube = _prior_cube(gap_prior, width) if gap_prior is not None else []
619
+ lengths = {len(s) for axis in axes for s in axis}
620
+ width = max(lengths, default=0)
621
+ cube = _prior_cube(gap_prior, width, lengths) if gap_prior is not None else []
616
622
  return go, ge, cube, width, threads
617
623
 
618
624
 
@@ -915,11 +921,11 @@ def _additional_geometry(geometry, features):
915
921
  geometry.additional_features = terms
916
922
 
917
923
 
918
- def _lane_prior(geometry, prior, width):
924
+ def _lane_prior(geometry, prior, width, queries, refs):
919
925
  if prior is not None:
920
926
  if not hasattr(geometry, "gap_prior"):
921
927
  raise RuntimeError("lane priors require native grouped gap_prior support")
922
- geometry.gap_prior = _prior_cube(prior, width)
928
+ geometry.gap_prior = _prior_cube(prior, width, {len(s) for s in queries + refs})
923
929
 
924
930
 
925
931
  def _alignment_options(geometry, q, r, mode, scale, left, right, matrix, prior,
@@ -1099,8 +1105,8 @@ def grouped_paired_sum_count_batch(queries_a, queries_b, refs_a, refs_b, thresho
1099
1105
  reference_position_weights_beta, feature_offsets_beta, score_quantum_beta)
1100
1106
  _additional_geometry(alpha, additional_feature_offsets_alpha)
1101
1107
  _additional_geometry(beta, additional_feature_offsets_beta)
1102
- _lane_prior(alpha, gap_prior_alpha, width)
1103
- _lane_prior(beta, gap_prior_beta, width)
1108
+ _lane_prior(alpha, gap_prior_alpha, width, qa, ra)
1109
+ _lane_prior(beta, gap_prior_beta, width, qb, rb)
1104
1110
  _alignment_options(alpha, qa, ra, alignment_mode, alignment_scale, trim_left, trim_right,
1105
1111
  matrix, gap_prior if gap_prior_alpha is None else gap_prior_alpha, position_weights_by_length_alpha, query_position_weights_alpha,
1106
1112
  reference_position_weights_alpha)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes