Skip to content

indel search: walk each side of a seed once, not once per budget split - #36

Merged
dmx2 merged 3 commits into
IEDB:masterfrom
Roman-Young:perf/indel-dfs
Oct 15, 2026
Merged

dmx2 merged 3 commits into
IEDB:masterfrom
Roman-Young:perf/indel-dfs

Conversation

@Roman-Young

Copy link
Copy Markdown
Contributor

Two-indel search spent almost all its time extending seeds. A 3-mer seed shows up about 3,700 times in human, and at every one of those spots extend_bidirectional ran dfs once for every way of splitting the indel budget between the two sides of the seed, with a new Vec allocated at every dfs node, even at dead ends:

                 dfs walks per candidate position
                 1 indel    2 indels
before              8          12
this PR             4           4    (2 when the right side finds nothing, which is most positions)

Now each side is walked once per pass at the full budget, and every path records how many edits it used. A budget split just takes the paths that fit. The budget only decides whether an edit branch is open, so those are exactly the paths a smaller budget walk returns, in the same order, and the windows come out in the same order too. On top of that the left walk is skipped when the right one finds nothing, each (consumed, edits) path end is kept once (within a pass the protein consumed follows from the edit count), and windows are (start, end) positions instead of copied sequences.

Engine instructions (callgrind) drop 2.4x for 1 indel and 3.3 to 4.2x for 2 indels, including 2-mer seeds and repeat tracts. In wall time a 2-indel search of 20k synthetic queries goes from 30.0 s to 13.9 s, and the 1-indel engine on 50k queries from 3.3 s to 2.3 s.

Output is byte-identical, not just the same matches:

  • the old and new extension code side by side on every case over a 2- and 3-letter alphabet up to 7-residue queries and 10-residue proteins with 0 to 3 indels (608M calls), plus 30M random repeat-rich cases
  • rs_indel_match output identical on 123 jobs over the human proteome (k from 2 to 15, 0 to 3 indels, the benchmark files, repeat tracts, protein ends), single-threaded and not
  • all 11 indel benchmark sets: 100% recall, output identical to master

New test_indel_golden.py (in the CI matrix) checks indel output row for row, every column and the row order. The property tests compare sets of matches, so an engine that reorders rows or merges hits on two proteins at the same coordinates passes all of them; both fail this one.

Also gets rid of clippy's too_many_arguments warning on dfs.

… split

extend_bidirectional ran dfs for every split of the indel budget between the two
sides of a seed: 2 passes x (n+1) splits x 2 directions, so 12 walks per candidate
position with 2 indels and 8 with 1, and every node of every walk allocated a Vec.
dfs now walks once at the full budget and records (protein chars consumed, edits
used) per path into a reused list; each split takes the entries with at most its
share of edits. The budget only decides whether an edit branch is open, so those
entries are exactly what a walk at the smaller budget returns, in the same order,
and the pairing loops emit the same windows in the same order. Windows are (start,
end) positions instead of copied sequences, deduped against a short list, and
indel_search_peptide dedups on (protein, start, end) and builds the matched string
only for hits it keeps.

Output is byte-identical to before. The old and new extension code agree side by
side on every case over a 2- and 3-letter alphabet up to query 7 and protein 10
(608M calls) and on 30M random repeat-rich cases; rs_indel_match output is identical
on 123 jobs over the human proteome (every cached k, 0 to 3 indels, the benchmark
files, repeat tracts, protein ends, at 1 thread and all); the 29-scenario human
comparison and all 11 indel benchmark sets match master with 100% recall. 2-indel
synthetic search: 13.3 s -> 7.1 s.
…list each path end once

Two exact shortcuts on top of the single walk. Most candidate seed positions fail on
the right side, and with no right path there is nothing to pair, so the left walk
is skipped. And within one pass the protein consumed follows from the edit count
(one fewer per deletion, one more per insertion), so a later path ending in the same
(consumed, edits) could only re-emit windows its first copy already emitted; each
pair is now listed once, which keeps repeat-rich proteins from multiplying the
pairing loops.

Output is byte-identical: the same side-by-side check against the old extension code
(608M exhaustive and 30M random cases), identical rs_indel_match output on all 123
human-proteome jobs at 1 thread and all, the 29-scenario comparison and all 11 indel
benchmark sets identical to master with 100% recall.
The brute-force property tests compare sets of matches, so they cannot see a change
that reorders, duplicates or relabels rows without changing the set. This records
indel search output row for row (every column, row order) for 53 queries against
twelve proteins built around repeat tracts, two of them isoform-like so hits on both
share coordinates: exact windows, one and two edits of each kind, windows inside
homopolymer and short repeats and at protein ends, a motif that occurs twice, a mixed
edit that is never found, short peptides and decoys. Four runs: 1 and 2 indels, with
the derived k and with k=2. Recorded from the engine before the single-walk change.

An engine that pairs the budget splits in another order (same rows, new order) or
dedups hits without the protein passes the whole existing suite; both fail this test,
and the last six queries were chosen for that. The file has its CI matrix row.
@dmx2
dmx2 merged commit 44007cf into IEDB:master Oct 15, 2026
11 checks passed
@dmx2

dmx2 commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

Merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants