Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bf9bbbf
Merge pull request #161 from IntGenomicsLab/dev
robert-a-forsyth Jun 3, 2026
9af50f1
feat: add LRSOMATICREPORT as the final pipeline step
ljwharbers Jul 15, 2026
c802922
fix: resolve five bugs blocking LRSOMATICREPORT CI (PR #176)
ljwharbers Jul 16, 2026
f9c1d3d
fix: use docker-context Wave build for LRSOMATICREPORT container
ljwharbers Jul 17, 2026
4df21e1
fix: replace stageInMode copy with explicit cp -rL for report_src
ljwharbers Jul 17, 2026
95bb782
feat: vendor lrsomatic_report v1.1.0 and wire the report to SV VEP + …
ljwharbers Aug 12, 2026
820bbf7
fix: exempt .gitattributes from the nf-core template-unchanged check
ljwharbers Aug 12, 2026
2308cb9
Merge remote-tracking branch 'origin/dev' into worktree-lrsomatic-rep…
ljwharbers Aug 14, 2026
d729fcf
fix: stage the report gene panel and stop clobbering CONDA_PREFIX
ljwharbers Aug 14, 2026
60847fc
chore: re-sync the vendored lrsomatic_report to v1.2.1
ljwharbers Aug 28, 2026
21d6bdb
Merge remote-tracking branch 'origin/dev' into worktree-lrsomatic-rep…
ljwharbers Aug 28, 2026
1942b50
fix(lrsomaticreport): drop task.ext reference from container directive
laulambr Sep 1, 2026
fe4de42
fix(lrsomatic): flatten ch_ascat_files before it reaches LRSOMATICREPORT
laulambr Sep 1, 2026
5dfdb69
Remove explanatory comments from container and ascat fixes
ljwharbers Sep 1, 2026
300907f
Merge pull request #185 from IntGenomicsLab/fix/lrsomaticreport-conta…
ljwharbers Sep 1, 2026
6eb07df
feat: apply several report gene panels at once
ljwharbers Sep 1, 2026
5a13513
fix(tests): stop snapshotting sample4's merged BAM md5
ljwharbers Sep 2, 2026
ddfc4ea
Merge pull request #186 from IntGenomicsLab/fix/lrsomaticreport-conta…
ljwharbers Sep 2, 2026
cdd98db
fix: address remaining PR #176 review comments
ljwharbers Sep 2, 2026
3d454bf
Merge pull request #187 from IntGenomicsLab/fix/pr176-review-nits
ljwharbers Sep 2, 2026
20ccd2a
chore: trim over-explained comments added by this PR
ljwharbers Sep 2, 2026
a1b8855
style: trim report-module comments in pipeline-owned files to one line
ljwharbers Sep 3, 2026
45a59e6
chore: re-sync vendored lrsomatic_report to v1.3.2
ljwharbers Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
*.nf.test linguist-language=nextflow
modules/nf-core/** linguist-generated
subworkflows/nf-core/** linguist-generated
assets/lrsomatic_report/** linguist-vendored
3 changes: 3 additions & 0 deletions .nf-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ lint:
- .github/workflows/awsfulltest.yml
- .github/CONTRIBUTING.md
files_unchanged:
# Exempt: carries `assets/lrsomatic_report/** linguist-vendored`, which keeps the
# vendored tool source out of GitHub's language statistics
- .gitattributes
- CODE_OF_CONDUCT.md
- assets/nf-core-lrsomatic_logo_light.png
- docs/images/nf-core-lrsomatic_logo_light.png
Expand Down
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ repos:
.*ro-crate-metadata.json$|
modules/(?!local/).*|
subworkflows/(?!local/).*|
assets/lrsomatic_report/.*|
.*\.snap$
)$
- id: end-of-file-fixer
Expand All @@ -23,6 +24,7 @@ repos:
.*ro-crate-metadata.json$|
modules/(?!local/).*|
subworkflows/(?!local/).*|
assets/lrsomatic_report/.*|
.*\.snap$
)$
- repo: https://github.com/seqeralabs/nf-lint-pre-commit
Expand Down
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ bin/
ro-crate-metadata.json
modules/nf-core/
subworkflows/nf-core/
# Vendored upstream tool source -- see assets/lrsomatic_report/VENDORED.md
assets/lrsomatic_report/
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## v1.2.0dev

### `Added`

- [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Added `LRSOMATICREPORT` as the final pipeline step: a self-contained per-sample HTML report covering small variants, structural variants, copy number and QC. Skip it with `--skip_report`; choose the gene panel selected on load with `--report_gene_panel` (@ljwharbers).
- [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Vendored the [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) v1.3.2 tool source at `assets/lrsomatic_report`, so `nextflow run IntGenomicsLab/lrsomatic` ships it without a submodule checkout (@ljwharbers).
- [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Added a `solution_dirs` output to the WAKHAN module so its per-solution copy-number plots can be staged downstream (@ljwharbers).

### `Changed`

- [#186](https://github.com/IntGenomicsLab/lrsomatic/pull/186) - Re-synced the vendored [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) to v1.3.0, which adds tickbox dropdown filters on the categorical columns of both variant tables and turns the report's gene panel selector into checkboxes (@ljwharbers).
- [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Re-synced the vendored [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) to v1.3.2: facet dropdown counts follow the active filters, opening a facet menu no longer resets the table's horizontal scroll, a flatter clinical theme, and inline code comments trimmed to one line (@ljwharbers).
- [#186](https://github.com/IntGenomicsLab/lrsomatic/pull/186) - `--report_gene_panel` now takes a comma-separated list, so several panels can be applied at once: a variant or SV is kept if it hits any of them. Panel values are also validated at launch instead of failing inside the report task (@ljwharbers).
- [#184](https://github.com/IntGenomicsLab/lrsomatic/pull/184) - Replaced the CHM13 Severus panel of normals with the merged 1000 Genomes + ASAP panel (@AmberVerhasselt).

### `Fixed`

- [#186](https://github.com/IntGenomicsLab/lrsomatic/pull/186) - Stopped snapshotting the md5 of sample4's merged tumour BAM and its index in the `clair_only` nf-test: `samtools merge` gives the colliding `@PG` IDs of the two replicates a random hex suffix, so neither digest is reproducible. The alignment records are, and are now asserted with `bam().getReadsMD5()` instead (@ljwharbers).
- [#182](https://github.com/IntGenomicsLab/lrsomatic/pull/182) - Added `--vcf` to the default `vep_args` so VEP writes VCF output rather than its default tab-delimited format (@AmberVerhasselt).

## v1.1.0 - [2026-04-28]
Expand Down
4 changes: 4 additions & 0 deletions CITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@

> Lin JH, Chen LC, Yu SC, Huang YT. LongPhase: an ultra-fast chromosome-scale phasing algorithm for small and large variants. Bioinformatics. 2022 Apr 28;38(9):2452-2455. doi: 10.1093/bioinformatics/btac126. PubMed PMID: 35253834; PubMed Central PMCID: PMC9048675.

- [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report)

> Standalone R/Quarto reporting tool that renders the pipeline's final per-sample HTML report. https://github.com/ljwharbers/lrsomatic_report

- [minimap2](https://pubmed.ncbi.nlm.nih.gov/29750242/)

> Li H. Minimap2: pairwise alignment for nucleotide sequences. Bioinformatics. 2018 Sep 15;34(18):3094-3100. doi: 10.1093/bioinformatics/bty191. PubMed PMID: 29750242; PubMed Central PMCID: PMC6137996.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ IntGenomicsLab/lr_somatic was originally written by Luuk Harbers, Robert Forsyth

## Pipeline output

This pipeline produces a series of different output files. The main output is an aligned and phased tumour bam file. This bam file can be used by any typical downstream tool that uses bam files as input. Furthermore, we have sample-specific QC outputs from `cramino` (fastq), `cramino` (bam), `mosdepth`, `samtools` (stats/flagstat/idxstats), and optionally `fibertools`. Finally, we have a `multiqc` report from that combines the output from `mosdepth` and `samtools` into one html report.
This pipeline produces a series of different output files. The main output is an aligned and phased tumour bam file. This bam file can be used by any typical downstream tool that uses bam files as input. Furthermore, we have sample-specific QC outputs from `cramino` (fastq), `cramino` (bam), `mosdepth`, `samtools` (stats/flagstat/idxstats), and optionally `fibertools`. Finally, we have a `multiqc` report that combines the output from `mosdepth` and `samtools` into one HTML report, and a self-contained per-sample HTML report (`<sample>/report/<sample>_report.html`) covering small variants, structural variants, copy number and QC in one place — disable it with `--skip_report`.

Besides QC and the aligned and phased bam file, we have output from (structural) variant and copy number callers, of which some are optional. The output from these variant callers can be found in their respective folders. For small and structural variant callers (`clairS`, `clairS-TO`, and `severus`) these will contain, among others, `vcf` files with called variants. For `ascat` these contain files with final copy number information and plots of the copy number profiles.

Expand All @@ -128,6 +128,7 @@ Example output directory structure:
│ │ ├── germline
│ │ ├── somatic
│ │ ├── SVs
│ ├── report
├── Sample 2
│ ├── ascat
Expand Down
21 changes: 21 additions & 0 deletions assets/lrsomatic_report/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Luuk Harbers

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
263 changes: 263 additions & 0 deletions assets/lrsomatic_report/R/circos.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
suppressPackageStartupMessages({
library(circlize)
library(data.table)
})

# Colour palettes — keep in sync with --circos-* in assets/styles/report.scss

# SBS-6 SNV palette (SigProfiler/COSMIC standard, softened slightly toward report ink/paper)
SNV_COLOURS = c(
"C>A" = "#2EBAED",
"C>G" = "#1b1e22",
"C>T" = "#b3402f",
"T>A" = "#c7c2b8",
"T>C" = "#ADCC54",
"T>G" = "#F0D0CE"
)

# SV colours — saturated, hue-matched to --sv-* table tokens
SV_COLOURS = c(
INS = "#cf5b46",
DEL = "#2f6db3",
INV = "#c08a1e",
DUP = "#3f7d4e"
)

SV_YPOS = c(INS = 1.0, DEL = 0.66, INV = 0.33, DUP = 0.05)

# CNV colours — tied to the report spine
CNV_COLOURS = c(
major = "#b3402f", # brick = "more"
minor = "#0d5c75", # teal = "less"
total = "#1b1e22" # ink
)

# BND/translocation link colour
BND_COLOUR = "#8a5fa3"

# Classify SNV into 6 SBS categories (C/T-ref normalised)
.classify_mut = function(ref, alt) {
comp = c(A = "T", T = "A", C = "G", G = "C")
ref = toupper(ref); alt = toupper(alt)
use_comp = !(ref %in% c("C", "T"))
norm_ref = ifelse(use_comp, comp[ref], ref)
norm_alt = ifelse(use_comp, comp[alt], alt)
paste0(norm_ref, ">", norm_alt)
}

# Draw a circos plot to output_path (SVG or PNG by extension)
draw_circos = function(snv_data = NULL,
sv_nontrans = NULL,
sv_trans = NULL,
cnv_data = NULL,
cytobands,
chrom_lengths,
chromosomes,
output_path) {

# Filter cytobands and lengths to displayed chromosomes
cyto_filt = cytobands[cytobands$chrom %in% chromosomes, ]
lens_filt = chrom_lengths[names(chrom_lengths) %in% chromosomes]
lens_filt = lens_filt[chromosomes[chromosomes %in% names(lens_filt)]]

# Prepare SNV data
if (!is.null(snv_data) && nrow(snv_data) > 0) {
snv = as.data.table(snv_data)[nchar(ref) == 1 & nchar(alt) == 1]
snv = snv[chrom %in% chromosomes]
snv[, mut_cat := .classify_mut(ref, alt)]
snv[, circos_col := SNV_COLOURS[mut_cat]]
snv[is.na(circos_col), circos_col := "#AAAAAA"]
} else {
snv = data.table(chrom = character(), pos = integer(),
mut_cat = character(), circos_col = character())
}

# Prepare SV (non-BND) data
if (!is.null(sv_nontrans) && nrow(sv_nontrans) > 0) {
sv_nt = as.data.table(sv_nontrans)[chrom %in% chromosomes]
} else {
sv_nt = data.table(chrom = character(), pos = integer(), end = integer(),
svtype = character(), circos_pos = numeric(), circos_col = character())
}

# Prepare translocation (BND) data
if (!is.null(sv_trans) && nrow(sv_trans) > 0) {
sv_tr = as.data.table(sv_trans)[chrom %in% chromosomes & chrom2 %in% chromosomes]
} else {
sv_tr = data.table(chrom = character(), pos = integer(),
chrom2 = character(), pos2 = integer())
}

# Prepare CNV data
if (!is.null(cnv_data) && nrow(cnv_data) > 0) {
cnv = as.data.table(cnv_data)[chr %in% chromosomes]
cnv = cnv[order(chr, startpos)]
} else {
cnv = data.table(chr = character(), startpos = integer(), endpos = integer(),
major_cn = numeric(), minor_cn = numeric(), total_cn = numeric())
}

# Open device
ext = tolower(tools::file_ext(output_path))
if (ext == "svg") {
svglite::svglite(output_path, width = 8, height = 8)
} else {
png(output_path, width = 2400, height = 2400, res = 300)
}

plot.new()
circos.clear()

n_chr = length(chromosomes)
gap_degrees = c(rep(1.5, n_chr - 1), 7)

# Single quiet track surface; colours match the report.scss border/surface tokens
track_bg = "#fbfaf6"
track_border = "#e4e0d6"

circos.par(
"start.degree" = 90,
"gap.degree" = gap_degrees,
"track.margin" = c(0.006, 0.006),
"cell.padding" = c(0, 0, 0, 0)
)

# Build cytobands list as expected by circos.initializeWithIdeogram
cyto_list = list(
df = cyto_filt,
chromosome = chromosomes[chromosomes %in% unique(cyto_filt$chrom)],
chr.len = lens_filt
)

circos.initializeWithIdeogram(cyto_list$df,
chromosome.index = cyto_list$chromosome,
plotType = c("ideogram", "labels"),
labels.cex = 0.8)

# Pre-compute jitter once so it varies per chromosome but stays reproducible
set.seed(42)

# ---- Track 1: SNV dots (coloured by mutation category) ------------------
circos.trackPlotRegion(
factors = chromosomes,
ylim = c(0, 1),
bg.border = track_border,
bg.col = track_bg,
track.height = 0.16,
panel.fun = function(region, value, ...) {
chr = get.cell.meta.data("sector.index")
sub_snv = snv[chrom == chr]
if (nrow(sub_snv) == 0) return(invisible(NULL))
y_jitter = runif(nrow(sub_snv), 0.05, 0.95)
# Translucent so a dense cloud reads as a tint, not confetti.
circos.points(
x = sub_snv$pos,
y = y_jitter,
col = adjustcolor(sub_snv$circos_col, alpha.f = 0.65),
pch = 19,
cex = 0.18
)
}
)

# ---- Track 2: Non-BND SVs (DEL/DUP/INV/INS as horizontal segments) -----
circos.trackPlotRegion(
factors = chromosomes,
ylim = c(0, 1),
bg.border = track_border,
bg.col = track_bg,
track.height = 0.10,
panel.fun = function(region, value, ...) {
chr = get.cell.meta.data("sector.index")
sub_sv = sv_nt[chrom == chr & !is.na(circos_pos)]
if (nrow(sub_sv) == 0) return(invisible(NULL))
for (i in seq_len(nrow(sub_sv))) {
x1 = sub_sv$pos[i]
x2 = if (!is.na(sub_sv$end[i]) && sub_sv$end[i] > x1) sub_sv$end[i] else x1 + 1L
circos.segments(
x0 = x1, x1 = x2,
y0 = sub_sv$circos_pos[i], y1 = sub_sv$circos_pos[i],
col = sub_sv$circos_col[i],
lwd = 2.5
)
}
}
)

# Y-axis labels for SV track
tryCatch(
circos.yaxis(
side = "left",
at = c(0.05, 0.33, 0.66, 1.0),
labels = c("DUP", "INV", "DEL", "INS"),
track.index = 3,
sector.index = chromosomes[1],
labels.niceFacing = TRUE,
labels.cex = 0.45
),
error = function(e) NULL
)

# ---- Track 3: ASCAT copy-number -----------------------------------------
circos.trackPlotRegion(
factors = chromosomes,
ylim = c(0, 4),
bg.border = track_border,
bg.col = track_bg,
track.height = 0.18,
panel.fun = function(region, value, ...) {
chr = get.cell.meta.data("sector.index")
sub_cnv = cnv[chr == get.cell.meta.data("sector.index")]
if (nrow(sub_cnv) == 0) return(invisible(NULL))

xmax = lens_filt[chr]
if (!is.na(xmax)) {
for (y_ref in c(1, 2, 3)) {
circos.lines(c(0, xmax), c(y_ref, y_ref),
col = track_border, lwd = 0.4, lty = "dotted")
}
}

circos.yaxis(
side = "left",
at = c(0, 1, 2, 3, 4),
labels = c("0", "1", "2", "3", "4+"),
sector.index = chromosomes[1],
labels.niceFacing = TRUE,
labels.cex = 0.40
)

for (i in seq_len(nrow(sub_cnv))) {
xl = sub_cnv$startpos[i]; xr = sub_cnv$endpos[i]
maj = sub_cnv$major_cn[i]
circos.rect(xl, maj + 0.02, xr, maj + 0.12,
col = CNV_COLOURS["major"], border = CNV_COLOURS["major"], lwd = 0.05)
min_cn = sub_cnv$minor_cn[i]
circos.rect(xl, min_cn - 0.12, xr, min_cn - 0.02,
col = CNV_COLOURS["minor"], border = CNV_COLOURS["minor"], lwd = 0.05)
tot = sub_cnv$total_cn[i]
circos.rect(xl, tot - 0.03, xr, tot + 0.03,
col = CNV_COLOURS["total"], border = CNV_COLOURS["total"], lwd = 0.05)
}
}
)

# ---- Translocation links (BND): one arc per mate-collapsed rearrangement ----
if (nrow(sv_tr) > 0) {
for (i in seq_len(nrow(sv_tr))) {
tryCatch(
circos.link(
sector.index1 = sv_tr$chrom[i], point1 = sv_tr$pos[i],
sector.index2 = sv_tr$chrom2[i], point2 = sv_tr$pos2[i],
col = adjustcolor(BND_COLOUR, alpha.f = 0.45),
lwd = 0.9
),
error = function(e) NULL
)
}
}

circos.clear()
dev.off()
invisible(output_path)
}
Loading
Loading