Skip to content

Add ShatterSeek chromothripsis calling with PCAWG confidence tiers - #222

Open
robert-a-forsyth wants to merge 5 commits into
devfrom
feat/shatterseek
Open

robert-a-forsyth wants to merge 5 commits into
devfrom
feat/shatterseek

Conversation

@robert-a-forsyth

Copy link
Copy Markdown
Collaborator

Summary

Adds ShatterSeek chromothripsis calling. It runs for every CN/SV caller pair that has output for a sample: ASCAT + Severus, Wakhan + Severus, and SAVANA. Each chromosome's cluster gets the PCAWG high/low-confidence tiers (HC1, HC2, HC3, LC1; Cortes-Ciriano et al. 2020, Supplementary Note 1.2, parklab/ShatterSeek#22). Results go to <sample>/shatterseek/<pair>/.

  • New subworkflow SHATTERSEEK_CHROMOTHRIPSIS: builds the same pairs as RECONPLOT_FIGURES and reads the callers with the same parsers (assets/reconplot/R/parsers), so ShatterSeek and ReConPlot see the same SVs and segments.
  • New wrapper assets/shatterseek/run_shatterseek.R: converts the caller output to ShatterSeek's input.
    • SVs: typed by strand, TRA for inter-chromosomal, breakpoints ordered with their strands, and INS / single breakends / chrY / non-primary contigs dropped.
    • Copy number: rounded to integers (SAVANA's is fractional), overlaps trimmed, and equal neighbouring segments merged.
    • It writes a per-chromosome summary, the exact ShatterSeek inputs, and a figure for every called chromosome.
  • New parameters:
    • --skip_shatterseek
    • --shatterseek_genome (inferred from --genome: GRCh38 → hg38, CHM13 → chm13)
    • --shatterseek_min_svlen (default 150 bp; translocations are always kept)
  • Container: docker.io/robertaforsyth/shatterseek:1.1.1-chm13-e1cee6c, recipe in containers/shatterseek/. Like CoRAL, Singularity pulls the Docker image, and the module refuses -profile conda.

The ShatterSeek fork

Upstream ShatterSeek computes several of its statistical criteria differently from how the PCAWG paper defines them. The module uses robert-a-forsyth/ShatterSeek@chm13-support, which fixes them in place:

  • Translocation selection used the wrong breakend (pos1 for pos2), and the narrowed set then replaced the translocation list for every later test. DO17373 chrX number_TRA goes from 1 → 600, the published value.
  • The fragment-joins test now uses intra-chromosomal SVs only, as the tutorial defines it and PCAWG's labels used. DO17373 chr2 goes from 0.87 → 0.39, the published value.
  • Breakpoint enrichment counted translocations only, was two-sided, and had chrY/M in the denominator. It now counts all breakpoints, one-sided, against the chromosome's share of 1–22 + X (hg38: non-N bases; chm13: length).
  • The exponential tests KS-tested raw coordinates against an exponential, so they were significant even for uniformly scattered breakpoints, and they drew random numbers. They now test the gaps between consecutive breakpoints, deterministically.
  • Tied clusters: clusters tied for largest were pooled into one region, e.g. every singleton SV on a chromosome. Now one cluster per chromosome is kept.
  • Smaller fixes: strands now swap with positions in SVs(); number_CN_segments_chr is no longer 2 short; the linked-window seq() crash (upstream add cramino to the main lr_somatic.nf workflow #19/Make sure that ASCAT allele/loci files are in correct format when downloaded #32) is guarded; and T2T-CHM13 support is added.

On the bundled DO17373 example, the fork reproduces PCAWG's published counts and labels: chr2, chr21 and chrX high confidence; chr3 only linked to the chr2 event. Upstream got chr3 wrong (HC2, from pooled clusters), and reached chrX only through HC3. The fork's tests/test_do17373.R checks this at every image build.

Also fixed

  • SAVANA mate collapsing (separate commit). The ReConPlot wrapper's SAVANA VCF parser now expresses _2 breakends from the _1 side before collapsing mates. SAVANA writes BP_NOTATION in (_1, _2) order on both mate records, so a junction whose _2 record came first in the file got the second breakend's position with the first breakend's orientation. ReConPlot's tables for the fixtures are unchanged; its snapshot moves only through the wrapper VERSION in versions.yml.

Things reviewers should know

  • FDR is per sample. The tiers use BH across the sample's 23 chromosomes. PCAWG corrected once across all 2,658 tumours. Raw P values are kept, so a cohort-wide correction can be recomputed.
  • HC3 (≥ 40 SVs) applies to every tumour, as in Clair split #22. PCAWG used it for three sarcoma types only; ext.args = '--no-hc3' drops it.
  • No centromere masking. On a validation sample, SAVANA (which has no panel of normals) produces a pericentromeric chr16 cluster that ShatterSeek calls high confidence, while Severus finds nothing there. Check figures of calls near centromeres.

Tests

  • New module tests: stub tests, plus real runs for all three pairs and for chm13, on synthetic fixtures (tests/fixtures/shatterseek/make_fixtures.py) in each caller's own layout. All pairs must call the chr19 event high (HC1,HC2) and nothing else. The SAVANA fixture puts _2 mates first.
  • New subworkflow stub test for the pairing.
  • tests/sv_annotation_compose.nf.test now asserts the SHATTERSEEK_* processes are present when ShatterSeek is on and absent with --skip_shatterseek.
  • Run locally (nf-test 0.9.3, Nextflow 25.10.4, Singularity):
    • ShatterSeek module + subworkflow: 7/7 passed
    • RECONPLOT: 8/8 passed
    • pipeline wiring: 2/2 passed

🤖 Generated with Claude Code

robert-a-forsyth and others added 3 commits October 14, 2026 15:45
SAVANA writes BP_NOTATION in (_1, _2) order on both mate records (all 12,679
_2 records of B2101426 are reversed relative to their own ALT). Mates were
collapsed keeping whichever record came first in the file, so a junction
whose _2 record came first paired the second breakend's position with the
first breakend's orientation. ReConPlot's tables for the fixtures are
unchanged; the snapshot moves only with the wrapper VERSION in versions.yml.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SHATTERSEEK_CHROMOTHRIPSIS runs ShatterSeek for every CN/SV caller pair with
output for a sample (ASCAT + Severus, Wakhan + Severus, SAVANA), reading the
callers with the ReConPlot wrapper's parsers, and labels each chromosome's
cluster with the PCAWG HC1/HC2/HC3/LC1 tiers (BH q at 0.2). The module uses
the robert-a-forsyth/ShatterSeek fork (chm13 support, statistics corrected to
the PCAWG definitions, one cluster per chromosome) in
docker.io/robertaforsyth/shatterseek:1.1.1-chm13-e1cee6c.

New parameters: --skip_shatterseek, --shatterseek_genome (inferred from
--genome) and --shatterseek_min_svlen (default 150 bp). Synthetic fixtures in
tests/fixtures/shatterseek cover every caller layout; module, subworkflow and
pipeline-wiring tests are tagged small.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread modules/local/shatterseek/main.nf Outdated
// No conda: the ShatterSeek fork (chm13, corrected statistics, PCAWG tiers) ships only in this image, built from
// containers/shatterseek/Dockerfile; the wrapper is pipeline glue in assets/shatterseek/ and reads the callers with
// the ReConPlot parsers in assets/reconplot/. Fork update = new commit there, rebuild, bump this tag.
container "docker.io/robertaforsyth/shatterseek:1.1.1-chm13-e1cee6c"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Container convention (REVIEW.md): this is a single Docker reference with no singularity/apptainer ternary. Custom images here are meant to use the oras://docker.io/...-sif / docker.io/... pair, as padfoot, reconplot, wakhan and lrsomaticreport do. As it stands, every Singularity/Apptainer run converts the image at pull time. nf-core pipelines download --container-system singularity won't stage a SIF for it either, even though the "Offline and air-gapped systems" section of docs/usage.md says the modules use the pinned-tag pair. AmpliconClassifier is the only other module that does this, so it isn't a reason to skip it here. Please push a robertaforsyth/shatterseek-sif:1.1.1-chm13-e1cee6c twin and use the ternary.

Comment thread assets/shatterseek/run_shatterseek.R Outdated
for (ch in unique(d$chrom)) {
x <- d[d$chrom == ch, , drop = FALSE]
# no overlaps: each segment starts after the previous one ends
for (i in seq_len(nrow(x))[-1]) if (x$start[i] <= x$end[i - 1]) x$start[i] <- x$end[i - 1] + 1L

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: overlap trimming misses nested segments. Each segment is compared only with the previous row's end, and a row that gets trimmed away (start > end) still counts as the previous row. Example: A = 1–1000, B = 500–800, C = 900–2000. B becomes 1001–800 and is dropped, but C is compared with B's end (800), so it stays 900–2000 and still overlaps A. ShatterSeek then gets overlapping CN segments, which changes the oscillation count. Comparing against the running maximum end fixes it:

Suggested change
for (i in seq_len(nrow(x))[-1]) if (x$start[i] <= x$end[i - 1]) x$start[i] <- x$end[i - 1] + 1L
for (i in seq_len(nrow(x))[-1]) { prev_end <- max(x$end[seq_len(i - 1)]); if (x$start[i] <= prev_end) x$start[i] <- prev_end + 1L }

@@ -0,0 +1,5 @@
process {
withName: 'SHATTERSEEK' {
ext.prefix = { cn_source == sv_source ? "${cn_source}" : "${sv_source}_${cn_source}" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Test gap: the pipeline's default config is never run. The module tests set only ext.prefix, so every "real" test runs the wrapper with --min-svlen 0. The pipeline instead runs --min-svlen 150 by default (conf/modules.config). That makes the length filter untested, along with the claim that translocations are always kept. Please add ext.args = { params.shatterseek_min_svlen ? "--min-svlen ${params.shatterseek_min_svlen}" : '' } here, or set it in one real test. That test should include an intra-chromosomal SV shorter than 150 bp in the fixture and assert the log line that reports it was dropped.

@claude

claude Bot commented Oct 14, 2026

Copy link
Copy Markdown

Automated review: 3 findings (see the inline comments).

  1. The SHATTERSEEK container has no singularity/docker ternary and no SIF twin. This breaks the REVIEW.md container convention and offline staging.
  2. run_shatterseek.R only trims an overlapping CN segment against the row right before it, so nested segments can still reach ShatterSeek overlapping.
  3. The module tests never apply the pipeline's default --min-svlen 150, so the length filter and the claim that translocations are always kept are untested.

For a human reviewer to check by running things (I couldn't run any tests):

  • The new small module tests run the ShatterSeek container for real. Confirm they pass in PR CI with Docker and that the snapshots hold real outputs, not empty files. In the pipeline, errorStrategy 'ignore' would hide a failure.
  • The pipeline tests skip ShatterSeek (skip_shatterseek = true in test and test_full), so nothing runs it end to end on whole-genome caller output. Please run the Slurm test array or a real sample with ShatterSeek on, for both GRCh38 and CHM13.
  • The savana.R mate-ordering change also affects ReConPlot. Its snapshot only moved versions.yml, so check that the ReConPlot SAVANA figures look the same on a real SAVANA VCF where _2 records come first.

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

nf-core pipelines lint overall result: Passed ✅ ⚠️

Posted for pipeline commit 92c81a7

+| ✅ 233 tests passed       |+
#| ❔  22 tests were ignored |#
#| ❔   1 tests had warnings |#
!| ❗  47 tests had warnings |!
Details

❗ Test warnings:

  • nextflow_config - Config manifest.version should end in dev: 1.1.0
  • pipeline_todos - TODO string in lint_log.txt: Named file extensions MUST be emitted for ALL output channels
  • pipeline_todos - TODO string in lint_log.txt: List additional required output channels/values here
  • pipeline_todos - TODO string in lint_log.txt: Named file extensions MUST be emitted for ALL output channels
  • pipeline_todos - TODO string in lint_log.txt: List additional required output channels/values here
  • pipeline_todos - TODO string in lint_log.txt: Named file extensions MUST be emitted for ALL output channels
  • pipeline_todos - TODO string in lint_log.txt: List additional required output channels/values here
  • pipeline_todos - TODO string in lint_log.txt: Named file extensions MUST be emitted for ALL output channels
  • pipeline_todos - TODO string in lint_log.txt: List additional required output channels/values here
  • pipeline_todos - TODO string in lint_log.txt: Named file extensions MUST be emitted for ALL output channels
  • pipeline_todos - TODO string in lint_log.txt: List additional required output channels/values here
  • pipeline_todos - TODO string in lint_log.txt: Named file extensions MUST be emitted for ALL output channels
  • pipeline_todos - TODO string in lint_log.txt: List additional required output channels/values here
  • pipeline_todos - TODO string in lint_log.txt: Named file extensions MUST be emitted for ALL output channels
  • pipeline_todos - TODO string in lint_log.txt: List additional required output channels/values here
  • pipeline_todos - TODO string in lint_log.txt: Named file extensions MUST be emitted for ALL output channels
  • pipeline_todos - TODO string in lint_log.txt: List additional required output channels/values here
  • pipeline_todos - TODO string in README.md: Include a figure that guides the user through the major workflow steps. Many nf-core
  • pipeline_todos - TODO string in nextflow.config: Specify your pipeline's command line flags
  • pipeline_todos - TODO string in nextflow.config: Update the field with the details of the contributors to your pipeline. New with Nextflow version 24.10.0
  • pipeline_todos - TODO string in base.config: Check the defaults for all processes
  • pipeline_todos - TODO string in base.config: Customise requirements for specific processes.
  • pipeline_todos - TODO string in methods_description_template.yml: #Update the HTML below to your preferred methods description, e.g. add publication citation for this pipeline
  • pipeline_todos - TODO string in meta.yml: #Add a description of the module and list keywords
  • pipeline_todos - TODO string in CONTRIBUTING.md: Add any pipeline specific contribution guidelines here, such as coding styles, procedures, checklists etc.
  • pipeline_todos - TODO string in nextflow.config: Specify any additional parameters here
  • schema_description - Ungrouped param in schema: skip_modkit
  • schema_description - No description provided in schema for parameter: generate_gvcf
  • schema_description - No description provided in schema for parameter: autocorrelation
  • schema_description - No description provided in schema for parameter: vep_custom
  • schema_description - No description provided in schema for parameter: vep_custom_tbi
  • schema_description - No description provided in schema for parameter: severus_minsupport
  • schema_description - No description provided in schema for parameter: wakhan_chroms
  • local_component_structure - fiberseq.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure
  • local_component_structure - deepsomatic.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure
  • local_component_structure - dmr.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure
  • local_component_structure - prepare_reference_files.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure
  • local_component_structure - prepare_annotation.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure
  • local_component_structure - ecdna.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure
  • local_component_structure - prepare_vep_plugins.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure
  • local_component_structure - phasing_haplotyping.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure
  • local_component_structure - prepare_signatures.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure
  • local_component_structure - small_variant_consensus.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure
  • local_component_structure - prepare_aa_data_repo.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure
  • local_component_structure - reconplot_figures.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure
  • local_component_structure - padfoot_annotation.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure
  • local_component_structure - shatterseek_chromothripsis.nf in subworkflows/local should be moved to a SUBWORKFLOW_NAME/main.nf structure

❔ Tests ignored:

  • files_exist - File is ignored: CODE_OF_CONDUCT.md
  • files_exist - File is ignored: assets/nf-core-lrsomatic_logo_light.png
  • files_exist - File is ignored: docs/images/nf-core-lrsomatic_logo_light.png
  • files_exist - File is ignored: docs/images/nf-core-lrsomatic_logo_dark.png
  • files_exist - File is ignored: .github/ISSUE_TEMPLATE/config.yml
  • files_exist - File is ignored: .github/workflows/awstest.yml
  • files_exist - File is ignored: .github/workflows/awsfulltest.yml
  • files_exist - File is ignored: .github/CONTRIBUTING.md
  • nextflow_config - Config variable ignored: manifest.name
  • nextflow_config - Config variable ignored: manifest.homePage
  • nf_test_content - 'tests/sv_annotation_compose.nf.test' checking ignored
  • files_unchanged - File ignored due to lint config: CODE_OF_CONDUCT.md
  • files_unchanged - File ignored due to lint config: .github/ISSUE_TEMPLATE/bug_report.yml
  • files_unchanged - File ignored due to lint config: .github/PULL_REQUEST_TEMPLATE.md
  • files_unchanged - File ignored due to lint config: .github/workflows/branch.yml
  • files_unchanged - File ignored due to lint config: .github/workflows/linting.yml
  • files_unchanged - File ignored due to lint config: assets/email_template.txt
  • files_unchanged - File ignored due to lint config: assets/nf-core-lrsomatic_logo_light.png
  • files_unchanged - File ignored due to lint config: docs/images/nf-core-lrsomatic_logo_light.png
  • files_unchanged - File ignored due to lint config: docs/images/nf-core-lrsomatic_logo_dark.png
  • files_unchanged - File ignored due to lint config: docs/README.md
  • actions_awstest - 'awstest.yml' workflow not found: /home/runner/work/lrsomatic/lrsomatic/.github/workflows/awstest.yml

❔ Tests fixed:

✅ Tests passed:

Run details

  • nf-core/tools version 4.1.0
  • Run at 2026-10-30 10:46:02

robert-a-forsyth and others added 2 commits October 30, 2026 10:48
Replace the robert-a-forsyth ShatterSeek fork with upstream parklab/ShatterSeek
(3b3f4cd, v1.1) plus a build-time patch (containers/shatterseek/patch_chm13.R)
that adds T2T-CHM13 cytobands and chromosome sizes and accepts "chm13" in the
three genome checks; the statistics are upstream's.

The wrapper now computes the PCAWG tiers itself (HC1, HC2, HC3; issue #22)
with BH q-values per sample, drops the low-confidence tier (LC1,
--min-osc-lc/--max-osc-lc), and seeds R's RNG because upstream's exponential
tests draw random numbers.

Review fixes: the module uses the singularity/docker ternary with an
oras://...-sif twin; CN overlap trimming uses the running maximum end so
nested segments no longer slip through; the module tests run the pipeline's
default --min-svlen 150 against a new 100 bp fixture deletion.

CI: inline the wrapper digests in SHATTERSEEK_CHROMOTHRIPSIS (nextflow-lint
rejected the local closure) and exclude the ShatterSeek Wakhan fixture's
trailing tab from trailing-whitespace, as for the Padfoot one.

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

This branch has not been deployed

No deployments
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