From 8a911a08bdb99265e8d41b8e7d80962a54f1be03 Mon Sep 17 00:00:00 2001 From: robert-a-forsyth Date: Tue, 6 Oct 2026 14:22:32 +0200 Subject: [PATCH] Pair tumor and normal BAMs regardless of per-BAM kinetics Tumor/normal pairing joined on a meta that carried METAEXTRACT's per-BAM `kinetics` flag, so a pair where only one BAM kept PacBio kinetics was silently dropped from all variant calling; its QC still reached the report join, which then crashed with "Invalid method invocation call". - Drop `kinetics` from meta after alignment; only the m6A branch reads it. - Stop early when a sample's BAMs resolve to different Clair models (validateSampleModels), since those also key the pairing joins. - Make the tumor/normal join failOnMismatch so meta drift can no longer drop a pair silently. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../local/paired/paired_smallvar_germline.nf | 9 ++-- .../local/paired/paired_smallvar_somatic.nf | 3 +- subworkflows/local/phasing_haplotyping.nf | 12 ++--- subworkflows/local/small_variant_consensus.nf | 6 +-- .../local/tumor_only/tumoronly_smallvar.nf | 6 +-- .../utils_nfcore_lrsomatic_pipeline/main.nf | 15 ++++++ .../tests/main.function.nf.test | 49 +++++++++++++++++++ workflows/lrsomatic.nf | 26 +++++----- 8 files changed, 90 insertions(+), 36 deletions(-) diff --git a/subworkflows/local/paired/paired_smallvar_germline.nf b/subworkflows/local/paired/paired_smallvar_germline.nf index f1ba424d..0edebc2b 100644 --- a/subworkflows/local/paired/paired_smallvar_germline.nf +++ b/subworkflows/local/paired/paired_smallvar_germline.nf @@ -44,8 +44,7 @@ workflow PAIRED_SMALLVAR_GERMLINE { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return [ new_meta, meta.clair3_model, bam, bai ] } .set { normal_bams_model } @@ -112,8 +111,7 @@ workflow PAIRED_SMALLVAR_GERMLINE { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') def intervals = [] return [new_meta, bam, bai, intervals] } @@ -188,8 +186,7 @@ workflow PAIRED_SMALLVAR_GERMLINE { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return[new_meta, vcf, tbi] } .set{germline_vcf} diff --git a/subworkflows/local/paired/paired_smallvar_somatic.nf b/subworkflows/local/paired/paired_smallvar_somatic.nf index 5a499ec0..a0fd1a17 100644 --- a/subworkflows/local/paired/paired_smallvar_somatic.nf +++ b/subworkflows/local/paired/paired_smallvar_somatic.nf @@ -176,8 +176,7 @@ workflow PAIRED_SMALLVAR_SOMATIC { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return[new_meta, vcf, tbi] } .set{somatic_vcf} diff --git a/subworkflows/local/phasing_haplotyping.nf b/subworkflows/local/phasing_haplotyping.nf index 8dbb4184..6896c0ef 100644 --- a/subworkflows/local/phasing_haplotyping.nf +++ b/subworkflows/local/phasing_haplotyping.nf @@ -51,8 +51,7 @@ workflow PHASING_HAPLOTYPING { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return [ new_meta, bam, bai ] } .set{ tumor_only_ch } @@ -78,8 +77,7 @@ workflow PHASING_HAPLOTYPING { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return [ new_meta, bam, bai ] } .set{ paired_normal_ch } @@ -94,8 +92,7 @@ workflow PHASING_HAPLOTYPING { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return [ new_meta, bam, bai ] } .set{ paired_tumor_ch } @@ -338,8 +335,7 @@ workflow PHASING_HAPLOTYPING { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return [ new_meta, mods ] } .set{modcall_vcf_ch} diff --git a/subworkflows/local/small_variant_consensus.nf b/subworkflows/local/small_variant_consensus.nf index 773609fd..8914ec2f 100644 --- a/subworkflows/local/small_variant_consensus.nf +++ b/subworkflows/local/small_variant_consensus.nf @@ -135,8 +135,7 @@ workflow SMALL_VARIANT_CONSENSUS { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return [ new_meta, vcfs, tbi] } .set{clair_ch} @@ -152,8 +151,7 @@ workflow SMALL_VARIANT_CONSENSUS { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return [ new_meta, vcfs, tbi] } .set{deepvariant_ch} diff --git a/subworkflows/local/tumor_only/tumoronly_smallvar.nf b/subworkflows/local/tumor_only/tumoronly_smallvar.nf index 4e85610c..9992b1c9 100644 --- a/subworkflows/local/tumor_only/tumoronly_smallvar.nf +++ b/subworkflows/local/tumor_only/tumoronly_smallvar.nf @@ -359,8 +359,7 @@ workflow TUMORONLY_SMALLVAR { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return[new_meta, vcf, tbi] } .set{somatic_vcf} @@ -374,8 +373,7 @@ workflow TUMORONLY_SMALLVAR { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return[new_meta, vcf, tbi] } .set{germline_vcf} diff --git a/subworkflows/local/utils_nfcore_lrsomatic_pipeline/main.nf b/subworkflows/local/utils_nfcore_lrsomatic_pipeline/main.nf index bc8c7da8..4636a6f8 100644 --- a/subworkflows/local/utils_nfcore_lrsomatic_pipeline/main.nf +++ b/subworkflows/local/utils_nfcore_lrsomatic_pipeline/main.nf @@ -330,6 +330,21 @@ def validateInputSamplesheet(input) { return [ metas[0], bams ] } + +// +// Caller models key the tumor/normal and replicate joins, so all BAMs of a sample must agree on them +// +def validateSampleModels(metas) { + def model_keys = ['clair3_model', 'clairS_model', 'clairSTO_model'] + metas.groupBy { meta -> meta.id }.each { id, rows -> + def differing = model_keys.findAll { key -> rows.collect { meta -> meta[key] }.unique().size() > 1 } + if (differing) { + def detail = differing.collect { key -> "${key}: ${rows.collect { meta -> "${meta.type}=${meta[key]}" }.join(', ')}" }.join('; ') + error("Sample '${id}': its BAMs resolve to different caller models (${detail}). Set ${differing.join(', ')} explicitly in the samplesheet for every row of this sample.") + } + } + return true +} // // Get attribute from genome config file e.g. fasta // diff --git a/subworkflows/local/utils_nfcore_lrsomatic_pipeline/tests/main.function.nf.test b/subworkflows/local/utils_nfcore_lrsomatic_pipeline/tests/main.function.nf.test index 8c90956b..8e38d85b 100644 --- a/subworkflows/local/utils_nfcore_lrsomatic_pipeline/tests/main.function.nf.test +++ b/subworkflows/local/utils_nfcore_lrsomatic_pipeline/tests/main.function.nf.test @@ -984,4 +984,53 @@ nextflow_function { ) } } + + // --- validateSampleModels: a sample's BAMs must agree on caller models, or the pairing joins drop it + + test("validateSampleModels accepts a pair that differs only in kinetics") { + + function "validateSampleModels" + + when { + function { + """ + input[0] = [ + [id: 's1', type: 'tumor', clair3_model: 'hifi', clairS_model: 'hifi_revio_ssrs', clairSTO_model: 'hifi_revio_ssrs', kinetics: 'false'], + [id: 's1', type: 'normal', clair3_model: 'hifi', clairS_model: 'hifi_revio_ssrs', clairSTO_model: 'hifi_revio_ssrs', kinetics: 'true'] + ] + """ + } + } + + then { + assertAll( + { assert function.success }, + { assert function.result == true } + ) + } + } + + test("validateSampleModels stops on a tumor/normal clair3_model mismatch") { + + function "validateSampleModels" + + when { + function { + """ + input[0] = [ + [id: 's1', type: 'tumor', clair3_model: 'r1041_e82_400bps_sup_v500', clairS_model: 'ont_r10_dorado_sup_5khz', clairSTO_model: 'ont_r10_dorado_sup_5khz'], + [id: 's1', type: 'normal', clair3_model: 'r1041_e82_400bps_sup_v430', clairS_model: 'ont_r10_dorado_sup_5khz', clairSTO_model: 'ont_r10_dorado_sup_5khz'], + [id: 's2', type: 'tumor', clair3_model: 'hifi', clairS_model: 'hifi_revio_ssrs', clairSTO_model: 'hifi_revio_ssrs'] + ] + """ + } + } + + then { + assertAll( + { assert function.failed }, + { assert function.stdout.any { line -> line.contains("Sample 's1'") && line.contains('clair3_model') } } + ) + } + } } diff --git a/workflows/lrsomatic.nf b/workflows/lrsomatic.nf index 381abb01..2238d396 100644 --- a/workflows/lrsomatic.nf +++ b/workflows/lrsomatic.nf @@ -12,7 +12,7 @@ include { getGenomeAttribute } from '../subworkflows/local/utils_nfcore_lrso include { reportGenePanelTokens } from '../subworkflows/local/utils_nfcore_lrsomatic_pipeline' include { reportGenePanelIsFile } from '../subworkflows/local/utils_nfcore_lrsomatic_pipeline' include { resolveVepPlugins; validateVepPluginParams } from '../subworkflows/local/utils_nfcore_lrsomatic_pipeline' -include { validateClairstoCnaResources } from '../subworkflows/local/utils_nfcore_lrsomatic_pipeline' +include { validateClairstoCnaResources; validateSampleModels } from '../subworkflows/local/utils_nfcore_lrsomatic_pipeline' include { PREPARE_VEP_PLUGINS } from '../subworkflows/local/prepare_vep_plugins' // @@ -273,6 +273,12 @@ workflow LRSOMATIC { // clair3_model, clairS_model, clairSTO_model, kinetics // bams are grouped per sample (multiple runs merged into a list) + // Fail fast if a sample's BAMs resolve to different caller models; they key the pairing joins + ch_samplesheet + .map { meta, _bam -> meta } + .collect() + .map { metas -> validateSampleModels(metas) } + // // SUBWORKFLOW: PREPARE_REFERENCE_FILES -- decompress and index the FASTA, fetch Clair3 models, unpack ASCAT references // Input: params.fasta, ASCAT file paths, basecall_meta, clair3_modelMap @@ -542,7 +548,6 @@ workflow LRSOMATIC { 'clair3_model', 'clairS_model', 'clairSTO_model', - 'kinetics', 'n_replicates') // groupKey: release each sample as soon as its own replicates arrive, not when all samples have return [groupKey(new_meta, new_meta.n_replicates), bam, bai] @@ -626,8 +631,7 @@ workflow LRSOMATIC { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return[new_meta, bam, bai] } .set{paired_normal_bams} @@ -643,11 +647,11 @@ workflow LRSOMATIC { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return[new_meta, bam, bai] } - .join(paired_normal_bams) + // failOnMismatch: a tumor/normal meta drift must stop the run, not drop the pair + .join(paired_normal_bams, failOnMismatch: true) .set { somatic_smallvar_input } // somatic_smallvar_input: [meta, tumor_bam, tumor_bai, normal_bam, normal_bai] @@ -672,8 +676,7 @@ workflow LRSOMATIC { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') def normal_bam = [] def normal_bai = [] return [new_meta, normal_bam, normal_bai, bam, bai] @@ -966,8 +969,7 @@ workflow LRSOMATIC { 'fiber', 'clair3_model', 'clairS_model', - 'clairSTO_model', - 'kinetics') + 'clairSTO_model') return[new_meta, bam, bai] } .map{meta, tumor_bam, tumor_bai-> @@ -1134,7 +1136,7 @@ workflow LRSOMATIC { // so build its input from PHASING_HAPLOTYPING's haplotagged BAMs rather than the // unphased ones severus_input carries. def savana_meta_keys = ['id', 'paired_data', 'platform', 'sex', 'fiber', - 'clair3_model', 'clairS_model', 'clairSTO_model', 'kinetics'] + 'clair3_model', 'clairS_model', 'clairSTO_model'] PHASING_HAPLOTYPING.out.tumor_normal_hapbams_ch .branch { meta, _bam, _bai ->