diff --git a/nextflow.config b/nextflow.config index f4a3370..a97baf9 100644 --- a/nextflow.config +++ b/nextflow.config @@ -25,7 +25,12 @@ params { imgt_lookup = "${projectDir}/assets/airr/imgt_adaptive_lookup.tsv" // Sample + compare parameters - workflow_level = "sample,compare" + run_sample = true + run_compare = true + run_patient = false + run_convert = false + + workflow_level = null project_name = "tcrtoolkit_"+ new Date().format("yyyy-MM-dd_HH-mm-ss") // Notebooks parameters diff --git a/notebooks/template_overlap.qmd b/notebooks/template_overlap.qmd index b6079b6..9c2432d 100644 --- a/notebooks/template_overlap.qmd +++ b/notebooks/template_overlap.qmd @@ -704,17 +704,21 @@ import pandas as pd # Prepare stats table stats_df = detailed_comparisons_table.copy() -stats_df['p_value'] = pd.to_numeric(stats_df['p_value'], errors='coerce') -stats_df['fold_change'] = pd.to_numeric(stats_df['fold_change'], errors='coerce') +if 'p_value' in stats_df.columns: + stats_df['p_value'] = pd.to_numeric(stats_df['p_value'], errors='coerce') +if 'fold_change' in stats_df.columns: + stats_df['fold_change'] = pd.to_numeric(stats_df['fold_change'], errors='coerce') # Helper function to get Top 15 Unique CDR3s per patient & origin def get_top_ids(df, direction): id_map = {} # Safely group by subject and origin if it exists - group_cols = [subject_col, 'origin'] if 'origin' in df.columns else [subject_col] - - for name, group in df.groupby(group_cols): + group_cols = set([subject_col, 'origin']).intersection(df.columns) + if len(group_cols) == 0: + raise ValueError("No valid grouping columns in the df. Available columns: " + ", ".join(df.columns)) + + for name, group in df.groupby(list(group_cols)): # name is either a tuple (subject, origin) or just subject if direction == 'up': subset = group[group['fold_change'] > 1] diff --git a/subworkflows/local/bulktcr_analysis.nf b/subworkflows/local/bulktcr_analysis.nf index 4c81c97..74201ec 100644 --- a/subworkflows/local/bulktcr_analysis.nf +++ b/subworkflows/local/bulktcr_analysis.nf @@ -40,6 +40,27 @@ workflow BULKTCR_ANALYSIS { main: ANNOTATE( sample_map, concat_cdr3 ) + def ch_sample_csv = channel.empty() + def ch_tcrdist_clone_df = channel.empty() + def ch_tcrdist_output = channel.empty() + def ch_v_family = channel.empty() + def ch_j_family = channel.empty() + def ch_tcrdist_files = channel.value([]) + def ch_olga_files = channel.value([]) + def ch_vdjdb_files = channel.value([]) + def ch_convergence_files = channel.value([]) + def ch_tcrpheno_files = channel.value([]) + + def ch_giana_clusters = channel.empty() + def ch_gliph2_cluster_details = channel.empty() + def ch_giana_files = channel.value([]) + def ch_gliph2_all_motifs = channel.value([]) + def ch_gliph2_clone_network = channel.value([]) + def ch_gliph2_cluster_member_details = channel.value([]) + def ch_gliph2_global_similarities = channel.value([]) + + def ch_shared_cdr3 = channel.empty() + if (levels.contains('sample')) { SAMPLE( ANNOTATE.out.processed_samples, @@ -47,10 +68,29 @@ workflow BULKTCR_ANALYSIS { ANNOTATE.out.cdr3_pgen, ANNOTATE.out.olga_stats ) + + ch_sample_csv = SAMPLE.out.sample_csv + ch_tcrdist_clone_df = SAMPLE.out.tcrdist_clone_df + ch_tcrdist_output = SAMPLE.out.tcrdist_output + ch_v_family = SAMPLE.out.v_family + ch_j_family = SAMPLE.out.j_family + ch_tcrdist_files = SAMPLE.out.tcrdist_files + ch_olga_files = SAMPLE.out.olga_files + ch_vdjdb_files = SAMPLE.out.vdjdb_files + ch_convergence_files = SAMPLE.out.convergence_files + ch_tcrpheno_files = SAMPLE.out.tcrpheno_files } if (levels.contains('patient')) { PATIENT( ANNOTATE.out.processed_samples ) + + ch_giana_clusters = PATIENT.out.giana_clusters + ch_gliph2_cluster_details = PATIENT.out.gliph2_cluster_details + ch_giana_files = PATIENT.out.giana_files + ch_gliph2_all_motifs = PATIENT.out.gliph2_all_motifs + ch_gliph2_clone_network = PATIENT.out.gliph2_clone_network + ch_gliph2_cluster_member_details = PATIENT.out.gliph2_cluster_member_details + ch_gliph2_global_similarities = PATIENT.out.gliph2_global_similarities } if (levels.contains('compare')) { @@ -58,12 +98,14 @@ workflow BULKTCR_ANALYSIS { ANNOTATE.out.concat_cdr3_sorted, ANNOTATE.out.cdr3_pgen ) + + ch_shared_cdr3 = COMPARE.out } - if (run_reports) { + if (run_reports && levels.contains('sample')) { ch_reports = channel.empty() - def sample_stats_agg = SAMPLE.out.sample_csv + def sample_stats_agg = ch_sample_csv .collectFile(name: "sample_stats.csv", keepHeader: true, skip: 1, sort: true) // input_format can now only be adaptive/airr (cellranger removed), so the @@ -84,56 +126,27 @@ workflow BULKTCR_ANALYSIS { } ch_reports = ch_reports.mix(ch_qc_report) - ch_discovery_report = sample_stats_agg - .combine(ANNOTATE.out.concat_cdr3_sorted) - .combine(COMPARE.out) - .combine(SAMPLE.out.tcrdist_files.map { l -> [l] }) - .combine(SAMPLE.out.vdjdb_files.map { l -> [l] }) - .combine(convert_files.map { l -> [l] }) - .combine(SAMPLE.out.tcrpheno_files.map { l -> [l] }) - .combine(pseudobulk_pheno_files.map { l -> [l] }) - .map { sample_stats_csv, concat_cdr3_sorted, shared_cdr3, tcrdist_files, vdjdb_files, convert_files_l, tcrpheno_files, pseudobulk_files -> - def report_files = [sample_stats_csv, concat_cdr3_sorted, shared_cdr3, pheno_notebook] + - tcrdist_files + vdjdb_files + convert_files_l + tcrpheno_files + pseudobulk_files - def staged_layout = [ - ["${params.project_name}/sample/${sample_stats_csv.name}", sample_stats_csv.name], - ["${params.project_name}/annotate/${concat_cdr3_sorted.name}", concat_cdr3_sorted.name], - ["${params.project_name}/tcrsharing/${shared_cdr3.name}", shared_cdr3.name], - ["template_pheno.qmd", pheno_notebook.name] - ] + tcrdist_files.collect { f -> ["${params.project_name}/tcrdist3/${f.name}", f.name] } + - vdjdb_files.collect { f -> ["${params.project_name}/vdjdb/${f.name}", f.name] } + - convert_files_l.collect { f -> ["${params.project_name}/convert/${f.name}", f.name] } + - tcrpheno_files.collect { f -> ["${params.project_name}/tcrpheno/${f.name}", f.name] } + - pseudobulk_files.collect { f -> ["${params.project_name}/pseudobulk/${f.name}", f.name] } - tuple( - file(params.template_discovery_brief), - report_files, - staged_layout - ) - } - ch_reports = ch_reports.mix(ch_discovery_report) - ch_details_part1_report = sample_stats_agg .combine(ANNOTATE.out.concat_cdr3_sorted) - .combine(SAMPLE.out.v_family) - .combine(SAMPLE.out.j_family) - .combine(SAMPLE.out.tcrdist_files.map { l -> [l] }) - .combine(SAMPLE.out.olga_files.map { l -> [l] }) - .combine(SAMPLE.out.vdjdb_files.map { l -> [l] }) - .combine(SAMPLE.out.convergence_files.map { l -> [l] }) - .map { sample_stats_csv, concat_cdr3_sorted, v_family, j_family, tcrdist_files, olga_files, vdjdb_files, convergence_files -> + .combine(ch_v_family) + .combine(ch_j_family) + .combine(ch_tcrdist_files.map { l -> [l] }) + .combine(ch_olga_files.map { l -> [l] }) + .combine(ch_vdjdb_files.map { l -> [l] }) + .combine(ch_convergence_files.map { l -> [l] }) + .map { sample_stats_csv, concat_cdr3_sorted, v_family_file, j_family_file, tcrdist_files_l, olga_files_l, vdjdb_files_l, convergence_files_l -> def include_files = [file("${file(params.template_details_part1).parent}/template_sample.qmd")] - def report_files = [sample_stats_csv, concat_cdr3_sorted, v_family, j_family] + - tcrdist_files + olga_files + vdjdb_files + convergence_files + include_files + def report_files = [sample_stats_csv, concat_cdr3_sorted, v_family_file, j_family_file] + + tcrdist_files_l + olga_files_l + vdjdb_files_l + convergence_files_l + include_files def staged_layout = [ ["${params.project_name}/sample/${sample_stats_csv.name}", sample_stats_csv.name], ["${params.project_name}/annotate/${concat_cdr3_sorted.name}", concat_cdr3_sorted.name], - ["${params.project_name}/sample/${v_family.name}", v_family.name], - ["${params.project_name}/sample/${j_family.name}", j_family.name] - ] + tcrdist_files.collect { f -> ["${params.project_name}/tcrdist3/${f.name}", f.name] } + - olga_files.collect { f -> ["${params.project_name}/olga/${f.name}", f.name] } + - vdjdb_files.collect { f -> ["${params.project_name}/vdjdb/${f.name}", f.name] } + - convergence_files.collect { f -> ["${params.project_name}/convergence/${f.name}", f.name] } + ["${params.project_name}/sample/${v_family_file.name}", v_family_file.name], + ["${params.project_name}/sample/${j_family_file.name}", j_family_file.name] + ] + tcrdist_files_l.collect { f -> ["${params.project_name}/tcrdist3/${f.name}", f.name] } + + olga_files_l.collect { f -> ["${params.project_name}/olga/${f.name}", f.name] } + + vdjdb_files_l.collect { f -> ["${params.project_name}/vdjdb/${f.name}", f.name] } + + convergence_files_l.collect { f -> ["${params.project_name}/convergence/${f.name}", f.name] } tuple( file(params.template_details_part1), report_files, @@ -142,73 +155,104 @@ workflow BULKTCR_ANALYSIS { } ch_reports = ch_reports.mix(ch_details_part1_report) - def details_part2_base = sample_stats_agg - .combine(ANNOTATE.out.concat_cdr3_sorted) - .combine(COMPARE.out) - - def run_patient_clustering = levels.contains('patient') - def patient_clustering_notebook = run_patient_clustering - ? file(params.template_patient_clustering_on) - : file(params.template_patient_clustering_off) - - def part2_notebooks_dir = file(params.template_details_part2).parent - def part2_include_files = [ - file("${part2_notebooks_dir}/template_overlap.qmd"), - file("${part2_notebooks_dir}/template_sharing.qmd"), - file("${part2_notebooks_dir}/template_giana.qmd"), - file("${part2_notebooks_dir}/template_gliph.qmd"), - patient_clustering_notebook - ] - def part2_staged_layout_extra = [ - ["template_patient_clustering.qmd", patient_clustering_notebook.name] - ] - - if (run_patient_clustering) { - ch_details_part2_report = details_part2_base - .combine(PATIENT.out.giana_files.map { l -> [l] }) - .combine(PATIENT.out.gliph2_all_motifs.map { l -> [l] }) - .combine(PATIENT.out.gliph2_clone_network.map { l -> [l] }) - .combine(PATIENT.out.gliph2_cluster_member_details.map { l -> [l] }) - .combine(PATIENT.out.gliph2_global_similarities.map { l -> [l] }) - .map { sample_stats_csv, concat_cdr3_sorted, shared_cdr3, giana_files, - all_motifs_pairs, clone_network_pairs, cluster_member_pairs, global_sim_pairs -> - def report_files = [sample_stats_csv, concat_cdr3_sorted, shared_cdr3] + giana_files + - all_motifs_pairs.collect { p -> p[1] } + - clone_network_pairs.collect { p -> p[1] } + - cluster_member_pairs.collect { p -> p[1] } + - global_sim_pairs.collect { p -> p[1] } + - part2_include_files + if (levels.contains('compare')) { + ch_discovery_report = sample_stats_agg + .combine(ANNOTATE.out.concat_cdr3_sorted) + .combine(ch_shared_cdr3) + .combine(ch_tcrdist_files.map { l -> [l] }) + .combine(ch_vdjdb_files.map { l -> [l] }) + .combine(convert_files.map { l -> [l] }) + .combine(ch_tcrpheno_files.map { l -> [l] }) + .combine(pseudobulk_pheno_files.map { l -> [l] }) + .map { sample_stats_csv, concat_cdr3_sorted, shared_cdr3_file, tcrdist_files_l, vdjdb_files_l, convert_files_l, tcrpheno_files_l, pseudobulk_files_l -> + def report_files = [sample_stats_csv, concat_cdr3_sorted, shared_cdr3_file, pheno_notebook] + + tcrdist_files_l + vdjdb_files_l + convert_files_l + tcrpheno_files_l + pseudobulk_files_l def staged_layout = [ ["${params.project_name}/sample/${sample_stats_csv.name}", sample_stats_csv.name], ["${params.project_name}/annotate/${concat_cdr3_sorted.name}", concat_cdr3_sorted.name], - ["${params.project_name}/tcrsharing/${shared_cdr3.name}", shared_cdr3.name] - ] + giana_files.collect { f -> ["${params.project_name}/giana/${f.name}", f.name] } + - all_motifs_pairs.collect { p -> ["${params.project_name}/gliph2/${p[0]}/${p[1].name}", p[1].name] } + - clone_network_pairs.collect { p -> ["${params.project_name}/gliph2/${p[0]}/${p[1].name}", p[1].name] } + - cluster_member_pairs.collect { p -> ["${params.project_name}/gliph2/${p[0]}/${p[1].name}", p[1].name] } + - global_sim_pairs.collect { p -> ["${params.project_name}/gliph2/${p[0]}/${p[1].name}", p[1].name] } + - part2_staged_layout_extra + ["${params.project_name}/tcrsharing/${shared_cdr3_file.name}", shared_cdr3_file.name], + ["template_pheno.qmd", pheno_notebook.name] + ] + tcrdist_files_l.collect { f -> ["${params.project_name}/tcrdist3/${f.name}", f.name] } + + vdjdb_files_l.collect { f -> ["${params.project_name}/vdjdb/${f.name}", f.name] } + + convert_files_l.collect { f -> ["${params.project_name}/convert/${f.name}", f.name] } + + tcrpheno_files_l.collect { f -> ["${params.project_name}/tcrpheno/${f.name}", f.name] } + + pseudobulk_files_l.collect { f -> ["${params.project_name}/pseudobulk/${f.name}", f.name] } tuple( - file(params.template_details_part2), + file(params.template_discovery_brief), report_files, staged_layout ) } - } else { - ch_details_part2_report = details_part2_base - .map { sample_stats_csv, concat_cdr3_sorted, shared_cdr3 -> - tuple( - file(params.template_details_part2), - [sample_stats_csv, concat_cdr3_sorted, shared_cdr3] + part2_include_files, - [ + ch_reports = ch_reports.mix(ch_discovery_report) + + def details_part2_base = sample_stats_agg + .combine(ANNOTATE.out.concat_cdr3_sorted) + .combine(ch_shared_cdr3) + + def run_patient_clustering = levels.contains('patient') + def patient_clustering_notebook = run_patient_clustering + ? file(params.template_patient_clustering_on) + : file(params.template_patient_clustering_off) + + def part2_notebooks_dir = file(params.template_details_part2).parent + def part2_include_files = [ + file("${part2_notebooks_dir}/template_overlap.qmd"), + file("${part2_notebooks_dir}/template_sharing.qmd"), + file("${part2_notebooks_dir}/template_giana.qmd"), + file("${part2_notebooks_dir}/template_gliph.qmd"), + patient_clustering_notebook + ] + def part2_staged_layout_extra = [ + ["template_patient_clustering.qmd", patient_clustering_notebook.name] + ] + + if (run_patient_clustering) { + ch_details_part2_report = details_part2_base + .combine(ch_giana_files.map { l -> [l] }) + .combine(ch_gliph2_all_motifs.map { l -> [l] }) + .combine(ch_gliph2_clone_network.map { l -> [l] }) + .combine(ch_gliph2_cluster_member_details.map { l -> [l] }) + .combine(ch_gliph2_global_similarities.map { l -> [l] }) + .map { sample_stats_csv, concat_cdr3_sorted, shared_cdr3_file, giana_files_l, + all_motifs_pairs, clone_network_pairs, cluster_member_pairs, global_sim_pairs -> + def report_files = [sample_stats_csv, concat_cdr3_sorted, shared_cdr3_file] + giana_files_l + + all_motifs_pairs.collect { p -> p[1] } + + clone_network_pairs.collect { p -> p[1] } + + cluster_member_pairs.collect { p -> p[1] } + + global_sim_pairs.collect { p -> p[1] } + + part2_include_files + def staged_layout = [ ["${params.project_name}/sample/${sample_stats_csv.name}", sample_stats_csv.name], ["${params.project_name}/annotate/${concat_cdr3_sorted.name}", concat_cdr3_sorted.name], - ["${params.project_name}/tcrsharing/${shared_cdr3.name}", shared_cdr3.name] - ] + part2_staged_layout_extra - ) - } + ["${params.project_name}/tcrsharing/${shared_cdr3_file.name}", shared_cdr3_file.name] + ] + giana_files_l.collect { f -> ["${params.project_name}/giana/${f.name}", f.name] } + + all_motifs_pairs.collect { p -> ["${params.project_name}/gliph2/${p[0]}/${p[1].name}", p[1].name] } + + clone_network_pairs.collect { p -> ["${params.project_name}/gliph2/${p[0]}/${p[1].name}", p[1].name] } + + cluster_member_pairs.collect { p -> ["${params.project_name}/gliph2/${p[0]}/${p[1].name}", p[1].name] } + + global_sim_pairs.collect { p -> ["${params.project_name}/gliph2/${p[0]}/${p[1].name}", p[1].name] } + + part2_staged_layout_extra + tuple( + file(params.template_details_part2), + report_files, + staged_layout + ) + } + } else { + ch_details_part2_report = details_part2_base + .map { sample_stats_csv, concat_cdr3_sorted, shared_cdr3_file -> + tuple( + file(params.template_details_part2), + [sample_stats_csv, concat_cdr3_sorted, shared_cdr3_file] + part2_include_files, + [ + ["${params.project_name}/sample/${sample_stats_csv.name}", sample_stats_csv.name], + ["${params.project_name}/annotate/${concat_cdr3_sorted.name}", concat_cdr3_sorted.name], + ["${params.project_name}/tcrsharing/${shared_cdr3_file.name}", shared_cdr3_file.name] + ] + part2_staged_layout_extra + ) + } + } + ch_reports = ch_reports.mix(ch_details_part2_report) } - ch_reports = ch_reports.mix(ch_details_part2_report) REPORT( ch_reports ) } @@ -219,24 +263,24 @@ workflow BULKTCR_ANALYSIS { cdr3_pgen = ANNOTATE.out.cdr3_pgen olga_stats = ANNOTATE.out.olga_stats - sample_csv = levels.contains('sample') ? SAMPLE.out.sample_csv : channel.empty() - tcrdist_clone_df = levels.contains('sample') ? SAMPLE.out.tcrdist_clone_df : channel.empty() - tcrdist_output = levels.contains('sample') ? SAMPLE.out.tcrdist_output : channel.empty() - v_family = levels.contains('sample') ? SAMPLE.out.v_family : channel.empty() - j_family = levels.contains('sample') ? SAMPLE.out.j_family : channel.empty() - tcrdist_files = levels.contains('sample') ? SAMPLE.out.tcrdist_files : channel.value([]) - olga_files = levels.contains('sample') ? SAMPLE.out.olga_files : channel.value([]) - vdjdb_files = levels.contains('sample') ? SAMPLE.out.vdjdb_files : channel.value([]) - convergence_files = levels.contains('sample') ? SAMPLE.out.convergence_files : channel.value([]) - tcrpheno_files = levels.contains('sample') ? SAMPLE.out.tcrpheno_files : channel.value([]) - - giana_clusters = levels.contains('patient') ? PATIENT.out.giana_clusters : channel.empty() - gliph2_cluster_details = levels.contains('patient') ? PATIENT.out.gliph2_cluster_details : channel.empty() - giana_files = levels.contains('patient') ? PATIENT.out.giana_files : channel.value([]) - gliph2_all_motifs = levels.contains('patient') ? PATIENT.out.gliph2_all_motifs : channel.value([]) - gliph2_clone_network = levels.contains('patient') ? PATIENT.out.gliph2_clone_network : channel.value([]) - gliph2_cluster_member_details = levels.contains('patient') ? PATIENT.out.gliph2_cluster_member_details : channel.value([]) - gliph2_global_similarities = levels.contains('patient') ? PATIENT.out.gliph2_global_similarities : channel.value([]) - - shared_cdr3 = levels.contains('compare') ? COMPARE.out : channel.empty() + sample_csv = ch_sample_csv + tcrdist_clone_df = ch_tcrdist_clone_df + tcrdist_output = ch_tcrdist_output + v_family = ch_v_family + j_family = ch_j_family + tcrdist_files = ch_tcrdist_files + olga_files = ch_olga_files + vdjdb_files = ch_vdjdb_files + convergence_files = ch_convergence_files + tcrpheno_files = ch_tcrpheno_files + + giana_clusters = ch_giana_clusters + gliph2_cluster_details = ch_gliph2_cluster_details + giana_files = ch_giana_files + gliph2_all_motifs = ch_gliph2_all_motifs + gliph2_clone_network = ch_gliph2_clone_network + gliph2_cluster_member_details = ch_gliph2_cluster_member_details + gliph2_global_similarities = ch_gliph2_global_similarities + + shared_cdr3 = ch_shared_cdr3 } diff --git a/tests/main.nf.test b/tests/main.nf.test index b1a73cd..3005af8 100644 --- a/tests/main.nf.test +++ b/tests/main.nf.test @@ -3,6 +3,9 @@ nextflow_pipeline { name "Test Workflow main.nf" script "main.nf" + def bulkContainer = System.getenv("CONTAINER") ?: "ghcr.io/karchinlab/tcrtoolkit:main" + def integrationModes = "../../../tests/test_data/integration-modes" + test("Minimal example dataset") { tag "minimal-example" @@ -14,7 +17,7 @@ nextflow_pipeline { input_format = "adaptive" max_memory = "8.GB" max_cpus = 4 - container = System.getenv("CONTAINER") ?: "ghcr.io/karchinlab/tcrtoolkit:main" + container = bulkContainer } } @@ -55,7 +58,7 @@ nextflow_pipeline { pseudobulk_qc_min_cells = 1 max_memory = "8.GB" max_cpus = 4 - container = System.getenv("CONTAINER") ?: "ghcr.io/karchinlab/tcrtoolkit:main" + container = bulkContainer } } @@ -77,7 +80,7 @@ nextflow_pipeline { input_format = "cellranger" max_memory = "8.GB" max_cpus = 4 - container = System.getenv("CONTAINER") ?: "ghcr.io/karchinlab/tcrtoolkit:main" + container = bulkContainer } } @@ -86,4 +89,326 @@ nextflow_pipeline { } } + + test("Bulk convert only") { + + tag "bulk" + tag "convert-only" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_only.csv" + outdir = "out-bulk-convert-only" + input_format = "adaptive" + workflow_level = "convert" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } + + test("Bulk sample only") { + + tag "bulk" + tag "sample-only" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_only.csv" + outdir = "out-bulk-sample-only" + input_format = "adaptive" + workflow_level = "sample" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } + + test("Bulk patient only") { + + tag "bulk" + tag "patient-only" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_patient_compare.csv" + outdir = "out-bulk-patient-only" + input_format = "adaptive" + workflow_level = "patient" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } + + test("Bulk compare only") { + + tag "bulk" + tag "compare-only" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_patient_compare.csv" + outdir = "out-bulk-compare-only" + input_format = "adaptive" + workflow_level = "compare" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } + + test("Bulk sample and convert") { + + tag "bulk" + tag "sample-convert" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_only.csv" + outdir = "out-bulk-sample-convert" + input_format = "adaptive" + workflow_level = "convert,sample" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } + + test("Bulk sample and compare") { + + tag "bulk" + tag "sample-compare" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_patient_compare.csv" + outdir = "out-bulk-sample-compare" + input_format = "adaptive" + workflow_level = "sample,compare" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } + + test("Bulk sample, patient") { + + tag "bulk" + tag "sample-patient" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_patient_compare.csv" + outdir = "out-bulk-sample-patient" + input_format = "adaptive" + workflow_level = "sample,patient" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } + + test("Bulk patient and convert") { + + tag "bulk" + tag "patient-convert" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_patient_compare.csv" + outdir = "out-bulk-patient-convert" + input_format = "adaptive" + workflow_level = "convert,patient" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } + + test("Bulk compare and convert") { + + tag "bulk" + tag "compare-convert" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_patient_compare.csv" + outdir = "out-bulk-compare-convert" + input_format = "adaptive" + workflow_level = "convert,compare" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } + + test("Bulk sample, patient, compare") { + + tag "bulk" + tag "sample-patient-compare" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_patient_compare.csv" + outdir = "out-bulk-sample-patient-compare" + input_format = "adaptive" + workflow_level = "sample,patient,compare" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } + + test("Bulk sample, patient, convert") { + + tag "bulk" + tag "sample-patient-convert" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_patient_compare.csv" + outdir = "out-bulk-sample-patient-convert" + input_format = "adaptive" + workflow_level = "convert,sample,patient" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } + + test("Bulk sample, compare, convert") { + + tag "bulk" + tag "sample-compare-convert" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_patient_compare.csv" + outdir = "out-bulk-sample-compare-convert" + input_format = "adaptive" + workflow_level = "convert,sample,compare" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } + + test("Bulk patient, compare, convert") { + + tag "bulk" + tag "patient-compare-convert" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_patient_compare.csv" + outdir = "out-bulk-patient-compare-convert" + input_format = "adaptive" + workflow_level = "convert,patient,compare" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } + + test("Bulk sample, patient, compare, convert") { + + tag "bulk" + tag "sample-patient-compare-convert" + + when { + params { + samplesheet = "${integrationModes}/bulk_sample_patient_compare.csv" + outdir = "out-bulk-sample-patient-compare-convert" + input_format = "adaptive" + workflow_level = "convert,sample,patient,compare" + max_memory = "8.GB" + max_cpus = 4 + container = bulkContainer + } + } + + then { + assert workflow.success + } + + } } diff --git a/tests/test_data/integration-modes/bulk_sample_only.csv b/tests/test_data/integration-modes/bulk_sample_only.csv new file mode 100644 index 0000000..a4b6032 --- /dev/null +++ b/tests/test_data/integration-modes/bulk_sample_only.csv @@ -0,0 +1,3 @@ +sample,alias,patient,timepoint,origin,file +Patient01_Base,Patient01_Base,Patient01,Base,tumor,https://raw.githubusercontent.com/KarchinLab/TCRtoolkit/refs/heads/main/tests/test_data/minimal-example/PD1_Patient01_Base.tsv +Patient02_Base,Patient02_Base,Patient02,Base,tumor,https://raw.githubusercontent.com/KarchinLab/TCRtoolkit/refs/heads/main/tests/test_data/minimal-example/PD1_Patient02_Base.tsv \ No newline at end of file diff --git a/tests/test_data/integration-modes/bulk_sample_patient_compare.csv b/tests/test_data/integration-modes/bulk_sample_patient_compare.csv new file mode 100644 index 0000000..52648e7 --- /dev/null +++ b/tests/test_data/integration-modes/bulk_sample_patient_compare.csv @@ -0,0 +1,5 @@ +sample,alias,patient,timepoint,origin,file +Patient16_Base,Patient16_Base,Patient16,Base,tumor,https://raw.githubusercontent.com/KarchinLab/TCRtoolkit/refs/heads/main/tests/test_data/minimal-example/PD1_Patient16_Base.tsv +Patient16_Post,Patient16_Post,Patient16,Post,tumor,https://raw.githubusercontent.com/KarchinLab/TCRtoolkit/refs/heads/main/tests/test_data/minimal-example/PD1_Patient16_Post.tsv +Patient17_Base,Patient17_Base,Patient17,Base,tumor,https://raw.githubusercontent.com/KarchinLab/TCRtoolkit/refs/heads/main/tests/test_data/minimal-example/PD1_Patient17_Base.tsv +Patient17_Post,Patient17_Post,Patient17,Post,tumor,https://raw.githubusercontent.com/KarchinLab/TCRtoolkit/refs/heads/main/tests/test_data/minimal-example/PD1_Patient17_Post.tsv \ No newline at end of file diff --git a/workflows/tcrtoolkit_bulk.nf b/workflows/tcrtoolkit_bulk.nf index 6abb281..8a56b20 100644 --- a/workflows/tcrtoolkit_bulk.nf +++ b/workflows/tcrtoolkit_bulk.nf @@ -23,11 +23,21 @@ workflow TCRTOOLKIT_BULK { println("Running TCRTOOLKIT_BULK workflow...") - // Split the workflow_level parameter into a list of levels - def levels = params.workflow_level.toLowerCase().tokenize(',') + // Construct levels list from the run_sample, run_compare, and run_patient parameters + def levels = [] + if (params.run_sample) levels << 'sample' + if (params.run_compare) levels << 'compare' + if (params.run_patient) levels << 'patient' + if (params.run_convert) levels << 'convert' + def input_format = params.input_format.toLowerCase() // Validate + if (params.workflow_level) { + println("[WARN] workflow_level is set for deprecation, please use run_ flags. Overriding flags with workflow_level.") + levels = params.workflow_level.toLowerCase().tokenize(',') + } + if (levels.contains('convert') && input_format != 'adaptive') { println("\u001B[33m[WARN]\u001B[0m To run Convert workflow, please specify a valid convertible --input_format (adaptive)") if (!levels.contains('sample') && !levels.contains('compare')) { @@ -62,6 +72,11 @@ workflow TCRTOOLKIT_BULK { ? CONVERT.out.map { _meta, f -> f }.collect() : channel.value([]) + // Bulk reports are sample-centric. Compare- and patient-dependent sections are + // added only when those workflow levels are present. Change this once reports do + // not always require sample workflow. + def run_reports = levels.contains('sample') + // --- Main Analysis --- if (levels.intersect(['sample','patient','compare'])) { ANNOTATE_INGEST( sample_map_final ) @@ -71,7 +86,7 @@ workflow TCRTOOLKIT_BULK { ANNOTATE_INGEST.out.per_sample_stats, ANNOTATE_INGEST.out.concat_cdr3, levels, - true, + run_reports, convert_files ) }