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