From 91cd6e8d45ca7ce754cc73725b127d371e8188fc Mon Sep 17 00:00:00 2001 From: Shen-YuFei Date: Tue, 15 Sep 2026 05:24:17 -0400 Subject: [PATCH 1/4] fix(input): allow empty fixed modifications from SDRF --- nextflow_schema.json | 2 +- .../local/create_input_channel/main.nf | 11 ++-- .../create_input_channel/tests/main.nf.test | 59 +++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 subworkflows/local/create_input_channel/tests/main.nf.test diff --git a/nextflow_schema.json b/nextflow_schema.json index f1ba2051..58f21764 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -19,7 +19,7 @@ "mimetype": "text/csv", "pattern": "^\\S+\\.(?:csv|tsv|sdrf)$", "description": "URI/path to an SDRF file in SDRF format with .sdrf, .tsv, or .csv extension. For more info see help text or docs.", - "help_text": "Input is specified by using a path or URI to a PRIDE Sample to Data Relation Format file (SDRF), e.g. as part of a submitted and annotated PRIDE experiment (see [here](https://github.com/bigbio/proteomics-metadata-standard/tree/master/annotated-projects) for examples). Input files will be downloaded and cached from the URIs specified in the SDRF file.\n\nThe SDRF file can have .sdrf, .tsv, or .csv extensions. An OpenMS-style experimental design will be generated based on the factor columns of the SDRF.\n\nThe following parameters are read **exclusively** from the SDRF file (required columns):\n\n * `acquisition_method` (from 'Proteomics Data Acquisition Method' column),\n * `labelling_type` (from 'Label' column),\n * `enzyme` (from 'Enzyme' column),\n * `fixed_mods` (from 'FixedModifications' column)\n\nThe following parameters are read from SDRF but can be **overridden via command line**:\n\n * `precursor_mass_tolerance` (from 'PrecursorMassTolerance' column),\n * `precursor_mass_tolerance_unit` (from 'PrecursorMassToleranceUnit' column),\n * `fragment_mass_tolerance` (from 'FragmentMassTolerance' column),\n * `fragment_mass_tolerance_unit` (from 'FragmentMassToleranceUnit' column),\n * `variable_mods` (from 'VariableModifications' column)", + "help_text": "Input is specified by using a path or URI to a PRIDE Sample to Data Relation Format file (SDRF), e.g. as part of a submitted and annotated PRIDE experiment (see [here](https://github.com/bigbio/proteomics-metadata-standard/tree/master/annotated-projects) for examples). Input files will be downloaded and cached from the URIs specified in the SDRF file.\n\nThe SDRF file can have .sdrf, .tsv, or .csv extensions. An OpenMS-style experimental design will be generated based on the factor columns of the SDRF.\n\nThe following parameters are read **exclusively** from the SDRF file (required columns):\n\n * `acquisition_method` (from 'Proteomics Data Acquisition Method' column),\n * `labelling_type` (from 'Label' column),\n * `enzyme` (from 'Enzyme' column)\n\nFixed modifications are also read exclusively from the SDRF ('FixedModifications' column). An empty set is supported and does not add a default fixed modification. When using MS-GF+, at least one fixed or variable modification must remain after applying the variable-modification fallback, because OpenMS MSGFPlusAdapter otherwise enables fixed Carbamidomethyl (C). Use Comet or Sage for a search without modifications.\n\nThe following parameters are read from SDRF but can be **overridden via command line**:\n\n * `precursor_mass_tolerance` (from 'PrecursorMassTolerance' column),\n * `precursor_mass_tolerance_unit` (from 'PrecursorMassToleranceUnit' column),\n * `fragment_mass_tolerance` (from 'FragmentMassTolerance' column),\n * `fragment_mass_tolerance_unit` (from 'FragmentMassToleranceUnit' column),\n * `variable_mods` (from 'VariableModifications' column)", "fa_icon": "fas fa-file-csv" }, "outdir": { diff --git a/subworkflows/local/create_input_channel/main.nf b/subworkflows/local/create_input_channel/main.nf index 64790355..9690b8bd 100644 --- a/subworkflows/local/create_input_channel/main.nf +++ b/subworkflows/local/create_input_channel/main.nf @@ -105,8 +105,7 @@ def create_meta_channel(LinkedHashMap row, enzymes, files, wrapper) { // Validate required SDRF columns - these parameters are exclusively read from SDRF (no command-line override) def requiredColumns = [ 'Label': row.Label, - 'Enzyme': row.Enzyme, - 'FixedModifications': row.FixedModifications + 'Enzyme': row.Enzyme ] def missingColumns = [] @@ -122,9 +121,9 @@ def create_meta_channel(LinkedHashMap row, enzymes, files, wrapper) { exit(1) } - // Set values from SDRF (required columns) + // An empty fixed-modification set must remain an empty string for the search adapters. meta.labelling_type = row.Label - meta.fixedmodifications = row.FixedModifications + meta.fixedmodifications = row.FixedModifications?.toString()?.trim() ?: '' meta.enzyme = row.Enzyme // Set tolerance values: use SDRF if available, otherwise fall back to params @@ -183,6 +182,10 @@ def create_meta_channel(LinkedHashMap row, enzymes, files, wrapper) { meta.variablemodifications = params.variable_mods } + if (params.search_engines.contains('msgf') && !meta.fixedmodifications && !meta.variablemodifications) { + exit(1, "ERROR: Both modification lists are empty for '${filestr}', but OpenMS MSGFPlusAdapter would enable fixed Carbamidomethyl (C) in this case. Use Comet or Sage for a search without modifications.") + } + enzymes += row.Enzyme if (enzymes.size() > 1) { log.error("Currently only one enzyme is supported for the whole experiment. Specified was '${enzymes}'. Check or split your SDRF.") diff --git a/subworkflows/local/create_input_channel/tests/main.nf.test b/subworkflows/local/create_input_channel/tests/main.nf.test new file mode 100644 index 00000000..6b98f75a --- /dev/null +++ b/subworkflows/local/create_input_channel/tests/main.nf.test @@ -0,0 +1,59 @@ +nextflow_function { + + name "SDRF search modification metadata" + script "../main.nf" + function "create_meta_channel" + tag "create_input_channel" + + [ + [name: 'empty fixed set', fixed: '', variable: 'Carbamidomethyl (C),Oxidation (M),Propionamide (C)'], + [name: 'missing fixed set', fixed: null, variable: 'Oxidation (M)'], + [name: 'whitespace fixed set', fixed: ' ', variable: 'Carbamidomethyl (C),Oxidation (M),Acetyl (Protein N-term)'], + [name: 'nonempty fixed set', fixed: 'Carbamidomethyl (C)', variable: 'Oxidation (M)'], + [name: 'variable fallback', fixed: '', variable: null], + [name: 'no modifications with Comet', fixed: '', variable: '', fallback: '', engines: 'comet'], + [name: 'no modifications with Sage', fixed: '', variable: '', fallback: '', engines: 'sage'], + [name: 'reject MSGF implicit CAM', fixed: '', variable: '', fallback: '', error: 'Both modification lists are empty'], + [name: 'require label', fixed: '', variable: 'Oxidation (M)', label: '', error: 'Missing or empty required SDRF columns'], + [name: 'require enzyme', fixed: '', variable: 'Oxidation (M)', enzyme: '', error: 'Missing or empty required SDRF columns'], + ].each { example -> + test(example.name) { + when { + params { + root_folder = null + search_engines = example.engines ?: 'comet,msgf,sage' + variable_mods = example.containsKey('fallback') ? example.fallback : 'Oxidation (M)' + } + function { + """ + input[0] = [ + URI: "${projectDir}/main.nf", + 'Proteomics Data Acquisition Method': 'data-dependent acquisition', + DissociationMethod: 'BEAM-TYPE COLLISION-INDUCED DISSOCIATION', + Label: ${(example.containsKey('label') ? example.label : 'label free sample').inspect()}, + Enzyme: ${(example.containsKey('enzyme') ? example.enzyme : 'Trypsin').inspect()}, + FixedModifications: ${example.fixed.inspect()}, + VariableModifications: ${example.variable.inspect()} + ] + input[1] = [] as Set + input[2] = [] as Set + input[3] = [labelling_type: '', experiment_id: 'test.sdrf.tsv'] + """ + } + } + then { + if (example.error) { + assert function.failed + assert function.stdout.any { it.contains(example.error) } + } else { + assert function.success + assert function.result[0].fixedmodifications == (example.fixed?.trim() ?: '') + def expectedVariable = example.variable ?: (example.containsKey('fallback') ? example.fallback : 'Oxidation (M)') + assert function.result[0].variablemodifications == expectedVariable + assert function.result[0].enzyme == 'Trypsin' + assert function.result[0].labelling_type == 'label free sample' + } + } + } + } +} From f66644f07d066e8481880cabed5b711996cc1b6a Mon Sep 17 00:00:00 2001 From: Shen-YuFei Date: Wed, 16 Sep 2026 03:50:08 -0400 Subject: [PATCH 2/4] fix(hooks): make pipeline output guard executable --- .hooks/block_pipeline_outdir.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .hooks/block_pipeline_outdir.sh diff --git a/.hooks/block_pipeline_outdir.sh b/.hooks/block_pipeline_outdir.sh old mode 100644 new mode 100755 From edaab787ec65846c01bf4000177d0cf841cd9468 Mon Sep 17 00:00:00 2001 From: Shen-YuFei Date: Wed, 16 Sep 2026 03:50:25 -0400 Subject: [PATCH 3/4] docs(input): document empty SDRF modification handling --- CHANGELOG.md | 6 ++++++ docs/usage.md | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49cab941..f07c47bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### `Fixed` + +- Allow an empty fixed-modification set from SDRF without adding a default modification. Reject searches using MS-GF+ when both modification sets remain empty after the variable-modification fallback, to prevent OpenMS MSGFPlusAdapter from silently enabling fixed Carbamidomethyl (C). + ## [1.10.0] bigbio/quantms - [15/08/2026] - [London] ### `Changed` diff --git a/docs/usage.md b/docs/usage.md index bc0f7417..471a4a10 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -16,6 +16,14 @@ nextflow run bigbio/quantms --input '/url/path/to/your/experiment_design.sdrf.ts The input file must be in [Sample-to-data-relationship format (SDRF)](https://pubs.acs.org/doi/abs/10.1021/acs.jproteome.0c00376) and can have `.sdrf`, `.tsv`, or `.csv` file extensions. +### Search modifications from SDRF + +Fixed modifications are read exclusively from the SDRF modification annotations. If the parsed `FixedModifications` value is empty, quantms preserves an empty fixed-modification set instead of rejecting the input or adding a default modification such as Carbamidomethyl (C). Annotate modifications according to the experimental protocol; a modification that is variable must not be declared fixed just to make the input pass validation. + +Variable modifications are read from the SDRF when present. If the parsed variable-modification set is empty, quantms uses `--variable_mods`, whose default is `Oxidation (M)`. An empty fixed-modification set with a nonempty variable-modification set is supported with Comet, MS-GF+, and Sage. + +For a search without either fixed or variable modifications, select Comet and/or Sage and set `variable_mods` to an empty string. When `--search_engines` includes `msgf`, quantms rejects an empty final set of both modification types: the current OpenMS MSGFPlusAdapter would otherwise enable fixed Carbamidomethyl (C). This check runs after the variable-modification fallback has been applied. + ### Supported file formats The pipeline supports the following mass spectrometry data file formats: From 9978ecf6bee47519a24edfdba87406a53fe5e5df Mon Sep 17 00:00:00 2001 From: Shen-YuFei Date: Wed, 16 Sep 2026 04:22:27 -0400 Subject: [PATCH 4/4] fix(input): normalize variable modifications before validation --- CHANGELOG.md | 1 + docs/usage.md | 2 +- .../local/create_input_channel/main.nf | 5 +- .../create_input_channel/tests/main.nf.test | 48 +++++++++++++++++-- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f07c47bf..6e0d9693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### `Fixed` - Allow an empty fixed-modification set from SDRF without adding a default modification. Reject searches using MS-GF+ when both modification sets remain empty after the variable-modification fallback, to prevent OpenMS MSGFPlusAdapter from silently enabling fixed Carbamidomethyl (C). +- Trim the selected variable-modification value before validation so a whitespace-only fallback cannot bypass the MS-GF+ empty-modification check. ## [1.10.0] bigbio/quantms - [15/08/2026] - [London] diff --git a/docs/usage.md b/docs/usage.md index 471a4a10..d416ac90 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -20,7 +20,7 @@ The input file must be in [Sample-to-data-relationship format (SDRF)](https://pu Fixed modifications are read exclusively from the SDRF modification annotations. If the parsed `FixedModifications` value is empty, quantms preserves an empty fixed-modification set instead of rejecting the input or adding a default modification such as Carbamidomethyl (C). Annotate modifications according to the experimental protocol; a modification that is variable must not be declared fixed just to make the input pass validation. -Variable modifications are read from the SDRF when present. If the parsed variable-modification set is empty, quantms uses `--variable_mods`, whose default is `Oxidation (M)`. An empty fixed-modification set with a nonempty variable-modification set is supported with Comet, MS-GF+, and Sage. +Variable modifications are read from the SDRF when present. If the parsed variable-modification set is empty or contains only whitespace, quantms uses `--variable_mods`, whose default is `Oxidation (M)`. Leading and trailing whitespace is removed from the selected SDRF or fallback value before validation; a whitespace-only fallback is treated as an empty set. An empty fixed-modification set with a nonempty variable-modification set is supported with Comet, MS-GF+, and Sage. For a search without either fixed or variable modifications, select Comet and/or Sage and set `variable_mods` to an empty string. When `--search_engines` includes `msgf`, quantms rejects an empty final set of both modification types: the current OpenMS MSGFPlusAdapter would otherwise enable fixed Carbamidomethyl (C). This check runs after the variable-modification fallback has been applied. diff --git a/subworkflows/local/create_input_channel/main.nf b/subworkflows/local/create_input_channel/main.nf index 9690b8bd..878fcbbd 100644 --- a/subworkflows/local/create_input_channel/main.nf +++ b/subworkflows/local/create_input_channel/main.nf @@ -181,9 +181,12 @@ def create_meta_channel(LinkedHashMap row, enzymes, files, wrapper) { } else { meta.variablemodifications = params.variable_mods } + meta.variablemodifications = meta.variablemodifications?.toString()?.trim() ?: '' if (params.search_engines.contains('msgf') && !meta.fixedmodifications && !meta.variablemodifications) { - exit(1, "ERROR: Both modification lists are empty for '${filestr}', but OpenMS MSGFPlusAdapter would enable fixed Carbamidomethyl (C) in this case. Use Comet or Sage for a search without modifications.") + exit(1, "ERROR: Both modification lists are empty for '${filestr}', " + + 'but OpenMS MSGFPlusAdapter would enable fixed Carbamidomethyl (C) in this case. ' + + 'Use Comet or Sage for a search without modifications.') } enzymes += row.Enzyme diff --git a/subworkflows/local/create_input_channel/tests/main.nf.test b/subworkflows/local/create_input_channel/tests/main.nf.test index 6b98f75a..3f093d16 100644 --- a/subworkflows/local/create_input_channel/tests/main.nf.test +++ b/subworkflows/local/create_input_channel/tests/main.nf.test @@ -8,14 +8,50 @@ nextflow_function { [ [name: 'empty fixed set', fixed: '', variable: 'Carbamidomethyl (C),Oxidation (M),Propionamide (C)'], [name: 'missing fixed set', fixed: null, variable: 'Oxidation (M)'], - [name: 'whitespace fixed set', fixed: ' ', variable: 'Carbamidomethyl (C),Oxidation (M),Acetyl (Protein N-term)'], + [ + name: 'whitespace fixed set', fixed: ' ', + variable: 'Carbamidomethyl (C),Oxidation (M),Acetyl (Protein N-term)' + ], [name: 'nonempty fixed set', fixed: 'Carbamidomethyl (C)', variable: 'Oxidation (M)'], [name: 'variable fallback', fixed: '', variable: null], + [ + name: 'trim SDRF variable set', fixed: '', variable: ' Oxidation (M) ', + fallback: 'Acetyl (Protein N-term)', expectedVariable: 'Oxidation (M)' + ], + [ + name: 'trim variable fallback', fixed: '', variable: null, + fallback: ' Oxidation (M) ', expectedVariable: 'Oxidation (M)' + ], + [ + name: 'whitespace SDRF uses fallback', fixed: '', variable: ' ', + expectedVariable: 'Oxidation (M)' + ], [name: 'no modifications with Comet', fixed: '', variable: '', fallback: '', engines: 'comet'], [name: 'no modifications with Sage', fixed: '', variable: '', fallback: '', engines: 'sage'], - [name: 'reject MSGF implicit CAM', fixed: '', variable: '', fallback: '', error: 'Both modification lists are empty'], - [name: 'require label', fixed: '', variable: 'Oxidation (M)', label: '', error: 'Missing or empty required SDRF columns'], - [name: 'require enzyme', fixed: '', variable: 'Oxidation (M)', enzyme: '', error: 'Missing or empty required SDRF columns'], + [ + name: 'whitespace fallback with Comet', fixed: '', variable: '', fallback: ' ', + engines: 'comet', expectedVariable: '' + ], + [ + name: 'null fallback with Sage', fixed: '', variable: null, fallback: null, + engines: 'sage', expectedVariable: '' + ], + [ + name: 'reject MSGF implicit CAM', fixed: '', variable: '', fallback: '', + error: 'Both modification lists are empty' + ], + [ + name: 'reject MSGF whitespace fallback', fixed: '', variable: '', fallback: ' ', + error: 'Both modification lists are empty' + ], + [ + name: 'require label', fixed: '', variable: 'Oxidation (M)', label: '', + error: 'Missing or empty required SDRF columns' + ], + [ + name: 'require enzyme', fixed: '', variable: 'Oxidation (M)', enzyme: '', + error: 'Missing or empty required SDRF columns' + ], ].each { example -> test(example.name) { when { @@ -48,7 +84,9 @@ nextflow_function { } else { assert function.success assert function.result[0].fixedmodifications == (example.fixed?.trim() ?: '') - def expectedVariable = example.variable ?: (example.containsKey('fallback') ? example.fallback : 'Oxidation (M)') + def fallback = example.containsKey('fallback') ? example.fallback : 'Oxidation (M)' + def expectedVariable = example.containsKey('expectedVariable') + ? example.expectedVariable : (example.variable ?: fallback) assert function.result[0].variablemodifications == expectedVariable assert function.result[0].enzyme == 'Trypsin' assert function.result[0].labelling_type == 'label free sample'