Skip to content

ADD pam_only to the compiled core, so a dropped match never reaches Python - #18

Merged
emattei merged 1 commit into
mainfrom
feat/core-pam-only
Nov 9, 2026
Merged

emattei merged 1 commit into
mainfrom
feat/core-pam-only

Conversation

@emattei

@emattei emattei commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #17. --pam-only dropped the matches with no PAM, but the Rust
scan had already pushed every one of them into a Vec that crosses into
Python as a list, so the peak was set at that boundary whatever Python did
afterwards -- on 500 9-mers against chr21, 1,056 MB full and 910 MB filtered.

scan_fasta now takes pam_only and skips the hit instead of pushing it.
It returns, beside the matches, one (found, off_end, no_pam) per spacer, so
n_matches, n_off_contig_end, n_no_pam, n_sites and the per-guide match
count come from what the scan saw rather than from what it handed back. Python
no longer tallies hits at all, which removes the chance of the two drifting.

500 9-mers against chr21: 1,056 MB to 538 MB, with
guide_genome_annotation.tsv and qc.tsv byte-identical to the Python-side
filter.

matches_pam agreed with none of this. Its code has no value for a
genomic N, so it rejected one even at an N of the pattern -- where
base_matches accepts it by design, and the pam_matches column always has.
NGG asks nothing of its first position, so an unsequenced base there is still
a PAM; any other code names bases, and an N is not known to be one of them.
Filtering on the core's own verdict would have dropped rows the table calls
TRUE, which is the bug #17 fixed once on the Python side. Both rules now read
the same, and the oracle in test_core_scan.py, which had encoded the core's,
reads it too.

That oracle is the only thing standing behind the scan, so it gained the
tally as well: every run now checks the counts against a naive count of the
naive scan's own hits.

🤖 Generated with Claude Code

The Rust scan pushed every hit into a Vec that crosses into Python as a
list, so the peak was set at that boundary whatever Python did with it
afterwards. It now skips a hit with no PAM, and returns a per-spacer
tally of what it found, what ran off a contig end and what had no PAM,
so the counters still say what the run saw rather than what it kept.

500 9-mers against chr21: peak 1,056 MB to 538 MB, with the tables
byte-identical to the Python-side filter.

matches_pam agreed with none of this: its `code` has no value for a
genomic N, so it rejected one even at an N of the pattern, where
base_matches accepts it by design and the pam_matches column always has.
Filtering on the core's own verdict would have dropped rows the table
calls TRUE. Both rules now read the same, and the oracle, which had
encoded the core's, reads it too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@emattei
emattei merged commit a635d5e into main Nov 9, 2026
9 checks passed
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.

1 participant