Skip to content

Key CRISPR guides on (anchor, protospacer) via disambiguate-on-collision#248

Open
Lioscro wants to merge 4 commits into
mainfrom
joseph.min/crispr-anchor-protospacer-keying
Open

Key CRISPR guides on (anchor, protospacer) via disambiguate-on-collision#248
Lioscro wants to merge 4 commits into
mainfrom
joseph.min/crispr-anchor-protospacer-keying

Conversation

@Lioscro

@Lioscro Lioscro commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Problem

CrisprMapper identified each guide by its protospacer alone and fed the raw (un-deduplicated) protospacer column to seqhash::SeqHash::new, which rejects duplicate parents. On libraries where the same protospacer is deployed on both anchor scaffolds, the build aborts with:

parent at index 1 is duplicate of parent at index 0

This blocks cyto detect crispr, cyto map crispr, and the crispr workflow on such libraries (e.g. FLEXLIBPOOL009: 278 guides, 2 anchors, 6 protospacers shared across the two anchors).

Fix

Key guide identity on the (anchor, protospacer) pair via disambiguate-on-collision:

  • Dedup protospacers before hashingSeqHash never sees a duplicate parent, so the crash is structurally impossible.
  • Unique protospacer → ignore the anchor (the overwhelming common case) — behavior byte-identical to today.
  • Shared protospacer → the matched anchor selects the guide; a matched anchor carrying none of that protospacer's guides is unmapped (None).
  • feature_idx is now the 0-based TSV row index (contiguous 0..N, aligned in order with names / record_stream() / metadata/features.tsv).
  • Two rows identical in both anchor and protospacer are a load-time error naming both guides (replaces the opaque seqhash message).
  • statistics() reports the true guide count; an info! line logs when protospacers are shared.

Change is confined to crates/cyto-map/src/mapper/crispr.rs (plus two test fixtures and a CLAUDE.md doc update). The downstream pipeline (processor.rscyto-ibu-count → workflow) is feature-index-agnostic and unchanged.

Scope: previous behavior explicitly retained (decision deferred)

This change is deliberately minimal. The previous mapping behavior is explicitly retained: a protospacer is matched regardless of anchor for every guide whose protospacer is unique (the common case). The anchor is consulted only to break ties among the rare protospacers shared across anchors — the minimum needed to fix the build crash without changing mapping outcomes for any library that builds today.

This PR does not make the anchor a mandatory part of guide identity (i.e. strict (anchor, protospacer) pairing enforced on every read). That is a broader decision — it would change mapping rates for all libraries — and is deferred until we decide whether the anchor should also be considered as part of guide identity generally.

Testing

  • cargo test -p cyto-map: 77/77 (7 new unit tests covering collision resolution with exact per-anchor row index, unique-protospacer backward-compat, wrong-anchor→None, dup-pair error, statistics/row-count, exact=true build, real-library guard). clippy clean (no new warnings), fmt clean.
  • Mutation check: neutering the disambiguation branch fails exactly the two collision tests and passes when restored.
  • Regression proof (real data): the exact bug-report cyto detect crispr on FLEXLIBPOOL009 now exits 0, emits geometry [barcode][umi:12] | [:2][probe][anchor][protospacer], and logs 6 protospacer(s) shared across anchors; disambiguating by anchor — no duplicate error.
  • FLEXLIBPOOL009 features.tsv = 278 rows, all 12 shared-protospacer guides present as distinct rows.
  • Zero regression: standard-library data/libraries/crispr_guides.tsv features.tsv is byte-identical between main and this branch (6066 rows, matching md5).

Note

cargo test --workspace may show a cyto-download::run_uses_server flake (localhost HTTP port contention); it passes in isolation and that crate is untouched by this PR.

🤖 Generated with Claude Code

Lioscro and others added 3 commits July 1, 2026 14:27
CrisprMapper identified guides by protospacer alone and fed the raw
protospacer column to SeqHash::new, which rejects duplicate parents,
crashing on libraries where a protospacer appears under two anchor
scaffolds (e.g. FLEXLIBPOOL009: "parent at index 1 is duplicate of
parent at index 0").

Key guide identity on (anchor, protospacer) via disambiguate-on-collision:
- dedup protospacers before hashing (SeqHash never sees a duplicate)
- proto_guides maps each protospacer parent idx to its (anchor, row) guides
- query ignores the anchor when a protospacer is unique (backward-compatible)
  and uses the matched anchor to disambiguate when shared; unmapped if the
  matched anchor carries none of that protospacer's guides
- feature_idx is now the TSV row index (contiguous 0..N, aligned with
  features.tsv / record_stream)
- unresolvable identical (anchor, protospacer) rows bail! naming both guides
- statistics report the true guide count; info! logs shared protospacers

Adds 7 unit tests (collision resolution asserting exact per-anchor row
index, unique-protospacer backward-compat, wrong-anchor->None, dup-pair
error, statistics/row-count, exact-build path, real-library guard) and
two fixtures. Mutation check confirms the collision tests catch the
regression. Confined to crispr.rs; downstream is index-agnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update crates/cyto-map/CLAUDE.md for the Phase 1 keying change:
- crispr.rs bullet now describes (anchor, protospacer) disambiguate-on-
  collision keying (protospacer dedup, feature_idx = TSV row index,
  load-time error on a duplicate (anchor, protospacer) pair)
- fix stale Mapper::query signature: Option<usize> -> Option<FeatureMatch>

Integration-validated on real data (not in repo): the exact bug-report
`cyto detect crispr` on FLEXLIBPOOL009 now exits 0 with a geometry string
(was: "parent at index 1 is duplicate of parent at index 0"); features.tsv
has 278 rows with all 12 shared-protospacer guides; and the standard
library's features.tsv is byte-identical (md5 unchanged) to main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address Step 6 integration-review suggestion: the anchor-constant block
header implied all three test anchors are from the real library. Anchors
A and B are the real library anchors; C is a synthetic 30bp anchor used
for the wrong-anchor -> None path. Comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the CRISPR mapper to support disambiguating guides when a protospacer is shared across multiple anchors. It introduces a new proto_guides mapping, rejects duplicate anchor-protospacer pairs at load time, and updates the query logic to resolve collisions based on the matched anchor. The review feedback suggests destructuring the CrisprRecord during deserialization to avoid unnecessary string clones, which is a valuable performance improvement.

Comment thread crates/cyto-map/src/mapper/crispr.rs
…om_file

Address Gemini PR review: destructure CrisprRecord into owned fields so
the anchor-miss path moves instead of clones, and the protospacer-miss
path clones once (HashMap key) then moves into proto_unique instead of
cloning twice. Load-time only; behavior unchanged. Tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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