Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 60 additions & 0 deletions bin/featurecounts_merge.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Merge multiple featureCounts count tables into a single matrix.
#
# The consensus-peak quantification runs featureCounts once per library type
# (single-end / paired-end), so each input table shares an identical annotation
# block (Geneid, Chr, Start, End, Strand, Length) computed from the same SAF, and
# differs only in its per-sample count columns. This column-binds those sample
# columns back together, keyed on Geneid, and reproduces the featureCounts output
# layout expected downstream by deseq2_qc.r:
#
# line 1 : a "# Program:featureCounts" comment (skipped via read.delim skip=1)
# line 2 : header Geneid Chr Start End Strand Length <sample cols...>
# remaining rows: annotation columns 1-6 followed by one count per sample
#
# With a single input table this is an order-preserving pass-through.
#
# Usage: featurecounts_merge.sh OUTFILE INPUT1 [INPUT2 ...]
set -euo pipefail

if [ "$#" -lt 2 ]; then
echo "Usage: $(basename "$0") OUTFILE INPUT1 [INPUT2 ...]" >&2
exit 1
fi

out=$1
shift

awk '
BEGIN { FS = OFS = "\t" }

# Skip the leading "# Program:featureCounts ..." comment of every input file.
FNR == 1 { fidx++; next }

# Header row: keep the 6 annotation columns from the first file only,
# then append every input file`s sample columns (7..NF) in argument order.
FNR == 2 {
if (fidx == 1) { hdr = $1; for (i = 2; i <= 6; i++) hdr = hdr OFS $i }
for (i = 7; i <= NF; i++) hdr = hdr OFS $i
next
}

# Data rows: index on Geneid (column 1). Preserve the first file`s row order
# and annotation; append sample counts from each file for the matching Geneid.
{
key = $1
if (fidx == 1) {
order[++n] = key
a = $1; for (i = 2; i <= 6; i++) a = a OFS $i; ann[key] = a
v = ""; for (i = 7; i <= NF; i++) v = v OFS $i; val[key] = v
} else {
for (i = 7; i <= NF; i++) val[key] = val[key] OFS $i
}
}

END {
print "# Program:featureCounts (merged single-end and paired-end libraries)"
print hdr
for (j = 1; j <= n; j++) print ann[order[j]] val[order[j]]
}
' "$@" > "$out"
18 changes: 18 additions & 0 deletions conf/modules.config
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,15 @@ process {

withName: '.*:MERGED_LIBRARY_CONSENSUS_PEAKS:SUBREAD_FEATURECOUNTS' {
ext.args = '-F SAF -O --fracOverlap 0.2'
ext.prefix = { "consensus_peaks.mLb.clN.${meta.single_end ? 'SE' : 'PE'}" }
publishDir = [
path: { "${params.outdir}/${params.aligner}/merged_library/macs3/${params.narrow_peak ? 'narrow_peak' : 'broad_peak'}/consensus" },
mode: params.publish_dir_mode,
saveAs: { filename -> filename.equals('versions.yml') ? null : filename }
]
}

withName: '.*:MERGED_LIBRARY_CONSENSUS_PEAKS:FEATURECOUNTS_MERGE' {
ext.prefix = "consensus_peaks.mLb.clN"
publishDir = [
path: { "${params.outdir}/${params.aligner}/merged_library/macs3/${params.narrow_peak ? 'narrow_peak' : 'broad_peak'}/consensus" },
Expand Down Expand Up @@ -946,6 +955,15 @@ process {

withName: '.*:MERGED_REPLICATE_CONSENSUS_PEAKS:SUBREAD_FEATURECOUNTS' {
ext.args = '-F SAF -O --fracOverlap 0.2'
ext.prefix = { "consensus_peaks.mRp.clN.${meta.single_end ? 'SE' : 'PE'}" }
publishDir = [
path: { "${params.outdir}/${params.aligner}/merged_replicate/macs3/${params.narrow_peak ? 'narrow_peak' : 'broad_peak'}/consensus" },
mode: params.publish_dir_mode,
saveAs: { filename -> filename.equals('versions.yml') ? null : filename }
]
}

withName: '.*:MERGED_REPLICATE_CONSENSUS_PEAKS:FEATURECOUNTS_MERGE' {
ext.prefix = "consensus_peaks.mRp.clN"
publishDir = [
path: { "${params.outdir}/${params.aligner}/merged_replicate/macs3/${params.narrow_peak ? 'narrow_peak' : 'broad_peak'}/consensus" },
Expand Down
3 changes: 1 addition & 2 deletions modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,7 @@
"subread/featurecounts": {
"branch": "master",
"git_sha": "6d46786420b4d7bc88eba026eb389c0c5535d120",
"installed_by": ["modules"],
"patch": "modules/nf-core/subread/featurecounts/subread-featurecounts.diff"
"installed_by": ["modules"]
},
"trimgalore": {
"branch": "master",
Expand Down
33 changes: 33 additions & 0 deletions modules/local/featurecounts_merge.nf
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
process FEATURECOUNTS_MERGE {
tag "${meta.id}"
label 'process_single'

conda "conda-forge::sed=4.7"
container "${ workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ?
'https://depot.galaxyproject.org/singularity/ubuntu:20.04' :
'nf-core/ubuntu:20.04' }"

input:
tuple val(meta), path('featurecounts/*')

output:
tuple val(meta), path("*.featureCounts.tsv"), emit: counts
tuple val("${task.process}"), val('sed'), eval("sed --version | sed '1!d;s/.*GNU sed) //'"), topic: versions

when:
task.ext.when == null || task.ext.when

script:
def prefix = task.ext.prefix ?: "${meta.id}"
"""
featurecounts_merge.sh \\
${prefix}.featureCounts.tsv \\
\$(ls featurecounts/*.featureCounts.tsv | sort)
"""

stub:
def prefix = task.ext.prefix ?: "${meta.id}"
"""
touch ${prefix}.featureCounts.tsv
"""
}
102 changes: 102 additions & 0 deletions modules/local/tests/featurecounts_merge.nf.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
nextflow_process {

name "Test Process FEATURECOUNTS_MERGE"
script "../featurecounts_merge.nf"
process "FEATURECOUNTS_MERGE"

tag "modules"
tag "modules_local"
tag "featurecounts_merge"

test("merge single-end and paired-end count tables") {

when {
process {
"""
input[0] = [
[ id:'consensus_peaks' ],
[
file("\${projectDir}/modules/local/tests/fixtures/se.featureCounts.tsv", checkIfExists: true),
file("\${projectDir}/modules/local/tests/fixtures/pe.featureCounts.tsv", checkIfExists: true)
]
]
"""
}
}

then {
def merged = path(process.out.counts.get(0).get(1)).readLines()
def header = merged.find { it.startsWith('Geneid') }.split('\t')
def sampleCols = header[6..-1]
assertAll(
{ assert process.success },
// one merged table emitted
{ assert process.out.counts.size() == 1 },
// The module merges its inputs in `ls | sort` filename order, so the
// column order is deterministic. Here 'pe.featureCounts.tsv' sorts
// before 'se.featureCounts.tsv' (in the pipeline the files are
// '...PE.featureCounts.tsv' / '...SE.featureCounts.tsv', so PE-first).
// Every SE and PE sample column appears exactly once.
{ assert sampleCols == ['T0_PE.bam', 'T15_PE.bam', 'T100_SE.bam', 'T150_SE.bam'] },
{ assert sampleCols.toUnique().size() == sampleCols.size() },
// Annotation + row order preserved from the first-sorted (PE) table;
// SE counts matched to rows by Geneid despite the different row order.
{ assert merged.contains('peak_3\tIII\t9\t309\t+\t301\t131\t132\t31\t32') },
{ assert merged.contains('peak_1\tI\t1\t100\t+\t100\t111\t112\t11\t12') },
{ assert merged.contains('peak_2\tII\t5\t205\t+\t201\t121\t122\t21\t22') },
{ assert snapshot(process.out.versions).match("versions") }
)
}
}

test("single-file pass-through (homogeneous cohort)") {

when {
process {
"""
input[0] = [
[ id:'consensus_peaks' ],
[ file("\${projectDir}/modules/local/tests/fixtures/se.featureCounts.tsv", checkIfExists: true) ]
]
"""
}
}

then {
def merged = path(process.out.counts.get(0).get(1)).readLines()
def header = merged.find { it.startsWith('Geneid') }.split('\t')
assertAll(
{ assert process.success },
{ assert (header[6..-1]) == ['T100_SE.bam', 'T150_SE.bam'] },
{ assert merged.contains('peak_1\tI\t1\t100\t+\t100\t11\t12') },
{ assert merged.contains('peak_3\tIII\t9\t309\t+\t301\t31\t32') }
)
}
}

test("stub") {

options "-stub"

when {
process {
"""
input[0] = [
[ id:'consensus_peaks' ],
[
file("\${projectDir}/modules/local/tests/fixtures/se.featureCounts.tsv", checkIfExists: true),
file("\${projectDir}/modules/local/tests/fixtures/pe.featureCounts.tsv", checkIfExists: true)
]
]
"""
}
}

then {
assertAll(
{ assert process.success },
{ assert snapshot(process.out).match() }
)
}
}
}
44 changes: 44 additions & 0 deletions modules/local/tests/featurecounts_merge.nf.test.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"versions": {
"content": null,
"timestamp": "2026-07-23T03:28:47.894742",
"meta": {
"nf-test": "0.9.4",
"nextflow": "26.04.4"
}
},
"stub": {
"content": [
{
"0": [
[
{
"id": "consensus_peaks"
},
"consensus_peaks.featureCounts.tsv:md5,d41d8cd98f00b204e9800998ecf8427e"
]
],
"1": [
[
"FEATURECOUNTS_MERGE",
"sed",
""
]
],
"counts": [
[
{
"id": "consensus_peaks"
},
"consensus_peaks.featureCounts.tsv:md5,d41d8cd98f00b204e9800998ecf8427e"
]
]
}
],
"timestamp": "2026-07-23T03:28:54.791169",
"meta": {
"nf-test": "0.9.4",
"nextflow": "26.04.4"
}
}
}
5 changes: 5 additions & 0 deletions modules/local/tests/fixtures/pe.featureCounts.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Program:featureCounts v2.1.1; Command:"featureCounts" "-F" "SAF" "-p"
Geneid Chr Start End Strand Length T0_PE.bam T15_PE.bam
peak_3 III 9 309 + 301 131 132
peak_1 I 1 100 + 100 111 112
peak_2 II 5 205 + 201 121 122
5 changes: 5 additions & 0 deletions modules/local/tests/fixtures/se.featureCounts.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Program:featureCounts v2.1.1; Command:"featureCounts" "-F" "SAF"
Geneid Chr Start End Strand Length T100_SE.bam T150_SE.bam
peak_1 I 1 100 + 100 11 12
peak_2 II 5 205 + 201 21 22
peak_3 III 9 309 + 301 31 32
6 changes: 1 addition & 5 deletions modules/nf-core/subread/featurecounts/environment.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions modules/nf-core/subread/featurecounts/main.nf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 0 additions & 35 deletions modules/nf-core/subread/featurecounts/subread-featurecounts.diff

This file was deleted.

Loading
Loading