diff --git a/.cirro/bulk_analysis/preprocess.py b/.cirro/bulk_analysis/preprocess.py index 45a4c95..8431046 100644 --- a/.cirro/bulk_analysis/preprocess.py +++ b/.cirro/bulk_analysis/preprocess.py @@ -25,14 +25,4 @@ samplesheet.to_csv('samplesheet.csv', index=None) ds.add_param("samplesheet", "samplesheet.csv") - -# 3. Set workflow_level value based on form input -ds.logger.info("Setting workflow_level") - -levels = ['convert', 'sample', 'patient', 'compare'] -flags = [ds.params['convert_lvl'], ds.params['sample_lvl'], ds.params['patient_lvl'], ds.params['compare_lvl']] -workflow_level = [lvl for lvl, flag in zip(levels, flags) if flag] - -ds.add_param('workflow_level', ','.join(workflow_level)) - ds.logger.info(ds.params) diff --git a/.cirro/bulk_analysis/process-input.json b/.cirro/bulk_analysis/process-input.json index 8ca08df..4cc09ca 100644 --- a/.cirro/bulk_analysis/process-input.json +++ b/.cirro/bulk_analysis/process-input.json @@ -1,9 +1,8 @@ { "input_format": "airr", - "convert_lvl": false, - "sample_lvl": "$.params.dataset.paramJson.sample_lvl", - "patient_lvl": "$.params.dataset.paramJson.patient_lvl", - "compare_lvl": "$.params.dataset.paramJson.compare_lvl", + "run_sample": "$.params.dataset.paramJson.sample_lvl", + "run_patient": "$.params.dataset.paramJson.patient_lvl", + "run_compare": "$.params.dataset.paramJson.compare_lvl", "olga_chunk_length": "$.params.dataset.paramJson.olga_chunk_length", "matrix_sparsity": "sparse", "distance_metric": "$.params.dataset.paramJson.distance_metric", diff --git a/.cirro/convert_adaptive/preprocess.py b/.cirro/convert_adaptive/preprocess.py index ede348a..8431046 100644 --- a/.cirro/convert_adaptive/preprocess.py +++ b/.cirro/convert_adaptive/preprocess.py @@ -25,14 +25,4 @@ samplesheet.to_csv('samplesheet.csv', index=None) ds.add_param("samplesheet", "samplesheet.csv") - -# 3. Set workflow_level value based on form input -ds.logger.info("Setting workflow_level") - -levels = ['convert', 'sample', 'compare'] -flags = [ds.params['convert_lvl'], ds.params['sample_lvl'], ds.params['compare_lvl']] -workflow_level = [lvl for lvl, flag in zip(levels, flags) if flag] - -ds.add_param('workflow_level', ','.join(workflow_level)) - ds.logger.info(ds.params) diff --git a/.cirro/convert_adaptive/process-input.json b/.cirro/convert_adaptive/process-input.json index a5d764f..10f7cc1 100644 --- a/.cirro/convert_adaptive/process-input.json +++ b/.cirro/convert_adaptive/process-input.json @@ -1,7 +1,7 @@ { "input_format": "adaptive", - "convert_lvl": true, - "sample_lvl": false, - "compare_lvl": false, + "run_convert": true, + "run_sample": false, + "run_compare": false, "outdir": "$.params.dataset.s3|/data/" } \ No newline at end of file diff --git a/bin/samplesheet.py b/bin/samplesheet.py index eb1e06f..add7260 100755 --- a/bin/samplesheet.py +++ b/bin/samplesheet.py @@ -9,7 +9,7 @@ def samplesheet(samplesheet): ss.to_csv('samplesheet_utf8.csv', index=False, encoding='utf-8-sig') stats = ss.describe() - stats.to_csv('samplesheet_stats.csv', index=False, encoding='utf-8-sig') + stats.to_csv('samplesheet_stats.csv', index=True, encoding='utf-8-sig') print(ss.head()) diff --git a/conf/modules.config b/conf/modules.config index 1b959dd..6582ad1 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -29,6 +29,10 @@ process { mode: params.publish_dir_mode, saveAs: { filename -> filename.equals('versions.yml') ? null : filename } ] + // template_details_sample builds dense per-sample TCRdist matrices (OOM'd at 16GB); compare/discovery_brief process full VDJdb match files. + cpus = { ['template_details_sample', 'template_details_compare', 'template_discovery_brief'].contains(notebook.baseName) ? 16 * task.attempt : 4 * task.attempt } + memory = { ['template_details_sample', 'template_details_compare', 'template_discovery_brief'].contains(notebook.baseName) ? 64.GB * task.attempt : 16.GB * task.attempt } + time = { ['template_details_sample', 'template_details_compare', 'template_discovery_brief'].contains(notebook.baseName) ? 16.h : 4.h } } } \ No newline at end of file diff --git a/modules/local/report/render_notebook.nf b/modules/local/report/render_notebook.nf index b9b6e3b..5717d7d 100644 --- a/modules/local/report/render_notebook.nf +++ b/modules/local/report/render_notebook.nf @@ -1,7 +1,6 @@ // Generic process to render a Quarto notebook to HTML process RENDER_NOTEBOOK { tag "${notebook.getBaseName()}" - label 'process_single' input: // path(files) stages files flat in the root dir; staged_layout optionally @@ -16,6 +15,8 @@ process RENDER_NOTEBOOK { tuple path(notebook), path(files), val(staged_layout) val project_name val workflow_cmd + // Fixed name avoids colliding with a same-named file in `files`. + path samplesheet, stageAs: 'render_notebook_samplesheet.csv' output: path "${notebook.getBaseName()}.html", emit: report_html @@ -34,7 +35,7 @@ process RENDER_NOTEBOOK { quarto render ${notebook} \\ -P project_name:${project_name} \\ -P workflow_cmd:'${workflow_cmd}' \\ - -P sample_table:${file(params.samplesheet)} \\ + -P sample_table:${samplesheet} \\ -P subject_col:'${params.subject_col}' \\ -P timepoint_col:'${params.timepoint_col}' \\ -P timepoint_order_col:'${params.timepoint_order_col}' \\ diff --git a/nextflow_schema.json b/nextflow_schema.json index 5b7bdbd..8350b27 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -214,19 +214,19 @@ "default": true }, "local_min_pvalue": { - "type": "string", - "default": 0.001 + "type": ["string", "number"], + "default": "0.001" }, "simulation_depth": { "type": "string", "default": 1000 }, "kmer_min_depth": { - "type": "string", - "default": 3 + "type": ["string", "integer"], + "default": "3" }, "local_min_OVE": { - "type": "string", + "type": ["string", "integer"], "default": "c(1000, 100, 10)" }, "all_aa_interchangeable": { diff --git a/notebooks/template_discovery_brief.qmd b/notebooks/template_discovery_brief.qmd index d0da26d..178494c 100644 --- a/notebooks/template_discovery_brief.qmd +++ b/notebooks/template_discovery_brief.qmd @@ -148,6 +148,10 @@ print('Date and time: ' + str(datetime.datetime.now())) meta = pd.read_csv(sample_table, sep=',') +# alias_col is optional; default to sample name if missing. +if alias_col not in meta.columns: + meta[alias_col] = meta['sample'] + # timepoint_order_col isn't a samplesheet column - compute and inject it here. # timepoint_order (an ordered comma-separated list, e.g. "Base,Week4,EOT") ranks # timepoints by position in that list; any timepoint not listed sorts after the @@ -892,7 +896,7 @@ def create_styled_table(df, title="", max_height='400px', formatter=None): import pandas as pd import numpy as np import itertools -from scipy.stats import fisher_exact +from scipy.stats import hypergeom from statsmodels.stats.multitest import multipletests # --- Setup & Pre-computation --- @@ -911,11 +915,13 @@ sample_total_counts.rename(columns={'duplicate_count': 'total_counts'}, inplace= clonotypes_df = pd.merge(clonotypes_df, sample_total_counts, on=[subject_col, 'origin', timepoint_col]) clonotypes_df['frequency'] = clonotypes_df['duplicate_count'] / clonotypes_df['total_counts'] -# Helper to run fisher exact test on rows -def run_fisher(row, alt_hyp): - table = [[row['count_post'], row['total_post'] - row['count_post']], - [row['count_pre'], row['total_pre'] - row['count_pre']]] - return fisher_exact(table, alternative=alt_hyp)[1] +# One-sided Fisher's exact test reduces to a single vectorized hypergeometric call since total_pre/total_post are fixed margins. +def run_fisher_vectorized(count_post, count_pre, total_post, total_pre, alt_hyp): + M = total_pre + total_post + n = count_post + count_pre + if alt_hyp == 'greater': + return hypergeom.sf(count_post - 1, M, n, total_post) + return hypergeom.cdf(count_post, M, n, total_post) # ========================================== @@ -968,7 +974,9 @@ for (subject, origin), subject_df in clonotypes_df.groupby([subject_col, 'origin continue # 5. Apply Fisher's Exact test only to the remaining rows - comp_df['p_value'] = comp_df.apply(run_fisher, axis=1, alt_hyp='greater') + comp_df['p_value'] = run_fisher_vectorized( + comp_df['count_post'], comp_df['count_pre'], comp_df['total_post'], comp_df['total_pre'], 'greater' + ) # Add metadata comp_df[subject_col] = subject @@ -1090,7 +1098,9 @@ for (subject, origin), subject_df in clonotypes_df.groupby([subject_col, 'origin continue # 5. Apply Fisher - comp_df['p_value'] = comp_df.apply(run_fisher, axis=1, alt_hyp='less') + comp_df['p_value'] = run_fisher_vectorized( + comp_df['count_post'], comp_df['count_pre'], comp_df['total_post'], comp_df['total_pre'], 'less' + ) # Add metadata comp_df[subject_col] = subject @@ -1652,17 +1662,12 @@ for subject, subj_df in clonotypes_df.groupby(subject_col): pivot['freq_t1_plot'] = pivot[t1].replace(0, min_freq) pivot['freq_t2_plot'] = pivot[t2].replace(0, min_freq) - # Assign highlight status using our sets - def assign_status(row): - key = (subject, origin, t1, t2, row['junction_aa']) - if key in sig_exp_lookup: - return "Expanded" - elif key in sig_cont_lookup: - return "Contracted" - else: - return "Not Significant" - - pivot['Status'] = pivot.apply(assign_status, axis=1) + # Avoids the per-row Series-construction overhead of .apply(axis=1). + keys = [(subject, origin, t1, t2, j) for j in pivot['junction_aa']] + pivot['Status'] = [ + "Expanded" if key in sig_exp_lookup else ("Contracted" if key in sig_cont_lookup else "Not Significant") + for key in keys + ] # Sort so that colored points render on top of gray points pivot['sort_order'] = pivot['Status'].map({"Not Significant": 0, "Expanded": 1, "Contracted": 2}) @@ -1869,24 +1874,27 @@ else: if len(tps) > 1: return COLOR_MULTI, 'Multiple Timepoints' else: return timepoint_colors.get(tps[0], '#888888'), tps[0] - node_properties[['color', 'category']] = node_properties['timepoints'].apply( - lambda x: pd.Series(get_color_and_category(x)) - ) + colors_categories = [get_color_and_category(x) for x in node_properties['timepoints']] + node_properties['color'] = [c for c, _ in colors_categories] + node_properties['category'] = [cat for _, cat in colors_categories] - node_properties['size'] = node_properties['max_frequency'].apply(lambda x: max(10, np.sqrt(x * 100) * 15)) + node_properties['size'] = np.maximum(10, np.sqrt(node_properties['max_frequency'] * 100) * 15) tcr_to_idx = {tcr: i for i, tcr in enumerate(node_properties['cdr3_b_aa'])} g = ig.Graph() g.add_vertices(len(node_properties)) g.vs['name'] = node_properties['cdr3_b_aa'] - g.vs['size'] = node_properties['size'] + g.vs['size'] = node_properties['size'] g.vs['color'] = node_properties['color'] g.vs['category'] = node_properties['category'] - g.vs['title'] = node_properties.apply( - lambda row: f"TCR: {row['cdr3_b_aa']}
Peak Freq: {(row['max_frequency']*100):.3f}%
Peak Count: {row['max_count']}
TPs: {row['timepoints']}", - axis=1 - ).tolist() + g.vs['title'] = [ + f"TCR: {cdr3}
Peak Freq: {freq*100:.3f}%
Peak Count: {count}
TPs: {tps}" + for cdr3, freq, count, tps in zip( + node_properties['cdr3_b_aa'], node_properties['max_frequency'], + node_properties['max_count'], node_properties['timepoints'] + ) + ] patient_samples = patient_df['sample'].unique() for sample_id in patient_samples: @@ -2480,7 +2488,11 @@ def plot_public_clones_upset(concat_df, subject_col, min_shared_patients=2): # --- 1. Extract Unique Clones per Patient --- patient_clones = df_clean.groupby(subject_col)['junction_aa'].unique().to_dict() patient_contents = {str(patient): list(clones) for patient, clones in patient_clones.items()} - + + if len(patient_contents) < 2: + print("Only one patient present; skipping UpSet plot (needs ≥ 2 patients to show intersections).") + return + # --- 2. Convert to UpSet Multi-Index Format --- upset_data = from_contents(patient_contents) diff --git a/notebooks/template_giana.qmd b/notebooks/template_giana.qmd index 6e2c56c..a8721ea 100644 --- a/notebooks/template_giana.qmd +++ b/notebooks/template_giana.qmd @@ -37,6 +37,11 @@ warnings.filterwarnings( # Loading data ## reading sample metadata meta = pd.read_csv(sample_table, sep=',') + +# alias_col is optional; default to sample name if missing. +if alias_col not in meta.columns: + meta[alias_col] = meta['sample'] + meta_cols = meta.columns.tolist() ``` diff --git a/notebooks/template_gliph.qmd b/notebooks/template_gliph.qmd index 004975b..33924ab 100644 --- a/notebooks/template_gliph.qmd +++ b/notebooks/template_gliph.qmd @@ -52,6 +52,10 @@ if timepoint_order_col not in meta.columns: rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=str))} meta[timepoint_order_col] = meta[timepoint_col].map(rank_map) +# alias_col is optional; default to sample name if missing. +if alias_col not in meta.columns: + meta[alias_col] = meta['sample'] + concat_df = pd.read_csv(concat_csv, sep='\t') concat_df = concat_df.merge(meta[['sample', 'origin', timepoint_col, timepoint_order_col, alias_col]], on='sample', how='left') diff --git a/notebooks/template_overlap.qmd b/notebooks/template_overlap.qmd index 7312aac..ae18596 100644 --- a/notebooks/template_overlap.qmd +++ b/notebooks/template_overlap.qmd @@ -23,7 +23,7 @@ concat_csv = f"{project_dir}/annotate/concatenated_cdr3_sorted.tsv" from IPython.display import Image, display, Markdown, HTML from matplotlib.colors import LinearSegmentedColormap from scipy.sparse import csr_matrix -from scipy.stats import gaussian_kde, fisher_exact +from scipy.stats import gaussian_kde from statsmodels.stats.multitest import multipletests from scipy.cluster.hierarchy import linkage, leaves_list from scipy.spatial.distance import pdist @@ -53,6 +53,10 @@ import re ## Reading sample metadata meta = pd.read_csv(sample_table, sep=',') +# alias_col is optional; default to sample name if missing. +if alias_col not in meta.columns: + meta[alias_col] = meta['sample'] + # timepoint_order_col isn't a samplesheet column - compute and inject it here. # timepoint_order (an ordered comma-separated list, e.g. "Base,Week4,EOT") ranks # timepoints by position in that list; any timepoint not listed sorts after the @@ -149,7 +153,7 @@ def create_styled_table(df, title="", max_height='400px', formatter=None): import pandas as pd import numpy as np import itertools -from scipy.stats import fisher_exact +from scipy.stats import hypergeom from statsmodels.stats.multitest import multipletests # --- Setup & Pre-computation --- @@ -168,11 +172,13 @@ sample_total_counts.rename(columns={'duplicate_count': 'total_counts'}, inplace= clonotypes_df = pd.merge(clonotypes_df, sample_total_counts, on=[subject_col, 'origin', timepoint_col]) clonotypes_df['frequency'] = clonotypes_df['duplicate_count'] / clonotypes_df['total_counts'] -# Helper to run fisher exact test on rows -def run_fisher(row, alt_hyp): - table = [[row['count_post'], row['total_post'] - row['count_post']], - [row['count_pre'], row['total_pre'] - row['count_pre']]] - return fisher_exact(table, alternative=alt_hyp)[1] +# One-sided Fisher's exact test reduces to a single vectorized hypergeometric call since total_pre/total_post are fixed margins. +def run_fisher_vectorized(count_post, count_pre, total_post, total_pre, alt_hyp): + M = total_pre + total_post + n = count_post + count_pre + if alt_hyp == 'greater': + return hypergeom.sf(count_post - 1, M, n, total_post) + return hypergeom.cdf(count_post, M, n, total_post) # ========================================== @@ -225,7 +231,9 @@ for (subject, origin), subject_df in clonotypes_df.groupby([subject_col, 'origin continue # 5. Apply Fisher's Exact test only to the remaining rows - comp_df['p_value'] = comp_df.apply(run_fisher, axis=1, alt_hyp='greater') + comp_df['p_value'] = run_fisher_vectorized( + comp_df['count_post'], comp_df['count_pre'], comp_df['total_post'], comp_df['total_pre'], 'greater' + ) # Add metadata comp_df[subject_col] = subject @@ -347,7 +355,9 @@ for (subject, origin), subject_df in clonotypes_df.groupby([subject_col, 'origin continue # 5. Apply Fisher - comp_df['p_value'] = comp_df.apply(run_fisher, axis=1, alt_hyp='less') + comp_df['p_value'] = run_fisher_vectorized( + comp_df['count_post'], comp_df['count_pre'], comp_df['total_post'], comp_df['total_pre'], 'less' + ) # Add metadata comp_df[subject_col] = subject @@ -439,7 +449,6 @@ The heatmap displays only those TCR clonotypes that were identified as **"signif import plotly.express as px import plotly.graph_objects as go import scipy.cluster.hierarchy as sch -from scipy.stats import zscore from IPython.display import display, Markdown import pandas as pd @@ -524,7 +533,7 @@ else: # --- Calculate Z-Scores --- if matrix_df.shape[1] > 1: # fillna(0) handles cases where standard deviation is 0 (identical frequencies across time) - z_score_df = matrix_df.apply(zscore, axis=1, result_type='expand').fillna(0) + z_score_df = matrix_df.sub(matrix_df.mean(axis=1), axis=0).div(matrix_df.std(axis=1, ddof=0), axis=0).fillna(0) else: z_score_df = matrix_df.copy() * 0 # If only 1 timepoint, z-score is baseline 0 @@ -771,21 +780,16 @@ def generate_plot_quarto_tabs(id_map, title_prefix, line_color): print("*No clonotypes met the significance thresholds for this direction; " "showing background repertoire trajectories only.*\n") - # Define Status Helper - def get_status(row): - subj = row[subject_col] - orig = row['origin'] if 'origin' in row else None - seq = row['junction_aa'] - - # Check map using the correct key format - key = (subj, orig) if orig is not None else subj - if key in id_map and seq in id_map[key]: - return "Highlight" - return "Background" - # Create temp dataframe plot_data = cdr3_df.copy() - plot_data['status'] = plot_data.apply(get_status, axis=1) + + # Avoids the per-row Series-construction overhead of .apply(axis=1). + has_origin = 'origin' in plot_data.columns + keys = list(zip(plot_data[subject_col], plot_data['origin'])) if has_origin else plot_data[subject_col].tolist() + plot_data['status'] = [ + "Highlight" if (key in id_map and seq in id_map[key]) else "Background" + for key, seq in zip(keys, plot_data['junction_aa']) + ] subjects = sorted(plot_data[subject_col].unique()) color_map = {"Highlight": line_color, "Background": "lightgrey"} diff --git a/notebooks/template_pheno_bulk.qmd b/notebooks/template_pheno_bulk.qmd index 624b2cd..f1c7f72 100644 --- a/notebooks/template_pheno_bulk.qmd +++ b/notebooks/template_pheno_bulk.qmd @@ -61,6 +61,10 @@ warnings.filterwarnings( meta = pd.read_csv(sample_table, sep=',') meta.drop(columns=['file'], inplace=True) +# alias_col is optional; default to sample name if missing. +if alias_col not in meta.columns: + meta[alias_col] = meta['sample'] + # timepoint_order_col isn't a samplesheet column - compute and inject it here. # timepoint_order (an ordered comma-separated list, e.g. "Base,Week4,EOT") ranks # timepoints by position in that list; any timepoint not listed sorts after the diff --git a/notebooks/template_pheno_sc.qmd b/notebooks/template_pheno_sc.qmd index 5d30dc1..5124430 100644 --- a/notebooks/template_pheno_sc.qmd +++ b/notebooks/template_pheno_sc.qmd @@ -50,6 +50,10 @@ import re meta = pd.read_csv(sample_table, sep=',') meta.drop(columns=['file'], inplace=True) +# alias_col is optional; default to sample name if missing. +if alias_col not in meta.columns: + meta[alias_col] = meta['sample'] + # timepoint_order_col isn't a samplesheet column - compute and inject it here. # timepoint_order (an ordered comma-separated list, e.g. "Base,Week4,EOT") ranks # timepoints by position in that list; any timepoint not listed sorts after the diff --git a/notebooks/template_qc.qmd b/notebooks/template_qc.qmd index 471f256..23fdb8e 100644 --- a/notebooks/template_qc.qmd +++ b/notebooks/template_qc.qmd @@ -137,6 +137,10 @@ if timepoint_order_col not in meta.columns: rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=str))} meta[timepoint_order_col] = meta[timepoint_col].map(rank_map) +# alias_col is optional; default to sample name if missing. +if alias_col not in meta.columns: + meta[alias_col] = meta['sample'] + meta_cols = meta.columns.tolist() df = pd.read_csv(sample_stats_csv, sep=',') diff --git a/notebooks/template_sample.qmd b/notebooks/template_sample.qmd index aea664e..1494efe 100644 --- a/notebooks/template_sample.qmd +++ b/notebooks/template_sample.qmd @@ -50,6 +50,10 @@ warnings.filterwarnings( meta = pd.read_csv(sample_table, sep=',') meta.drop(columns=['file'], inplace=True) +# alias_col is optional; default to sample name if missing. +if alias_col not in meta.columns: + meta[alias_col] = meta['sample'] + # timepoint_order_col isn't a samplesheet column - compute and inject it here. # timepoint_order (an ordered comma-separated list, e.g. "Base,Week4,EOT") ranks # timepoints by position in that list; any timepoint not listed sorts after the diff --git a/subworkflows/local/bulktcr_analysis.nf b/subworkflows/local/bulktcr_analysis.nf index 857ba17..6c5fa88 100644 --- a/subworkflows/local/bulktcr_analysis.nf +++ b/subworkflows/local/bulktcr_analysis.nf @@ -165,8 +165,9 @@ workflow BULKTCR_ANALYSIS { .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 -> + // convert_files_l is [meta, file] pairs, staged as ${meta.sample}_airr.tsv regardless of source basename. 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 + tcrdist_files_l + vdjdb_files_l + convert_files_l.collect { _meta, f -> f } + 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], @@ -174,7 +175,7 @@ workflow BULKTCR_ANALYSIS { ["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] } + + convert_files_l.collect { meta, f -> ["${params.project_name}/convert/${meta.sample}_airr.tsv", 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( diff --git a/subworkflows/local/report.nf b/subworkflows/local/report.nf index f66c95a..855e1bb 100644 --- a/subworkflows/local/report.nf +++ b/subworkflows/local/report.nf @@ -23,7 +23,8 @@ workflow REPORT { RENDER_NOTEBOOK( ch_reports, params.project_name, - workflow.commandLine + workflow.commandLine, + file(params.samplesheet) ) emit: diff --git a/tests/modules/local/report/render_notebook.nf.test b/tests/modules/local/report/render_notebook.nf.test index 8f7290a..e6cdbb4 100644 --- a/tests/modules/local/report/render_notebook.nf.test +++ b/tests/modules/local/report/render_notebook.nf.test @@ -26,6 +26,7 @@ nextflow_process { ] input[1] = "TCRtoolkit" input[2] = "nextflow run main.nf" + input[3] = file(params.samplesheet) """ } } @@ -63,6 +64,7 @@ nextflow_process { ] input[1] = "TCRtoolkit" input[2] = "nextflow run main.nf" + input[3] = file(params.samplesheet) """ } } @@ -107,6 +109,7 @@ nextflow_process { ] input[1] = "TCRtoolkit" input[2] = "nextflow run main.nf" + input[3] = file(params.samplesheet) """ } } diff --git a/workflows/tcrtoolkit_bulk.nf b/workflows/tcrtoolkit_bulk.nf index 8a56b20..df72205 100644 --- a/workflows/tcrtoolkit_bulk.nf +++ b/workflows/tcrtoolkit_bulk.nf @@ -23,12 +23,12 @@ workflow TCRTOOLKIT_BULK { println("Running TCRTOOLKIT_BULK workflow...") - // Construct levels list from the run_sample, run_compare, and run_patient parameters + // .toBoolean(): CLI overrides can arrive as the string "false", which is truthy in Groovy. 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' + if (params.run_sample?.toString()?.toBoolean() ?: false) levels << 'sample' + if (params.run_compare?.toString()?.toBoolean() ?: false) levels << 'compare' + if (params.run_patient?.toString()?.toBoolean() ?: false) levels << 'patient' + if (params.run_convert?.toString()?.toBoolean() ?: false) levels << 'convert' def input_format = params.input_format.toLowerCase() @@ -65,12 +65,10 @@ workflow TCRTOOLKIT_BULK { sample_map_final = INPUT_CHECK.out.sample_map } - // template_discovery_brief.qmd stages AIRR-converted files only when CONVERT ran - // (adaptive); template_discovery_brief.qmd's VDJdb section otherwise reads the raw - // input directly, which already has AIRR-standard frequency columns. + // [meta, file] pairs for template_discovery_brief.qmd's VDJdb section. def convert_files = (input_format == 'adaptive') - ? CONVERT.out.map { _meta, f -> f }.collect() - : channel.value([]) + ? CONVERT.out.collect(flat: false) + : INPUT_CHECK.out.sample_map.collect(flat: false) // Bulk reports are sample-centric. Compare- and patient-dependent sections are // added only when those workflow levels are present. Change this once reports do