Skip to content

offtarget: optional per-read XC tags on a target-window BAM for IGV r… - #17

Merged
Bonney96 merged 2 commits into
feat/offtarget-wgsfrom
worktree-read-level-bam-tags
Jul 28, 2026
Merged

offtarget: optional per-read XC tags on a target-window BAM for IGV r…#17
Bonney96 merged 2 commits into
feat/offtarget-wgsfrom
worktree-read-level-bam-tags

Conversation

@Bonney96

Copy link
Copy Markdown
Contributor

…eview

The ECS caller decides per read whether it supports an edit, aggregates to per-site counts and throws the per-read decision away, so a reviewer sees indel_fraction = 0.42 and has to re-derive by eye which reads made it up.

Add --tagged-bam-out to bin/find_edited_reads.py: every read over a target window gets a string tag (default XC) naming its classification, so IGV can colour the pileup by the caller's own reasoning.

Two constraints shaped the implementation:

  • The per-target read loop fetches with a +/-150 bp pad, so overlapping targets make it visit the same alignment record repeatedly. Writing in-loop would emit duplicate records, which IGV renders as inflated depth. Tags are therefore only accumulated in the loop, keyed on (query_name, flag, reference_start) -- query_name alone collides between mates and between primary/supplementary records -- and the BAM is written in a second pass, sorted and indexed. Conflicting calls for one record resolve by a documented precedence (edit > reference > unevaluable > padding).

  • Output is window-restricted and off by default. On one AAVS1 site14 ECS sample (1149 targets, ~11,000x) it is 800 MB / 21.8 M reads and lifts peak RSS from 1.4 GB to 6.3 GB, so ECS_INDELS gets 24 GB instead of 8 GB while the flag is set. The dominant classification is left implicit rather than stored, which keeps the tag map roughly 40% smaller.

Verified on real data: with tagging off the TSV is byte-identical to the unmodified script; with it on, the full-panel BAM has 21,792,054 records, zero missing tags, zero duplicate keys, and is coordinate-sorted and indexable.

Note the tag counts do not equal the TSV columns and are not meant to: tags are per alignment record, indel_reads is per fragment and post-filter. Documented.

Wires params.offtarget_tagged_bam through ECS_INDELS and GET_INDELS, adds the schema entry, docs with the IGV recipe, and six tests on a new synthetic aligned-read fixture covering dedup, sort order, skip reasons and precedence.

PR checklist

  • This comment contains a description of changes (with reason).
  • If you've fixed a bug or added code that should be tested, add tests!
  • If you've added a new tool - have you followed the pipeline conventions in the contribution docs
  • If necessary, also make a PR on the nf-core/scge branch on the nf-core/test-datasets repository.
  • Make sure your code lints (nf-core lint).
  • Ensure the test suite passes (nextflow run . -profile test,docker --outdir <OUTDIR>).
  • Check for unexpected warnings in debug mode (nextflow run . -profile debug,test,docker --outdir <OUTDIR>).
  • Usage Documentation in docs/usage.md is updated.
  • Output Documentation in docs/output.md is updated.
  • CHANGELOG.md is updated.
  • README.md is updated (including new tool citations and authors/contributors).

Bonney96 and others added 2 commits July 28, 2026 15:40
…eview

The ECS caller decides per read whether it supports an edit, aggregates to
per-site counts and throws the per-read decision away, so a reviewer sees
indel_fraction = 0.42 and has to re-derive by eye which reads made it up.

Add --tagged-bam-out to bin/find_edited_reads.py: every read over a target
window gets a string tag (default XC) naming its classification, so IGV can
colour the pileup by the caller's own reasoning.

Two constraints shaped the implementation:

* The per-target read loop fetches with a +/-150 bp pad, so overlapping targets
  make it visit the same alignment record repeatedly. Writing in-loop would
  emit duplicate records, which IGV renders as inflated depth. Tags are
  therefore only accumulated in the loop, keyed on (query_name, flag,
  reference_start) -- query_name alone collides between mates and between
  primary/supplementary records -- and the BAM is written in a second pass,
  sorted and indexed. Conflicting calls for one record resolve by a documented
  precedence (edit > reference > unevaluable > padding).

* Output is window-restricted and off by default. On one AAVS1 site14 ECS
  sample (1149 targets, ~11,000x) it is 800 MB / 21.8 M reads and lifts peak
  RSS from 1.4 GB to 6.3 GB, so ECS_INDELS gets 24 GB instead of 8 GB while the
  flag is set. The dominant classification is left implicit rather than stored,
  which keeps the tag map roughly 40% smaller.

Verified on real data: with tagging off the TSV is byte-identical to the
unmodified script; with it on, the full-panel BAM has 21,792,054 records, zero
missing tags, zero duplicate keys, and is coordinate-sorted and indexable.

Note the tag counts do not equal the TSV columns and are not meant to: tags are
per alignment record, indel_reads is per fragment and post-filter. Documented.

Wires params.offtarget_tagged_bam through ECS_INDELS and GET_INDELS, adds the
schema entry, docs with the IGV recipe, and six tests on a new synthetic
aligned-read fixture covering dedup, sort order, skip reasons and precedence.
@Bonney96
Bonney96 merged commit e6b5a8d into feat/offtarget-wgs Jul 28, 2026
2 of 7 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