From 2b0fa4ab072d9858e2684cdda4cedb07561f0cac Mon Sep 17 00:00:00 2001 From: delfiterradas Date: Fri, 27 Mar 2026 16:25:11 +0000 Subject: [PATCH 1/3] Accept spaces in variable names and migrate to topics channels --- .../nf-core/variancepartition/dream/main.nf | 2 +- .../nf-core/variancepartition/dream/meta.yml | 15 ++++ .../variancepartition/dream/templates/dream.R | 85 +++++++++++++++++-- .../dream/tests/main.nf.test | 12 +-- 4 files changed, 100 insertions(+), 14 deletions(-) diff --git a/modules/nf-core/variancepartition/dream/main.nf b/modules/nf-core/variancepartition/dream/main.nf index 0e02941c755f..171947b62460 100644 --- a/modules/nf-core/variancepartition/dream/main.nf +++ b/modules/nf-core/variancepartition/dream/main.nf @@ -15,7 +15,7 @@ process VARIANCEPARTITION_DREAM { tuple val(meta), path("*.dream.results.tsv") , emit: results tuple val(meta), path("*.dream.model.txt") , emit: model tuple val(meta), path("*.normalised_counts.tsv") , emit: normalised_counts, optional: true - path "versions.yml" , emit: versions + path "versions.yml" , emit: versions_variancepartition, topic: versions when: task.ext.when == null || task.ext.when diff --git a/modules/nf-core/variancepartition/dream/meta.yml b/modules/nf-core/variancepartition/dream/meta.yml index a98fe8fd98eb..dd5b37c0132b 100644 --- a/modules/nf-core/variancepartition/dream/meta.yml +++ b/modules/nf-core/variancepartition/dream/meta.yml @@ -92,6 +92,14 @@ output: pattern: "*.normalised_counts.tsv" ontologies: - edam: http://edamontology.org/format_3475 # TSV + versions_variancepartition: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML +topics: versions: - versions.yml: type: file @@ -107,3 +115,10 @@ maintainers: - "@alanmmobbs03" - "@nschcolnicov" - "@atrigila" +versions: + - versions.yml: + type: file + description: File containing software versions + pattern: "versions.yml" + ontologies: + - edam: http://edamontology.org/format_3750 # YAML \ No newline at end of file diff --git a/modules/nf-core/variancepartition/dream/templates/dream.R b/modules/nf-core/variancepartition/dream/templates/dream.R index d5443eeb6a52..389a5950dd5a 100644 --- a/modules/nf-core/variancepartition/dream/templates/dream.R +++ b/modules/nf-core/variancepartition/dream/templates/dream.R @@ -23,6 +23,65 @@ is_valid_string <- function(input) { !is.null(input) && nzchar(trimws(input)) } +#' Rewrite a contrast expression using sanitised design column names +#' +#' `makeContrasts()` requires syntactically valid coefficient names. The DREAM +#' design matrix is sanitised with `make.names()`, so a user may provide +#' contrasts using the original names (for example containing spaces). This +#' helper rewrites exact design-column matches to their sanitised equivalents. +#' +#' @param contrast_string User-provided contrast expression. +#' @param design_names Original design matrix column names. +#' original_design_names +#' @return Contrast expression compatible with `makeContrasts()`. +normalise_contrast_string <- function(contrast_string, design_names) { + if (!is_valid_string(contrast_string)) { + return(contrast_string) + } + + sanitised_names <- make.names(design_names) + replacement_order <- order(nchar(design_names), decreasing = TRUE) + normalised <- contrast_string + + for (idx in replacement_order) { + normalised <- gsub( + design_names[[idx]], + sanitised_names[[idx]], + normalised, + fixed = TRUE + ) + } + + normalised +} + +#' Resolve user-supplied metadata column names against loaded metadata +#' +#' Metadata is read with `check.names = TRUE`, so columns containing spaces are +#' sanitised by R. This helper accepts either the original column name or the +#' sanitised version and returns the column present in `metadata`. +#' +#' @param column_name Column name provided by the user. +#' @param metadata Loaded metadata data frame. +#' +#' @return Resolved column name present in metadata. +resolve_metadata_column <- function(column_name, metadata) { + if (!is_valid_string(column_name)) { + return(column_name) + } + + if (column_name %in% colnames(metadata)) { + return(column_name) + } + + sanitised_name <- make.names(column_name) + if (sanitised_name %in% colnames(metadata)) { + return(sanitised_name) + } + + column_name +} + #' Parse out options from a string without recourse to optparse #' #' @param x Long-form argument list like --opt1 val1 --opt2 val2 @@ -138,6 +197,8 @@ if (!is.null(opt\$seed)) { # Load metadata metadata <- read_delim_flexible(opt\$sample_file, header = TRUE, stringsAsFactors = TRUE) rownames(metadata) <- metadata[[opt\$sample_id_col]] +contrast_variable <- NULL +blocking.vars <- c() # Check if required parameters have been provided if (is_valid_string(opt\$formula)) { @@ -164,8 +225,7 @@ if (length(missing) > 0) { } if (!is_valid_string(opt\$formula)) { - contrast_variable <- make.names(opt\$contrast_variable) - blocking.vars <- c() + contrast_variable <- resolve_metadata_column(opt\$contrast_variable, metadata) if (!contrast_variable %in% colnames(metadata)) { stop( @@ -184,7 +244,12 @@ if (!is_valid_string(opt\$formula)) { ) ) } else if (is_valid_string(opt\$blocking_variables)) { - blocking.vars = make.names(unlist(strsplit(opt\$blocking_variables, split = ';'))) + blocking.vars <- vapply( + unlist(strsplit(opt\$blocking_variables, split = ';')), + resolve_metadata_column, + character(1), + metadata = metadata + ) if (!all(blocking.vars %in% colnames(metadata))) { missing_block <- paste(blocking.vars[! blocking.vars %in% colnames(metadata)], collapse = ',') stop( @@ -199,8 +264,10 @@ if (!is_valid_string(opt\$formula)) { # Ensure contrast variable is factor, then relevel if (!is.null(opt\$contrast_reference) && opt\$contrast_reference != "") { - metadata[[opt\$contrast_variable]] <- factor(metadata[[opt\$contrast_variable]]) - metadata[[opt\$contrast_variable]] <- relevel(metadata[[opt\$contrast_variable]], ref = opt\$contrast_reference) + if (!is.null(contrast_variable) && contrast_variable %in% colnames(metadata)) { + metadata[[contrast_variable]] <- factor(metadata[[contrast_variable]]) + metadata[[contrast_variable]] <- relevel(metadata[[contrast_variable]], ref = opt\$contrast_reference) + } } # Exclude samples in metadata if specified @@ -256,7 +323,7 @@ if (as.logical(opt\$apply_voom)) { vobjDream <- voomWithDreamWeights(dge, form, metadata, BPPARAM = bp) # Write normalized counts matrix to a TSV file - normalized_counts <- vobjDream\$E + normalized_counts <- vobjDream\$E if (!is.null(opt\$round_digits)) { normalized_counts <- apply(normalized_counts, 2, function(x) round(x, opt\$round_digits)) } @@ -302,7 +369,11 @@ if (!is.null(opt\$contrast_string)) { if (is_valid_string(contrast_string)) { cat("Using contrast string:", contrast_string, "\n") - colnames(fitmm\$design) <- make.names(colnames(fitmm\$design)) + original_design_names <- colnames(fitmm\$design) + colnames(fitmm\$design) <- make.names(original_design_names) + contrast_string <- normalise_contrast_string(contrast_string, original_design_names) + cat("Normalised contrast string:", contrast_string, "\n") + contrast_matrix <- makeContrasts(contrast = contrast_string, levels = colnames(fitmm\$design)) fit2 <- contrasts.fit(fitmm, contrast_matrix) fit2 <- eBayes(fit2, proportion = opt\$proportion, diff --git a/modules/nf-core/variancepartition/dream/tests/main.nf.test b/modules/nf-core/variancepartition/dream/tests/main.nf.test index f807d885db02..8dcc97c65a71 100644 --- a/modules/nf-core/variancepartition/dream/tests/main.nf.test +++ b/modules/nf-core/variancepartition/dream/tests/main.nf.test @@ -41,7 +41,7 @@ nextflow_process { { assert path(process.out.results[0][1]).getText().contains("gene_id\tlogFC\tAveExpr\tt\tP.Value\tadj.P.Val\tB") }, { assert path(process.out.results[0][1]).getText().contains("1309.83") }, { assert path(process.out.results[0][1]).getText().contains("557\t279.88\t18.81") }, - { assert snapshot(process.out.model, process.out.versions).match() } + { assert snapshot(process.out.model, process.out.versions_variancepartition).match() } ) } } @@ -77,7 +77,7 @@ nextflow_process { { assert path(process.out.results[0][1]).getText().contains("gene_id\tlogFC\tAveExpr\tt\tP.Value\tadj.P.Val\tB") }, { assert path(process.out.results[0][1]).getText().contains("5.07") }, { assert path(process.out.results[0][1]).getText().contains("2.97\t3.88\t38.17") }, - { assert snapshot(process.out.model, process.out.normalised_counts, process.out.versions).match() } + { assert snapshot(process.out.model, process.out.normalised_counts, process.out.versions_variancepartition).match() } ) } } @@ -111,7 +111,7 @@ nextflow_process { { assert path(process.out.results[0][1]).getText().contains("gene_id\tlogFC\tAveExpr\tt\tP.Value\tadj.P.Val\tB") }, { assert path(process.out.results[0][1]).getText().contains("-849.67") }, { assert path(process.out.results[0][1]).getText().contains("-1050\t549\t-3.78") }, - { assert snapshot(process.out.model, process.out.versions).match() } + { assert snapshot(process.out.model, process.out.versions_variancepartition).match() } ) } } @@ -146,7 +146,7 @@ nextflow_process { { assert path(process.out.results[0][1]).getText().contains("gene_id\tlogFC\tAveExpr\tt\tP.Value\tadj.P.Val\tB") }, { assert path(process.out.results[0][1]).getText().contains("1050") }, { assert path(process.out.results[0][1]).getText().contains("849.67\t432.83\t3.8") }, - { assert snapshot(process.out.model, process.out.versions).match() } + { assert snapshot(process.out.model, process.out.versions_variancepartition).match() } ) } } @@ -180,7 +180,7 @@ nextflow_process { { assert path(process.out.results[0][1]).getText().contains("gene_id\tlogFC\tAveExpr\tt\tP.Value\tadj.P.Val\tB") }, { assert path(process.out.results[0][1]).getText().contains("2124\t549\t4.84") }, { assert path(process.out.results[0][1]).getText().contains("1707.33") }, - { assert snapshot(process.out.model, process.out.versions).match() } + { assert snapshot(process.out.model, process.out.versions_variancepartition).match() } ) } } @@ -211,7 +211,7 @@ nextflow_process { then { assertAll( { assert process.success }, - { assert snapshot(process.out.model, process.out.versions).match() }, + { assert snapshot(process.out.model, process.out.versions_variancepartition).match() }, { assert path(process.out.results[0][1]).getText().contains("gene_id\tlogFC\tAveExpr\tt\tP.Value\tadj.P.Val\tB") }, { assert path(process.out.results[0][1]).getText().contains("-95.67") }, { assert path(process.out.results[0][1]).getText().contains("1050\t549\t4.16") } From 984cc7fdc3b506d1af5eaabf6e752f22c67f0ec4 Mon Sep 17 00:00:00 2001 From: delfiterradas Date: Fri, 27 Mar 2026 17:48:32 +0100 Subject: [PATCH 2/3] Update snapshots --- .../dream/tests/main.nf.test.snap | 50 +++++++++---------- .../abundance_differential_filter/main.nf | 2 - .../tests/main.nf.test.snap | 23 ++++----- 3 files changed, 35 insertions(+), 40 deletions(-) diff --git a/modules/nf-core/variancepartition/dream/tests/main.nf.test.snap b/modules/nf-core/variancepartition/dream/tests/main.nf.test.snap index 46afa72c1495..a2b053f08148 100644 --- a/modules/nf-core/variancepartition/dream/tests/main.nf.test.snap +++ b/modules/nf-core/variancepartition/dream/tests/main.nf.test.snap @@ -15,11 +15,11 @@ "versions.yml:md5,fc1f26eb2194018e99fc2916332676b7" ] ], - "timestamp": "2025-12-23T18:57:51.598745459", "meta": { "nf-test": "0.9.3", "nextflow": "25.04.2" - } + }, + "timestamp": "2025-12-23T18:57:51.598745459" }, "RNAseq - Feature Counts - formula + comparison contrast string - interaction": { "content": [ @@ -37,11 +37,11 @@ "versions.yml:md5,fc1f26eb2194018e99fc2916332676b7" ] ], - "timestamp": "2025-12-26T15:10:51.980662299", "meta": { "nf-test": "0.9.3", "nextflow": "25.04.2" - } + }, + "timestamp": "2025-12-26T15:10:51.980662299" }, "Mus musculus - contrasts - matrix - no formula": { "content": [ @@ -61,11 +61,11 @@ "versions.yml:md5,fc1f26eb2194018e99fc2916332676b7" ] ], - "timestamp": "2025-12-16T15:52:08.047223666", "meta": { "nf-test": "0.9.3", "nextflow": "25.10.0" - } + }, + "timestamp": "2025-12-16T15:52:08.047223666" }, "Mus musculus - expression table - contrasts + blocking factors": { "content": [ @@ -130,11 +130,11 @@ ] } ], - "timestamp": "2025-12-16T19:59:57.947286403", "meta": { "nf-test": "0.9.3", "nextflow": "25.10.0" - } + }, + "timestamp": "2025-12-16T19:59:57.947286403" }, "Mus musculus - expression table - contrasts + blocking factors stub": { "content": [ @@ -199,11 +199,11 @@ ] } ], - "timestamp": "2025-11-10T16:18:37.294899756", "meta": { "nf-test": "0.9.2", "nextflow": "25.04.6" - } + }, + "timestamp": "2025-11-10T16:18:37.294899756" }, "Mus musculus - expression table - contrasts + formula + weighted comparison contrast string": { "content": [ @@ -221,11 +221,11 @@ "versions.yml:md5,fc1f26eb2194018e99fc2916332676b7" ] ], - "timestamp": "2025-12-23T18:57:42.761838155", "meta": { "nf-test": "0.9.3", "nextflow": "25.04.2" - } + }, + "timestamp": "2025-12-23T18:57:42.761838155" }, "Mus musculus - expression table - contrasts + formula + comparison contrast string - no intercept stub": { "content": [ @@ -279,16 +279,16 @@ "treatment_mCherry_hND6.dream.results.tsv:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "versions": [ + "versions_variancepartition": [ "versions.yml:md5,03b686ec8c67a91501ebb2b2a5234e77" ] } ], - "timestamp": "2026-01-13T15:35:07.121696674", "meta": { "nf-test": "0.9.3", - "nextflow": "25.04.2" - } + "nextflow": "25.10.3" + }, + "timestamp": "2026-03-27T17:37:18.306234608" }, "RNAseq - Voom - Feature Counts - formula + comparison contrast string - interaction": { "content": [ @@ -316,11 +316,11 @@ "versions.yml:md5,fc1f26eb2194018e99fc2916332676b7" ] ], - "timestamp": "2026-01-13T15:53:31.743589111", "meta": { "nf-test": "0.9.3", "nextflow": "25.04.2" - } + }, + "timestamp": "2026-01-13T15:53:31.743589111" }, "RNAseq - Feature Counts - formula + comparison contrast string - interaction with seed": { "content": [ @@ -374,16 +374,16 @@ "genotype_WT_KO_treatment_Control_Treated.dream.results.tsv:md5,90a06e6b4945c706025582a4c9fddf2c" ] ], - "versions": [ + "versions_variancepartition": [ "versions.yml:md5,fc1f26eb2194018e99fc2916332676b7" ] } ], - "timestamp": "2026-03-25T21:03:52.327619963", "meta": { - "nf-test": "0.9.5", - "nextflow": "25.10.4" - } + "nf-test": "0.9.3", + "nextflow": "25.10.3" + }, + "timestamp": "2026-03-27T17:37:36.063080982" }, "Mus musculus - expression table - contrasts + formula + comparison contrast string": { "content": [ @@ -401,10 +401,10 @@ "versions.yml:md5,fc1f26eb2194018e99fc2916332676b7" ] ], - "timestamp": "2025-12-26T15:11:01.472042429", "meta": { "nf-test": "0.9.3", "nextflow": "25.04.2" - } + }, + "timestamp": "2025-12-26T15:11:01.472042429" } } \ No newline at end of file diff --git a/subworkflows/nf-core/abundance_differential_filter/main.nf b/subworkflows/nf-core/abundance_differential_filter/main.nf index 923dfe28c5e1..caac84aa86ca 100644 --- a/subworkflows/nf-core/abundance_differential_filter/main.nf +++ b/subworkflows/nf-core/abundance_differential_filter/main.nf @@ -172,8 +172,6 @@ workflow ABUNDANCE_DIFFERENTIAL_FILTER { inputs.samples_and_matrix.filter{index -> index[0].differential_method == 'dream' } ) - ch_versions = ch_versions.mix( VARIANCEPARTITION_DREAM.out.versions.first() ) - // ---------------------------------------------------- // Collect results // ---------------------------------------------------- diff --git a/subworkflows/nf-core/abundance_differential_filter/tests/main.nf.test.snap b/subworkflows/nf-core/abundance_differential_filter/tests/main.nf.test.snap index f4bfb492b35b..8495f8ec5851 100644 --- a/subworkflows/nf-core/abundance_differential_filter/tests/main.nf.test.snap +++ b/subworkflows/nf-core/abundance_differential_filter/tests/main.nf.test.snap @@ -390,15 +390,14 @@ ] ], [ - "versions.yml:md5,1c02d4e455e8f3809c8ce37bee947690", - "versions.yml:md5,736da31f06f854355d45aeb9d9c874e0" + "versions.yml:md5,1c02d4e455e8f3809c8ce37bee947690" ] ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.10.4" + "nf-test": "0.9.3", + "nextflow": "25.10.3" }, - "timestamp": "2026-03-23T16:09:15.466767644" + "timestamp": "2026-03-27T17:43:27.437927044" }, "deseq2 + limma-voom + propd - mouse - basic": { "content": [ @@ -1139,15 +1138,14 @@ ] ], [ - "versions.yml:md5,1c02d4e455e8f3809c8ce37bee947690", - "versions.yml:md5,736da31f06f854355d45aeb9d9c874e0" + "versions.yml:md5,1c02d4e455e8f3809c8ce37bee947690" ] ], "meta": { "nf-test": "0.9.3", - "nextflow": "25.04.2" + "nextflow": "25.10.3" }, - "timestamp": "2026-01-16T17:04:34.296086424" + "timestamp": "2026-03-27T17:44:53.721889002" }, "deseq2 - mouse - basic": { "content": [ @@ -1302,15 +1300,14 @@ ] ], [ - "versions.yml:md5,1c02d4e455e8f3809c8ce37bee947690", - "versions.yml:md5,736da31f06f854355d45aeb9d9c874e0" + "versions.yml:md5,1c02d4e455e8f3809c8ce37bee947690" ] ], "meta": { "nf-test": "0.9.3", - "nextflow": "25.04.2" + "nextflow": "25.10.3" }, - "timestamp": "2025-12-23T19:05:11.641088832" + "timestamp": "2026-03-27T17:45:07.123499332" }, "deseq2 - with transcript lengths": { "content": [ From a87cf020d0acc0d7d0951f25a557f692e8bfd6c6 Mon Sep 17 00:00:00 2001 From: Delfina Terradas <155591053+delfiterradas@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:04:09 -0300 Subject: [PATCH 3/3] Fix linting error --- modules/nf-core/variancepartition/dream/meta.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/nf-core/variancepartition/dream/meta.yml b/modules/nf-core/variancepartition/dream/meta.yml index dd5b37c0132b..8e7d0f263173 100644 --- a/modules/nf-core/variancepartition/dream/meta.yml +++ b/modules/nf-core/variancepartition/dream/meta.yml @@ -121,4 +121,4 @@ versions: description: File containing software versions pattern: "versions.yml" ontologies: - - edam: http://edamontology.org/format_3750 # YAML \ No newline at end of file + - edam: http://edamontology.org/format_3750 # YAML