From d1122b8cb7bf71be4ef52e0fa76abdda0a08279c Mon Sep 17 00:00:00 2001 From: delfiterradas Date: Fri, 27 Mar 2026 16:32:31 +0000 Subject: [PATCH 1/2] Accept spaces in variable names and migrate to topic channels --- modules/nf-core/deseq2/differential/main.nf | 2 +- modules/nf-core/deseq2/differential/meta.yml | 12 +++ .../templates/deseq2_differential.R | 85 +++++++++++++++++-- .../deseq2/differential/tests/main.nf.test | 24 +++--- 4 files changed, 104 insertions(+), 19 deletions(-) diff --git a/modules/nf-core/deseq2/differential/main.nf b/modules/nf-core/deseq2/differential/main.nf index 3054ae1fbbca..85449931e304 100644 --- a/modules/nf-core/deseq2/differential/main.nf +++ b/modules/nf-core/deseq2/differential/main.nf @@ -24,7 +24,7 @@ process DESEQ2_DIFFERENTIAL { tuple val(meta), path("*.vst.tsv") , optional: true, emit: vst_counts tuple val(meta), path("*.deseq2.model.txt") , emit: model tuple val(meta), path("*.R_sessionInfo.log") , emit: session_info - path "versions.yml" , emit: versions + path "versions.yml" , emit: versions_deseq2, topic: versions when: task.ext.when == null || task.ext.when diff --git a/modules/nf-core/deseq2/differential/meta.yml b/modules/nf-core/deseq2/differential/meta.yml index 757e23549555..4169f1e2f494 100644 --- a/modules/nf-core/deseq2/differential/meta.yml +++ b/modules/nf-core/deseq2/differential/meta.yml @@ -195,6 +195,14 @@ output: description: Dump of R SessionInfo pattern: "*.R_sessionInfo.log" ontologies: [] + versions_deseq2: + - 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 @@ -206,3 +214,7 @@ authors: - "@pinin4fjords" maintainers: - "@pinin4fjords" +versions: + - versions.yml: + type: file + description: YAML file containing versions of tools used in the module diff --git a/modules/nf-core/deseq2/differential/templates/deseq2_differential.R b/modules/nf-core/deseq2/differential/templates/deseq2_differential.R index 9f1af35b532e..cf77c247479a 100755 --- a/modules/nf-core/deseq2/differential/templates/deseq2_differential.R +++ b/modules/nf-core/deseq2/differential/templates/deseq2_differential.R @@ -22,6 +22,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 @@ -192,7 +251,11 @@ if ( ! is.null(opt\$round_digits)){ } # If there is no option supplied, convert string "null" to NULL -keys <- c("formula", "contrast_string", "contrast_variable", "reference_level", "target_level", "seed", "blocking_variable", "transcript_lengths_file") +keys <- c( + "formula", "contrast_string", "contrast_variable", "reference_level", + "target_level", "seed", "blocking_variables", "transcript_lengths_file", + "exclude_samples_col", "exclude_samples_values" +) opt[keys] <- lapply(opt[keys], nullify) if ( ! is.null(opt\$seed)){ @@ -286,9 +349,11 @@ if (length(missing_samples) > 0) { ## CHECK CONTRAST SPECIFICATION ## ################################################ ################################################ +contrast_variable <- NULL +blocking.vars <- c() + if (! is_valid_string(opt\$formula)) { - contrast_variable <- make.names(opt\$contrast_variable) - blocking.vars <- c() + contrast_variable <- resolve_metadata_column(opt\$contrast_variable, sample.sheet) if (!contrast_variable %in% colnames(sample.sheet)) { stop( @@ -307,7 +372,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 = sample.sheet + ) if (!all(blocking.vars %in% colnames(sample.sheet))) { missing_block <- paste(blocking.vars[! blocking.vars %in% colnames(sample.sheet)], collapse = ',') stop( @@ -435,9 +505,12 @@ if (!is.null(opt\$contrast_string)) { } else { # Parse as limma-style contrast expression design_mat <- model.matrix(as.formula(model), data = as.data.frame(colData(dds))) - colnames(design_mat) <- make.names(colnames(design_mat)) + original_design_names <- colnames(design_mat) + colnames(design_mat) <- make.names(original_design_names) + contrast_string <- normalise_contrast_string(opt\$contrast_string, original_design_names) + message("Normalised contrast string: ", contrast_string) numeric_contrast <- as.numeric( - limma::makeContrasts(contrasts = opt\$contrast_string, levels = colnames(design_mat)) + limma::makeContrasts(contrasts = contrast_string, levels = colnames(design_mat)) ) # Run DESeq2 results with numeric contrast diff --git a/modules/nf-core/deseq2/differential/tests/main.nf.test b/modules/nf-core/deseq2/differential/tests/main.nf.test index ff3e579e684a..2bdd78ccf023 100644 --- a/modules/nf-core/deseq2/differential/tests/main.nf.test +++ b/modules/nf-core/deseq2/differential/tests/main.nf.test @@ -48,7 +48,7 @@ nextflow_process { process.out.rlog_counts, process.out.vst_counts, process.out.model, - process.out.versions, + process.out.versions_deseq2, file(process.out.dispersion_plot_png[0][1]).name, file(process.out.dispersion_plot_pdf[0][1]).name, file(process.out.rdata[0][1]).name @@ -97,7 +97,7 @@ nextflow_process { process.out.rlog_counts, process.out.vst_counts, process.out.model, - process.out.versions, + process.out.versions_deseq2, file(process.out.dispersion_plot_png[0][1]).name, file(process.out.dispersion_plot_pdf[0][1]).name, file(process.out.rdata[0][1]).name @@ -146,7 +146,7 @@ nextflow_process { process.out.rlog_counts, process.out.vst_counts, process.out.model, - process.out.versions, + process.out.versions_deseq2, file(process.out.dispersion_plot_png[0][1]).name, file(process.out.dispersion_plot_pdf[0][1]).name, file(process.out.rdata[0][1]).name @@ -195,7 +195,7 @@ nextflow_process { process.out.rlog_counts, process.out.vst_counts, process.out.model, - process.out.versions, + process.out.versions_deseq2, file(process.out.dispersion_plot_png[0][1]).name, file(process.out.dispersion_plot_pdf[0][1]).name, file(process.out.rdata[0][1]).name @@ -249,7 +249,7 @@ nextflow_process { process.out.rlog_counts, process.out.vst_counts, process.out.model, - process.out.versions, + process.out.versions_deseq2, file(process.out.dispersion_plot_png[0][1]).name, file(process.out.dispersion_plot_pdf[0][1]).name, file(process.out.rdata[0][1]).name @@ -299,7 +299,7 @@ nextflow_process { process.out.rlog_counts, process.out.vst_counts, process.out.model, - process.out.versions, + process.out.versions_deseq2, file(process.out.dispersion_plot_png[0][1]).name, file(process.out.dispersion_plot_pdf[0][1]).name, file(process.out.rdata[0][1]).name @@ -351,7 +351,7 @@ nextflow_process { process.out.rlog_counts, process.out.vst_counts, process.out.model, - process.out.versions, + process.out.versions_deseq2, file(process.out.dispersion_plot_png[0][1]).name, file(process.out.dispersion_plot_pdf[0][1]).name, file(process.out.rdata[0][1]).name @@ -403,7 +403,7 @@ nextflow_process { process.out.rlog_counts, process.out.vst_counts, process.out.model, - process.out.versions, + process.out.versions_deseq2, file(process.out.dispersion_plot_png[0][1]).name, file(process.out.dispersion_plot_pdf[0][1]).name, file(process.out.rdata[0][1]).name @@ -458,7 +458,7 @@ nextflow_process { process.out.rlog_counts, process.out.vst_counts, process.out.model, - process.out.versions, + process.out.versions_deseq2, file(process.out.dispersion_plot_png[0][1]).name, file(process.out.dispersion_plot_pdf[0][1]).name, file(process.out.rdata[0][1]).name @@ -507,7 +507,7 @@ nextflow_process { process.out.rlog_counts, process.out.vst_counts, process.out.model, - process.out.versions, + process.out.versions_deseq2, file(process.out.dispersion_plot_png[0][1]).name, file(process.out.dispersion_plot_pdf[0][1]).name, file(process.out.rdata[0][1]).name @@ -556,7 +556,7 @@ nextflow_process { process.out.rlog_counts, process.out.vst_counts, process.out.model, - process.out.versions, + process.out.versions_deseq2, file(process.out.dispersion_plot_png[0][1]).name, file(process.out.dispersion_plot_pdf[0][1]).name, file(process.out.rdata[0][1]).name @@ -605,7 +605,7 @@ nextflow_process { process.out.rlog_counts, process.out.vst_counts, process.out.model, - process.out.versions, + process.out.versions_deseq2, file(process.out.dispersion_plot_png[0][1]).name, file(process.out.dispersion_plot_pdf[0][1]).name, file(process.out.rdata[0][1]).name From 060b7c092bfcf63cce21ab0151cc77b1a8aadcf3 Mon Sep 17 00:00:00 2001 From: delfiterradas Date: Fri, 27 Mar 2026 18:47:26 +0100 Subject: [PATCH 2/2] Update snapshots --- .../differential/tests/main.nf.test.snap | 6 +-- .../abundance_differential_filter/main.nf | 4 -- .../tests/main.nf.test.snap | 46 +++++++------------ 3 files changed, 20 insertions(+), 36 deletions(-) diff --git a/modules/nf-core/deseq2/differential/tests/main.nf.test.snap b/modules/nf-core/deseq2/differential/tests/main.nf.test.snap index de6a362d82d6..18ff980717f8 100644 --- a/modules/nf-core/deseq2/differential/tests/main.nf.test.snap +++ b/modules/nf-core/deseq2/differential/tests/main.nf.test.snap @@ -1719,7 +1719,7 @@ "treatment_mCherry_hND6_sample_number.deseq2.sizefactors.tsv:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - "versions": [ + "versions_deseq2": [ "versions.yml:md5,2d67217d8adb1fa0d143b4017e6657fe", "versions.yml:md5,2d67217d8adb1fa0d143b4017e6657fe" ], @@ -1730,8 +1730,8 @@ ], "meta": { "nf-test": "0.9.3", - "nextflow": "25.04.2" + "nextflow": "25.10.3" }, - "timestamp": "2026-01-20T14:57:21.703291049" + "timestamp": "2026-03-27T18:06:42.719478946" } } \ 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..ebd2b066360c 100644 --- a/subworkflows/nf-core/abundance_differential_filter/main.nf +++ b/subworkflows/nf-core/abundance_differential_filter/main.nf @@ -121,8 +121,6 @@ workflow ABUNDANCE_DIFFERENTIAL_FILTER { norm_inputs.transcript_length.filter{index -> index[0].differential_method == 'deseq2'} ) - ch_versions = ch_versions.mix(DESEQ2_NORM.out.versions.first()) - DESEQ2_DIFFERENTIAL( inputs.contrasts_for_diff_with_formula.filter{index -> index[0].differential_method == 'deseq2'}, inputs.samples_and_matrix.filter{index -> index[0].differential_method == 'deseq2'}, @@ -130,8 +128,6 @@ workflow ABUNDANCE_DIFFERENTIAL_FILTER { inputs.transcript_length.filter{index -> index[0].differential_method == 'deseq2'} ) - ch_versions = ch_versions.mix(DESEQ2_DIFFERENTIAL.out.versions.first()) - // ---------------------------------------------------- // Run propd // ---------------------------------------------------- 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..3add07d5068b 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 @@ -262,18 +262,16 @@ "treatment_mCherry_hND6_sample_number_test_limma_voom.R_sessionInfo.log" ], [ - "versions.yml:md5,1a6a400c49aa4dda7ec5c4ed0cc56340", "versions.yml:md5,1c02d4e455e8f3809c8ce37bee947690", "versions.yml:md5,1ddaab440e2528c688c05a02dd066f12", - "versions.yml:md5,2c0576aefff8da32c7c0cfd8529aa4b5", "versions.yml:md5,b80e2c320ea0429466a7b7c3c3ac78fa" ] ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.10.4" + "nf-test": "0.9.3", + "nextflow": "25.10.3" }, - "timestamp": "2026-03-24T15:47:16.379223242" + "timestamp": "2026-03-27T18:12:02.032571635" }, "limma - voom": { "content": [ @@ -672,19 +670,17 @@ "treatment_mCherry_hND6_sample_number_test_limma_voom.R_sessionInfo.log" ], [ - "versions.yml:md5,1a6a400c49aa4dda7ec5c4ed0cc56340", "versions.yml:md5,1c02d4e455e8f3809c8ce37bee947690", "versions.yml:md5,1ddaab440e2528c688c05a02dd066f12", - "versions.yml:md5,2c0576aefff8da32c7c0cfd8529aa4b5", "versions.yml:md5,b80e2c320ea0429466a7b7c3c3ac78fa", "versions.yml:md5,ff5b7c1d83470f6f548f3643bb37a830" ] ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.10.4" + "nf-test": "0.9.3", + "nextflow": "25.10.3" }, - "timestamp": "2026-03-24T15:48:06.008809772" + "timestamp": "2026-03-27T18:12:28.325564701" }, "stub": { "content": [ @@ -766,8 +762,6 @@ ] ], "11": [ - "versions.yml:md5,05e3901f6d78f8839a7e07f422e9bc03", - "versions.yml:md5,1d567f203085b6ae7b621d5587260a23", "versions.yml:md5,50cd86004ca6259274b10316b1b96f00" ], "2": [ @@ -1091,17 +1085,15 @@ ] ], "versions": [ - "versions.yml:md5,05e3901f6d78f8839a7e07f422e9bc03", - "versions.yml:md5,1d567f203085b6ae7b621d5587260a23", "versions.yml:md5,50cd86004ca6259274b10316b1b96f00" ] } ], "meta": { - "nf-test": "0.9.2", - "nextflow": "25.10.4" + "nf-test": "0.9.3", + "nextflow": "25.10.3" }, - "timestamp": "2026-03-24T15:49:19.347075595" + "timestamp": "2026-03-27T18:12:42.759872833" }, "dream - voom": { "content": [ @@ -1264,16 +1256,14 @@ "treatment_mCherry_hND6_sample_number_test_deseq2.R_sessionInfo.log" ], [ - "versions.yml:md5,1a6a400c49aa4dda7ec5c4ed0cc56340", - "versions.yml:md5,1c02d4e455e8f3809c8ce37bee947690", - "versions.yml:md5,2c0576aefff8da32c7c0cfd8529aa4b5" + "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-24T15:32:07.906913925" + "timestamp": "2026-03-27T18:10:09.656585499" }, "dream - complex contrast - literal contrast string comparison": { "content": [ @@ -1427,16 +1417,14 @@ "treatment_mCherry_hND6_sample_number_test_deseq2.R_sessionInfo.log" ], [ - "versions.yml:md5,1a6a400c49aa4dda7ec5c4ed0cc56340", - "versions.yml:md5,1c02d4e455e8f3809c8ce37bee947690", - "versions.yml:md5,2c0576aefff8da32c7c0cfd8529aa4b5" + "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-24T15:46:27.915203315" + "timestamp": "2026-03-27T18:11:21.481800371" }, "propd - mouse - basic": { "content": [