Skip to content

feat: add formula and contrasts to limma #8429

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion modules/nf-core/limma/differential/main.nf
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ process LIMMA_DIFFERENTIAL {
'community.wave.seqera.io/library/bioconductor-edger_bioconductor-limma:176c202c82450990' }"

input:
tuple val(meta), val(contrast_variable), val(reference), val(target)
tuple val(meta), val(contrast_variable), val(reference), val(target), val(formula), val(comparison)
tuple val(meta2), path(samplesheet), path(intensities)

output:
Expand Down
6 changes: 6 additions & 0 deletions modules/nf-core/limma/differential/meta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ input:
description: |
The value within the contrast_variable column of the sample sheet that
should be used to derive the target samples
- formula:
type: string
description: (Mandatory) R formula string used for modeling, e.g. '~ treatment + (1 | sample_number)'.
- comparison:
type: string
description: (Optional) Literal string passed to `limma::makeContrasts`, e.g. 'treatmenthND6 - treatmentmCherry'.
- - meta2:
type: map
description: |
Expand Down
26 changes: 26 additions & 0 deletions modules/nf-core/limma/differential/templates/limma_de.R
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ read_delim_flexible <- function(file, header = TRUE, row.names = NULL, check.nam
)
}

#
#' Turn “null” or empty strings into actual NULL
#'
#' @param x Input option
#'
#' @return NULL or x
#'
nullify <- function(x) {
if (is.character(x) && (tolower(x) == "null" || x == "")) NULL else x
}

################################################
################################################
## PARSE PARAMETERS FROM NEXTFLOW ##
Expand All @@ -71,6 +82,8 @@ opt <- list(
contrast_variable = '$contrast_variable',
reference_level = '$reference',
target_level = '$target',
formula = '$formula',
contrast_string = '$comparison',
blocking_variables = NULL,
probe_id_col = "probe_id",
sample_id_col = "experiment_accession",
Expand Down Expand Up @@ -116,6 +129,10 @@ if ( ! is.null(opt\$round_digits)){
opt\$round_digits <- as.numeric(opt\$round_digits)
}

# If there is no option supplied, convert string "null" to NULL
keys <- c("formula", "contrast_string")
opt[keys] <- lapply(opt[keys], nullify)

# Check if required parameters have been provided

required_opts <- c('contrast_variable', 'reference_level', 'target_level', 'output_prefix')
Expand Down Expand Up @@ -282,6 +299,10 @@ if ((! is.null(opt\$exclude_samples_col)) && (! is.null(opt\$exclude_samples_val
################################################
################################################

if (!is.null(opt\$formula)) {
model <- opt\$formula
} else {

Copy link
Member

Choose a reason for hiding this comment

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

Indents below please?

# Build the model formula with blocking variables first
model_vars <- c()

Expand All @@ -302,6 +323,7 @@ for (v in vars_to_factor) {
sample.sheet[[v]] <- as.factor(sample.sheet[[v]])
}

}
################################################
################################################
## Run Limma processes ##
Expand Down Expand Up @@ -393,6 +415,9 @@ fit <- do.call(lmFit, lmfit_args)
# Contrasts bit

# Create the contrast string for the specified comparison
if (!is.null(opt\$contrast_string)) {
contrast_string <- opt\$contrast_string
} else {
Copy link
Member

Choose a reason for hiding this comment

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

Again, indents for conditional


# Construct the expected column names for the target and reference levels in the design matrix
treatment_target <- paste0(contrast_variable, ".", opt\$target_level)
Expand All @@ -418,6 +443,7 @@ if ((treatment_target %in% colnames(design)) && (treatment_reference %in% colnam
# This indicates an error; the specified levels are not found
stop(paste0(treatment_target, " and ", treatment_reference, " not found in design matrix"))
}
}

# Create the contrast matrix
contrast.matrix <- makeContrasts(contrasts=contrast_string, levels=design)
Expand Down
130 changes: 127 additions & 3 deletions modules/nf-core/limma/differential/tests/main.nf.test
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ nextflow_process {
)
input[0] = Channel.of(['id': 'diagnosis_normal_uremia', 'variable': 'diagnosis', 'reference': 'normal', 'target': 'uremia'])
.map{
tuple(it, it.variable, it.reference, it.target)
tuple(it, it.variable, it.reference, it.target, it.formula, it.comparison)
}
input[1] = ch_samplesheet
.join(AFFY_JUSTRMA.out.expression)
Expand All @@ -72,6 +72,130 @@ nextflow_process {
)
}

}

test("test_limma_differential - null formula and null complex contrasts") {
Copy link
Member

Choose a reason for hiding this comment

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

Needs an indent fix. Sorry if I'm not seeing it, but isn't this the same as one of the tests above?


config "./nextflow.config"

setup {
run("UNTAR") {
script "../../../untar/main.nf"
process {
"""
input[0] = [
[id: 'test'],
file(params.modules_testdata_base_path + "genomics/homo_sapiens/array_expression/GSE38751_RAW.tar", checkIfExists: true)
]
"""
}
}
run("AFFY_JUSTRMA") {
script "../../../affy/justrma/main.nf"
process {
"""
ch_samplesheet = Channel.of([
[ id:'test' ],
file(params.modules_testdata_base_path + "genomics/homo_sapiens/array_expression/GSE38751.csv", checkIfExists: true)
]
)
input[0] = ch_samplesheet.join(UNTAR.out.untar)
input[1] = [[],[]]
"""
}
}
}

when {
process {
"""
ch_samplesheet = Channel.of([
[ id:'test' ],
file(params.modules_testdata_base_path + "genomics/homo_sapiens/array_expression/GSE38751.csv", checkIfExists: true)
]
)
input[0] = Channel.of(['id': 'diagnosis_normal_uremia', 'variable': 'diagnosis', 'reference': 'normal', 'target': 'uremia'])
.map{
tuple(it, it.variable, it.reference, it.target, it.formula, it.comparison)
}
input[1] = ch_samplesheet
.join(AFFY_JUSTRMA.out.expression)
"""
}
}

then {
assertAll(
{ assert process.success },
{ assert snapshot(process.out.model, process.out.versions).match() },
{ assert path(process.out.session_info[0][1]).getText().contains("limma_3.58.1") },
{ assert path(process.out.results[0][1]).getText().contains("1007_s_at\t-0.2775254") },
{ assert path(process.out.results[0][1]).getText().contains("1053_at\t-0.071547786") }
)
}

}

test("test_limma_differential - with formula and with complex contrasts") {

config "./nextflow.config"

setup {
run("UNTAR") {
script "../../../untar/main.nf"
process {
"""
input[0] = [
[id: 'test'],
file(params.modules_testdata_base_path + "genomics/homo_sapiens/array_expression/GSE38751_RAW.tar", checkIfExists: true)
]
"""
}
}
run("AFFY_JUSTRMA") {
script "../../../affy/justrma/main.nf"
process {
"""
ch_samplesheet = Channel.of([
[ id:'test' ],
file(params.modules_testdata_base_path + "genomics/homo_sapiens/array_expression/GSE38751.csv", checkIfExists: true)
]
)
input[0] = ch_samplesheet.join(UNTAR.out.untar)
input[1] = [[],[]]
"""
}
}
}

when {
process {
"""
ch_samplesheet = Channel.of([
[ id:'test' ],
file(params.modules_testdata_base_path + "genomics/homo_sapiens/array_expression/GSE38751.csv", checkIfExists: true)
]
)
input[0] = Channel.of(['id': 'diagnosis_normal_uremia', 'variable': 'diagnosis', 'reference': 'normal', 'target': 'uremia', 'formula': '~ diagnosis', 'comparison': 'diagnosis.uremia'])
Copy link
Member

Choose a reason for hiding this comment

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

Should this test maybe have null target etc to ensure they're not being used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, this is a very good observation that led me to realise a few things about the current design.

.map{
tuple(it, it.variable, it.reference, it.target, it.formula, it.comparison)
}
input[1] = ch_samplesheet
.join(AFFY_JUSTRMA.out.expression)
"""
}
}

then {
assertAll(
{ assert process.success },
{ assert snapshot(process.out.model, process.out.versions).match() },
{ assert path(process.out.session_info[0][1]).getText().contains("limma_3.58.1") },
{ assert path(process.out.results[0][1]).getText().contains("1007_s_at\t-0.27752") },
{ assert path(process.out.results[0][1]).getText().contains("1053_at\t-0.0715477") }
)
}

}

test("test_limma_differential - exclude_samples") {
Expand Down Expand Up @@ -116,7 +240,7 @@ nextflow_process {
)
input[0] = Channel.of(['id': 'diagnosis_normal_uremia', 'variable': 'diagnosis', 'reference': 'normal', 'target': 'uremia'])
.map{
tuple(it, it.variable, it.reference, it.target)
tuple(it, it.variable, it.reference, it.target, it.formula, it.comparison)
}
input[1] = ch_samplesheet
.join(AFFY_JUSTRMA.out.expression)
Expand Down Expand Up @@ -179,7 +303,7 @@ nextflow_process {
)
input[0] = Channel.of(['id': 'diagnosis_normal_uremia', 'variable': 'diagnosis', 'reference': 'normal', 'target': 'uremia'])
.map{
tuple(it, it.variable, it.reference, it.target)
tuple(it, it.variable, it.reference, it.target, it.formula, it.comparison)
}
input[1] = ch_samplesheet
.join(AFFY_JUSTRMA.out.expression)
Expand Down
48 changes: 48 additions & 0 deletions modules/nf-core/limma/differential/tests/main.nf.test.snap
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,31 @@
},
"timestamp": "2024-10-31T12:34:25.24499"
},
"test_limma_differential - with formula and with complex contrasts": {
"content": [
[
[
{
"id": "diagnosis_normal_uremia",
"variable": "diagnosis",
"reference": "normal",
"target": "uremia",
"formula": "~ diagnosis",
"comparison": "diagnosis.uremia"
},
"diagnosis_normal_uremia.limma.model.txt:md5,660fe42c7c13e47524344eaf5b7d2e7c"
]
],
[
"versions.yml:md5,88a6e42d753077edab8daf829cd4d943"
]
],
"meta": {
"nf-test": "0.9.2",
"nextflow": "24.10.6"
},
"timestamp": "2025-05-09T13:31:02.129502295"
},
"test_limma_differential - stub": {
"content": [
[
Expand Down Expand Up @@ -138,6 +163,29 @@
},
"timestamp": "2024-10-31T12:36:56.462834"
},
"test_limma_differential - null formula and null complex contrasts": {
"content": [
[
[
{
"id": "diagnosis_normal_uremia",
"variable": "diagnosis",
"reference": "normal",
"target": "uremia"
},
"diagnosis_normal_uremia.limma.model.txt:md5,70b000f632b8bdba4917046362dd876b"
]
],
[
"versions.yml:md5,88a6e42d753077edab8daf829cd4d943"
]
],
"meta": {
"nf-test": "0.9.2",
"nextflow": "24.10.6"
},
"timestamp": "2025-05-09T13:30:02.217053547"
},
"test_limma_differential - voom_mixed": {
"content": [
[
Expand Down
Loading