diff --git a/.github/workflows/nextflow-test.yaml b/.github/workflows/nextflow-test.yaml index fcc538b..474d047 100644 --- a/.github/workflows/nextflow-test.yaml +++ b/.github/workflows/nextflow-test.yaml @@ -5,6 +5,9 @@ on: pull_request: branches: - 'main' + push: + branches: + - 'v2' workflow_dispatch: env: diff --git a/.gitignore b/.gitignore index 9c9b98a..e9ff6c1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ runs/* results* work out-* +out/* ## nf-test .nf-test/ @@ -34,4 +35,6 @@ notebooks/* tmp ## vscode -.vscode/* \ No newline at end of file +.vscode/* + +.e2e_test_tmp/ diff --git a/assets/NO_FILE b/assets/NO_FILE new file mode 100644 index 0000000..e69de29 diff --git a/bin/bulk_to_export.py b/bin/bulk_to_export.py new file mode 100755 index 0000000..64a67c5 --- /dev/null +++ b/bin/bulk_to_export.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +""" +Bridge: BULK_TO_EXPORT + +Builds a per-cell export table (the format REPERTOIRE / MASTER_SUMMARY consume) from +pseudobulk clonotype data, so those reports can run in the VDJ-only route where there is +no GEX Seurat object. + +The repertoire report derives per-clone cell counts by COUNTING ROWS grouped by +(sample, clone_id) — i.e. it treats each row as one cell. So each clonotype with +duplicate_count = N is expanded into N rows (one synthetic "cell" each). Without a GEX +object there is no cell-type annotation, so `annot` is a constant ("Unannotated"). + +Input : concatenated canonical clonotype table (junction_aa, v_call, j_call, + duplicate_count, ..., sample) — e.g. ANNOTATE_FROM_CONCAT's concat_cdr3_sorted. +Optional: samplesheet CSV mapping sample -> patient_id (else patient_id = sample). + +Output: export_cells.tsv with columns REPERTOIRE/MASTER_SUMMARY resolve: + cell_id, sample, patient_id, clone_id, clone_size, annot, has_tcr, paired_tcr +""" + +import sys +import argparse +import pandas as pd + + +def main(): + ap = argparse.ArgumentParser(description="BULK_TO_EXPORT bridge") + ap.add_argument("concat_cdr3", help="concatenated canonical clonotype TSV (has 'sample')") + ap.add_argument("--samplesheet", default=None, help="CSV with sample,patient_id (optional)") + ap.add_argument("--out", default="export_cells.tsv") + args = ap.parse_args() + + df = pd.read_csv(args.concat_cdr3, sep="\t", low_memory=False) + + for col in ("junction_aa", "duplicate_count", "sample"): + if col not in df.columns: + sys.exit(f"[BULK_TO_EXPORT] input missing required column '{col}'. Have: {list(df.columns)}") + + # Clone identity = CDR3b + V gene (falls back to junction_aa alone if v_call absent) + if "v_call" in df.columns: + df["clone_id"] = df["junction_aa"].astype(str) + "_" + df["v_call"].astype(str) + else: + df["clone_id"] = df["junction_aa"].astype(str) + + df["clone_size"] = pd.to_numeric(df["duplicate_count"], errors="coerce").fillna(0).astype(int) + df = df[df["clone_size"] > 0].copy() + if df.empty: + sys.exit("[BULK_TO_EXPORT] no clonotypes with positive counts.") + + # patient_id: from samplesheet if given, else = sample + if args.samplesheet: + ss = pd.read_csv(args.samplesheet) + pcol = next((c for c in ("patient_id", "patient") if c in ss.columns), None) + if pcol and "sample" in ss.columns: + pmap = dict(zip(ss["sample"].astype(str), ss[pcol].astype(str))) + df["patient_id"] = df["sample"].astype(str).map(pmap).fillna(df["sample"].astype(str)) + else: + df["patient_id"] = df["sample"].astype(str) + else: + df["patient_id"] = df["sample"].astype(str) + + # Expand each clonotype into `clone_size` per-cell rows (one row = one cell) + expanded = df.loc[df.index.repeat(df["clone_size"])].reset_index(drop=True) + expanded["annot"] = "Unannotated" # no GEX → no cell-type label + expanded["has_tcr"] = "TRUE" + expanded["paired_tcr"] = "FALSE" + expanded["cell_id"] = ["cell_%d" % i for i in range(len(expanded))] + + out_cols = ["cell_id", "sample", "patient_id", "clone_id", "clone_size", + "annot", "has_tcr", "paired_tcr"] + expanded[out_cols].to_csv(args.out, sep="\t", index=False) + print(f"[BULK_TO_EXPORT] {df['clone_id'].nunique()} clonotypes across " + f"{df['sample'].nunique()} sample(s) -> {len(expanded)} cell rows in {args.out}") + + +if __name__ == "__main__": + main() diff --git a/bin/cluster_to_sc.py b/bin/cluster_to_sc.py new file mode 100755 index 0000000..e739f30 --- /dev/null +++ b/bin/cluster_to_sc.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +""" +Bridge 2: CLUSTER_TO_SC (optional) +Maps TCRtoolkit bulk cluster assignments (CDR3b-level) onto single cells +using CDR3b as the join key, producing per-cell TSVs compatible with +SCRATCH-TCR's Consensus Clustering module inputs. + +Usage: + cluster_to_sc.py + +Pass 'NO_FILE' for any cluster file that is not available. + +Outputs (written only when input is not NO_FILE): + giana_export_cells.tsv — per-cell with giana_cluster column + gliph2_export_cells.tsv — per-cell with gliph2_cluster column +""" + +import sys +import pandas as pd + + +NO_FILE_SENTINEL = 'NO_FILE' + + +def extract_beta_cdr3(ctaa): + """Extract beta chain CDR3 from CTaa field (format: 'alpha;beta').""" + if pd.isna(ctaa): + return None + parts = str(ctaa).split(';') + return parts[1].strip().upper() if len(parts) > 1 else None + + +def map_clusters(cells: pd.DataFrame, cluster_file: str, cluster_col_name: str, + out_file: str) -> None: + """ + Join CDR3b-level cluster assignments from a TCRtoolkit output file onto + per-cell data and write the result. + + The cluster file is expected to have: + - First column: CDR3b amino acid sequence (may have a comment header) + - 'cluster' column (or second column if 'cluster' is absent) + """ + try: + cl = pd.read_csv(cluster_file, sep='\t', comment='#') + except Exception as e: + print(f"[Bridge 2] Warning: could not read {cluster_file}: {e}") + return + + cdr3_col = cl.columns[0] + cluster_col = 'cluster' if 'cluster' in cl.columns else cl.columns[1] + + cl_map = ( + cl[[cdr3_col, cluster_col]] + .rename(columns={cdr3_col: 'CDR3b', cluster_col: cluster_col_name}) + .drop_duplicates('CDR3b') + ) + cl_map['CDR3b'] = cl_map['CDR3b'].str.upper() + + merged = cells.merge(cl_map, on='CDR3b', how='left') + merged.to_csv(out_file, sep='\t', index=False) + assigned = merged[cluster_col_name].notna().sum() + print(f"[Bridge 2] {out_file}: {assigned}/{len(merged)} cells assigned a {cluster_col_name}") + + +def main(): + if len(sys.argv) < 4: + print("Usage: cluster_to_sc.py ") + sys.exit(1) + + export_cells_file = sys.argv[1] + giana_file = sys.argv[2] + gliph_file = sys.argv[3] + + cells = pd.read_csv(export_cells_file, sep='\t', low_memory=False) + + if 'CTaa' in cells.columns: + cells['CDR3b'] = cells['CTaa'].apply(extract_beta_cdr3) + elif 'junction_aa' in cells.columns: + cells['CDR3b'] = cells['junction_aa'].str.upper() + else: + raise ValueError("export_cells.tsv must contain 'CTaa' or 'junction_aa'") + + if giana_file and giana_file != NO_FILE_SENTINEL: + map_clusters(cells, giana_file, 'giana_cluster', 'giana_export_cells.tsv') + + if gliph_file and gliph_file != NO_FILE_SENTINEL: + map_clusters(cells, gliph_file, 'gliph2_cluster', 'gliph2_export_cells.tsv') + + +if __name__ == '__main__': + main() diff --git a/bin/enrich_seurat.R b/bin/enrich_seurat.R new file mode 100755 index 0000000..faeb9be --- /dev/null +++ b/bin/enrich_seurat.R @@ -0,0 +1,277 @@ +#!/usr/bin/env Rscript +# Bridge 2+: CLUSTER_TO_SC +# Maps bulk GIANA / GLIPH2 / TCRdist3 cluster assignments onto single cells. +# Adds metadata columns to the Seurat object and writes per-cell TSVs for +# the Consensus Clustering module. +# +# Usage (called from within a Nextflow work directory): +# Rscript enrich_seurat.R \ +# --seurat_rds \ +# --export_cells \ +# --tcrdist_radius + +suppressPackageStartupMessages({ + library(Seurat) + library(dplyr) + library(igraph) + library(Matrix) +}) + +# ── Argument parsing ────────────────────────────────────────────────────────── +args <- commandArgs(trailingOnly = TRUE) + +get_arg <- function(args, flag, default = NULL) { + idx <- which(args == flag) + if (length(idx) == 0) return(default) + args[idx + 1] +} + +seurat_rds_path <- get_arg(args, "--seurat_rds") +export_cells_path <- get_arg(args, "--export_cells") +tcrdist_radius <- as.numeric(get_arg(args, "--tcrdist_radius", "24")) + +if (is.null(seurat_rds_path) || is.null(export_cells_path)) { + stop("Usage: enrich_seurat.R --seurat_rds --export_cells [--tcrdist_radius ]") +} + +# ── Helpers ─────────────────────────────────────────────────────────────────── +# scRepertoire's CTaa format is "A:|B:" (pipe-delimited, +# explicit chain labels) - not the semicolon-delimited, label-free, always- +# alpha-then-beta format this used to assume. Match the "B:" segment by label +# rather than position, and strip the label, so it still works if a cell only +# has one chain or the chains are ordered differently. +extract_beta_cdr3 <- function(ctaa) { + sapply(ctaa, function(x) { + if (is.na(x) || x == "" || x == "None") return(NA_character_) + parts <- strsplit(x, "[|;]")[[1]] + beta_part <- parts[grepl("^B:", parts)] + if (length(beta_part) == 0) return(NA_character_) + toupper(trimws(sub("^B:", "", beta_part[1]))) + }, USE.NAMES = FALSE) +} + +# ── Load inputs ─────────────────────────────────────────────────────────────── +message("[Bridge 2+] Loading Seurat object: ", seurat_rds_path) +seurat_obj <- readRDS(seurat_rds_path) + +message("[Bridge 2+] Loading export_cells: ", export_cells_path) +cells <- tryCatch( + read.delim(export_cells_path, sep = "\t", stringsAsFactors = FALSE), + error = function(e) stop("Cannot read export_cells: ", e$message) +) + +# Determine CDR3b column +if ("CTaa" %in% colnames(cells)) { + cells$CDR3b <- extract_beta_cdr3(cells$CTaa) +} else if ("junction_aa" %in% colnames(cells)) { + cells$CDR3b <- toupper(cells$junction_aa) +} else { + stop("[Bridge 2+] export_cells must contain 'CTaa' or 'junction_aa'") +} + +# Determine barcode column +barcode_col <- intersect(c("barcode", "cell_id", "Barcode", "cell", "Cell"), + colnames(cells))[1] +if (is.na(barcode_col)) { + barcode_col <- colnames(cells)[1] + message("[Bridge 2+] Barcode column not found by name; using first column: ", barcode_col) +} +message("[Bridge 2+] Using barcode column: '", barcode_col, "'") + +# ── GIANA cluster mapping ───────────────────────────────────────────────────── +giana_files <- list.files(".", pattern = "_giana\\.txt$", full.names = TRUE) +giana_map <- NULL + +if (length(giana_files) > 0) { + message("[Bridge 2+] Reading ", length(giana_files), " GIANA file(s)") + parts <- lapply(giana_files, function(f) { + tryCatch({ + df <- read.delim(f, sep = "\t", comment.char = "#", + stringsAsFactors = FALSE, check.names = FALSE) + if (ncol(df) < 2) return(NULL) + cdr3_col <- colnames(df)[1] + cluster_col <- if ("cluster" %in% colnames(df)) "cluster" else colnames(df)[2] + patient <- sub("_giana\\.txt$", "", basename(f)) + df %>% + select(CDR3b = all_of(cdr3_col), raw = all_of(cluster_col)) %>% + mutate(CDR3b = toupper(CDR3b), + giana_cluster = paste0(patient, "_G", raw)) %>% + select(CDR3b, giana_cluster) %>% + distinct(CDR3b, .keep_all = TRUE) + }, error = function(e) { + message("[Bridge 2+] Warning: skipping GIANA file ", f, ": ", e$message) + NULL + }) + }) + giana_map <- bind_rows(Filter(Negate(is.null), parts)) %>% + distinct(CDR3b, .keep_all = TRUE) + message("[Bridge 2+] GIANA: ", nrow(giana_map), " unique CDR3b sequences mapped") +} else { + message("[Bridge 2+] No GIANA files found — skipping GIANA annotation") +} + +# ── GLIPH2 cluster mapping ──────────────────────────────────────────────────── +gliph2_files <- list.files(".", pattern = "cluster_member_details.*\\.txt$", + full.names = TRUE, recursive = TRUE) +gliph2_map <- NULL + +if (length(gliph2_files) > 0) { + message("[Bridge 2+] Reading ", length(gliph2_files), " GLIPH2 file(s)") + parts <- lapply(gliph2_files, function(f) { + tryCatch({ + df <- read.delim(f, sep = "\t", stringsAsFactors = FALSE, check.names = FALSE) + if (!"CDR3b" %in% colnames(df)) return(NULL) + # 'tag' is the GLIPH2 cluster/motif identifier + cluster_col <- intersect(c("tag", "seq_ID", "ultCDR3b"), colnames(df))[1] + if (is.na(cluster_col)) cluster_col <- colnames(df)[6] + # Patient identity comes from the filename (${patient}_cluster_member_details.txt); + # fall back to the parent directory for legacy layouts. + patient_dir <- sub("_cluster_member_details.*$", "", basename(f)) + if (patient_dir == basename(f)) patient_dir <- basename(dirname(f)) + df %>% + select(CDR3b = CDR3b, raw = all_of(cluster_col)) %>% + mutate(CDR3b = toupper(CDR3b), + gliph2_cluster = paste0(patient_dir, "_", raw)) %>% + select(CDR3b, gliph2_cluster) %>% + distinct(CDR3b, .keep_all = TRUE) + }, error = function(e) { + message("[Bridge 2+] Warning: skipping GLIPH2 file ", f, ": ", e$message) + NULL + }) + }) + gliph2_map <- bind_rows(Filter(Negate(is.null), parts)) %>% + distinct(CDR3b, .keep_all = TRUE) + message("[Bridge 2+] GLIPH2: ", nrow(gliph2_map), " unique CDR3b sequences mapped") +} else { + message("[Bridge 2+] No GLIPH2 files found — skipping GLIPH2 annotation") +} + +# ── TCRdist3 cluster mapping (connected components at radius) ───────────────── +clone_df_files <- list.files(".", pattern = "_clone_df\\.csv$", full.names = TRUE) +tcrdist_map <- NULL + +if (length(clone_df_files) > 0) { + message("[Bridge 2+] Processing ", length(clone_df_files), + " TCRdist3 sample(s) at radius ", tcrdist_radius) + + parts <- lapply(clone_df_files, function(cdf_path) { + tryCatch({ + sample_name <- sub("_clone_df\\.csv$", "", basename(cdf_path)) + clone_df <- read.csv(cdf_path, stringsAsFactors = FALSE, + check.names = FALSE) + + # Find matching distance matrix + mat_hdf5 <- paste0(sample_name, "_distance_matrix.hdf5") + mat_csv <- paste0(sample_name, "_distance_matrix.csv") + + if (file.exists(mat_hdf5) && requireNamespace("rhdf5", quietly = TRUE)) { + data <- rhdf5::h5read(mat_hdf5, "data") + indices <- rhdf5::h5read(mat_hdf5, "indices") + indptr <- rhdf5::h5read(mat_hdf5, "indptr") + shape <- as.integer(rhdf5::h5read(mat_hdf5, "shape")) + mat <- Matrix::sparseMatrix( + i = as.integer(indices) + 1L, + p = as.integer(indptr), + x = as.numeric(data), + dims = shape, + repr = "C" + ) + # Sentinel -1 encodes true zero-distance pairs (stored as -1 to + # distinguish from structural zeros in the sparse format) + mat@x[mat@x == -1] <- 0 + adj <- (mat > 0) & (mat <= tcrdist_radius) + } else if (file.exists(mat_csv)) { + mat <- as.matrix(read.csv(mat_csv, header = FALSE)) + adj <- (mat > 0) & (mat <= tcrdist_radius) + } else { + message("[Bridge 2+] No distance matrix for sample '", sample_name, + "' — skipping") + return(NULL) + } + + # Connected components → cluster IDs + g <- igraph::graph_from_adjacency_matrix(adj, mode = "undirected", + diag = FALSE) + comps <- igraph::components(g) + n <- nrow(clone_df) + + cdr3_col <- intersect(c("junction_aa", "CDR3b", "cdr3_b_aa", + "sequence_id"), colnames(clone_df))[1] + if (is.na(cdr3_col)) { + message("[Bridge 2+] Cannot identify CDR3b column in clone_df for '", + sample_name, "' — skipping") + return(NULL) + } + + data.frame( + CDR3b = toupper(clone_df[[cdr3_col]][seq_len(n)]), + tcrdist_cluster = paste0(sample_name, "_T", + comps$membership[seq_len(n)]), + stringsAsFactors = FALSE + ) %>% distinct(CDR3b, .keep_all = TRUE) + + }, error = function(e) { + message("[Bridge 2+] Warning: TCRdist3 clustering failed for '", + cdf_path, "': ", e$message) + NULL + }) + }) + + valid_parts <- Filter(Negate(is.null), parts) + if (length(valid_parts) == 0) { + tcrdist_map <- NULL + message("[Bridge 2+] TCRdist3: all samples failed or had no distance matrix — skipping") + } else { + tcrdist_map <- bind_rows(valid_parts) %>% + distinct(CDR3b, .keep_all = TRUE) + message("[Bridge 2+] TCRdist3: ", nrow(tcrdist_map), " unique CDR3b sequences mapped") + } +} else { + message("[Bridge 2+] No TCRdist3 clone_df files found — skipping TCRdist3 annotation") +} + +# ── Write per-cell export TSVs (for Consensus Clustering) ──────────────────── +write_export_tsv <- function(cells, cluster_map, cluster_col, out_file) { + if (is.null(cluster_map) || nrow(cluster_map) == 0) return(invisible(NULL)) + merged <- left_join(cells, cluster_map, by = "CDR3b") + assigned <- sum(!is.na(merged[[cluster_col]])) + message(sprintf("[Bridge 2+] %s: %d / %d cells assigned a %s", + out_file, assigned, nrow(merged), cluster_col)) + write.table(merged, out_file, sep = "\t", row.names = FALSE, quote = FALSE) +} + +write_export_tsv(cells, giana_map, "giana_cluster", "giana_export_cells.tsv") +write_export_tsv(cells, gliph2_map, "gliph2_cluster", "gliph2_export_cells.tsv") +write_export_tsv(cells, tcrdist_map, "tcrdist_cluster", "tcrdist_export_cells.tsv") + +# ── Add cluster columns to Seurat metadata ──────────────────────────────────── +add_to_seurat <- function(seurat_obj, cells, cluster_map, col_name, barcode_col) { + if (is.null(cluster_map) || nrow(cluster_map) == 0) return(seurat_obj) + joined <- left_join( + cells[, c(barcode_col, "CDR3b"), drop = FALSE], + cluster_map, + by = "CDR3b" + ) + vec <- setNames(joined[[col_name]], joined[[barcode_col]]) + common <- intersect(names(vec), colnames(seurat_obj)) + if (length(common) == 0) { + message("[Bridge 2+] No barcode overlap for '", col_name, + "' — check barcode format matches Seurat colnames") + return(seurat_obj) + } + meta_vec <- vec[colnames(seurat_obj)] + seurat_obj[[col_name]] <- unname(meta_vec) + n_ann <- sum(!is.na(seurat_obj[[col_name]])) + message(sprintf("[Bridge 2+] Added '%s' to Seurat: %d / %d cells annotated", + col_name, n_ann, ncol(seurat_obj))) + seurat_obj +} + +seurat_obj <- add_to_seurat(seurat_obj, cells, giana_map, "giana_cluster", barcode_col) +seurat_obj <- add_to_seurat(seurat_obj, cells, gliph2_map, "gliph2_cluster", barcode_col) +seurat_obj <- add_to_seurat(seurat_obj, cells, tcrdist_map, "tcrdist_cluster", barcode_col) + +# ── Save enriched Seurat ────────────────────────────────────────────────────── +message("[Bridge 2+] Saving enriched Seurat → enriched_seurat.rds") +saveRDS(seurat_obj, "enriched_seurat.rds") +message("[Bridge 2+] Done.") diff --git a/bin/pseudobulk_qc.py b/bin/pseudobulk_qc.py new file mode 100755 index 0000000..41554fa --- /dev/null +++ b/bin/pseudobulk_qc.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +""" +Pseudobulk QC gate — per-sample metrics + gene-family usage. + +Computes a lightweight QC summary for a single pseudobulk-derived TCR sample +table produced by either single-cell → bulk bridge: + + - SC_TO_BULK (AIRR-ish): junction_aa, v_call, j_call, duplicate_count + - SC_TO_CDR3 : CDR3b, TRBV, TRBJ, counts + +Metrics (intentionally minimal — diversity metrics such as Shannon/Simpson are +NOT computed here because the REPERTOIRE module already reports them): + + n_clones : number of unique clonotype rows + n_cells : total cells (sum of the per-clone count column) + +A sample PASSes the gate when n_clones >= min_clones AND n_cells >= min_cells. + +Also emits non-redundant gene-family usage (V-family clone counts, TRBV1..30) +for reporting. + +Usage: + pseudobulk_qc.py + +Outputs (cwd): + qc_.csv sample,n_clones,n_cells,min_clones,min_cells,pass + vfamily_.csv sample,TRBV1,...,TRBV30 +""" + +import sys +import re +import pandas as pd + + +def resolve_col(df, *candidates): + """Return the first candidate column present in df, else None.""" + for c in candidates: + if c in df.columns: + return c + return None + + +def extract_trbv_family(allele): + """Map a V allele (e.g. 'TRBV20-1*01') to its family ('TRBV20').""" + if pd.isna(allele): + return None + m = re.match(r'(TRBV)(\d+)', str(allele)) + return f"{m.group(1)}{m.group(2)}" if m else None + + +def main(): + if len(sys.argv) < 5: + sys.exit("Usage: pseudobulk_qc.py ") + + sample_tsv = sys.argv[1] + sample = sys.argv[2] + min_clones = int(sys.argv[3]) + min_cells = int(sys.argv[4]) + + df = pd.read_csv(sample_tsv, sep='\t', low_memory=False) + + # Auto-detect the count column (AIRR 'duplicate_count' vs SC_TO_CDR3 'counts') + count_col = resolve_col(df, 'duplicate_count', 'counts', 'count') + v_col = resolve_col(df, 'v_call', 'TRBV', 'trbv') + + n_clones = int(len(df)) + if count_col is not None: + n_cells = int(pd.to_numeric(df[count_col], errors='coerce').fillna(0).sum()) + else: + # No count column → treat each row as a single cell. + n_cells = n_clones + + passed = (n_clones >= min_clones) and (n_cells >= min_cells) + + pd.DataFrame([{ + 'sample': sample, + 'n_clones': n_clones, + 'n_cells': n_cells, + 'min_clones': min_clones, + 'min_cells': min_cells, + 'pass': 'PASS' if passed else 'FAIL', + }]).to_csv(f'qc_{sample}.csv', index=False) + + # ── V gene family usage (clone counts per TRBV family) ──────────────────── + all_fams = [f'TRBV{i}' for i in range(1, 31)] + if v_col is not None: + fam_counts = df[v_col].apply(extract_trbv_family).value_counts(dropna=True) + else: + fam_counts = pd.Series(dtype=int) + + row = {'sample': sample} + for fam in all_fams: + row[fam] = int(fam_counts.get(fam, 0)) + pd.DataFrame([row]).to_csv(f'vfamily_{sample}.csv', index=False) + + # Human-readable log line + print(f"[Pseudobulk QC] {sample}: n_clones={n_clones}, n_cells={n_cells} -> " + f"{'PASS' if passed else 'FAIL'} (min_clones={min_clones}, min_cells={min_cells})") + + +if __name__ == '__main__': + main() diff --git a/bin/sc_to_bulk.py b/bin/sc_to_bulk.py new file mode 100755 index 0000000..ab57eb8 --- /dev/null +++ b/bin/sc_to_bulk.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +""" +Bridge 1: SC_TO_BULK +Converts SCRATCH-TCR per-cell export_cells.tsv → per-sample AIRR-format TSVs. + +Usage: + sc_to_bulk.py [] + +Outputs: + bulk_samples/_bulk.tsv — one AIRR TSV per sample + synthetic_samplesheet.csv — TCRtoolkit-compatible samplesheet + +Column resolution priority for junction_aa / v_call / j_call: + 1. Dedicated columns: cdr3b, trbv, trbj (TCELL_INTEGRATION export format) + 2. CTaa / CTgene parsing (legacy scRepertoire semicolon format) + 3. Pre-existing junction_aa column (already AIRR format) +""" + +import sys +import os +import pandas as pd + + +def extract_beta_ctaa(field): + """Extract beta CDR3 from CTaa. + Handles both: + - semicolon-separated: 'ALPHA;BETA' + - pipe + label format: 'A:ALPHA|B:BETA' + """ + if pd.isna(field) or not field: + return None + s = str(field) + # pipe + label format (A:...|B:...) + if '|' in s: + for part in s.split('|'): + part = part.strip() + if part.upper().startswith('B:'): + return part[2:] or None + return None + # semicolon-separated format + parts = s.split(';') + return parts[1] if len(parts) > 1 else None + + +def extract_beta_ctgene(field): + """Extract beta gene string from CTgene (same split logic as CTaa).""" + return extract_beta_ctaa(field) + + +def extract_trbv(ctgene_beta): + if not ctgene_beta: + return None + genes = ctgene_beta.split('.') + return genes[0] if genes else None + + +def extract_trbj(ctgene_beta): + if not ctgene_beta: + return None + genes = ctgene_beta.split('.') + return genes[-1] if len(genes) > 1 else None + + +def main(): + if len(sys.argv) < 3: + print("Usage: sc_to_bulk.py [meta_cols_csv]") + sys.exit(1) + + export_cells_file = sys.argv[1] + sample_col = sys.argv[2] + meta_cols = [c for c in sys.argv[3].split(',') if c] if len(sys.argv) > 3 else [] + + df = pd.read_csv(export_cells_file, sep='\t', low_memory=False) + + # ── Resolve junction_aa (beta CDR3) ────────────────────────────────────── + # Priority: dedicated cdr3b col > CTaa parsing > existing junction_aa col + if 'cdr3b' in df.columns: + df['junction_aa'] = df['cdr3b'].where(df['cdr3b'].notna() & (df['cdr3b'] != '')) + print("[Bridge 1] Using 'cdr3b' column for junction_aa.") + elif 'CTaa' in df.columns: + df['junction_aa'] = df['CTaa'].apply(extract_beta_ctaa) + print("[Bridge 1] Parsed junction_aa from CTaa column.") + elif 'junction_aa' in df.columns: + print("[Bridge 1] Using pre-existing junction_aa column.") + else: + raise ValueError("export_cells.tsv must contain 'cdr3b', 'CTaa', or 'junction_aa'.") + + # ── Resolve v_call / j_call ─────────────────────────────────────────────── + if 'trbv' in df.columns: + df['v_call'] = df['trbv'].where(df['trbv'].notna() & (df['trbv'] != '')) + print("[Bridge 1] Using 'trbv' column for v_call.") + elif 'CTgene' in df.columns: + beta_gene = df['CTgene'].apply(extract_beta_ctgene) + df['v_call'] = beta_gene.apply(extract_trbv) + else: + df['v_call'] = None + + if 'trbj' in df.columns: + df['j_call'] = df['trbj'].where(df['trbj'].notna() & (df['trbj'] != '')) + print("[Bridge 1] Using 'trbj' column for j_call.") + elif 'CTgene' in df.columns: + beta_gene = df['CTgene'].apply(extract_beta_ctgene) + df['j_call'] = beta_gene.apply(extract_trbj) + else: + df['j_call'] = None + + # ── Keep only cells with a valid beta CDR3 ──────────────────────────────── + df = df[df['junction_aa'].notna() & (df['junction_aa'] != 'None') & (df['junction_aa'] != '')] + print(f"[Bridge 1] {len(df)} cells retained after filtering for valid junction_aa.") + + # ── Resolve sample column ───────────────────────────────────────────────── + if sample_col not in df.columns: + if 'sample' in df.columns: + print(f"[Bridge 1] WARNING: sample_col '{sample_col}' not found; falling back to 'sample'.") + sample_col = 'sample' + else: + raise ValueError(f"sample_col '{sample_col}' not found in export_cells.tsv. " + f"Available columns: {list(df.columns)}") + + os.makedirs('bulk_samples', exist_ok=True) + samplesheet_rows = [] + + for sample, grp in df.groupby(sample_col): + agg = ( + grp.groupby(['junction_aa', 'v_call', 'j_call']) + .size() + .reset_index(name='duplicate_count') + ) + total = agg['duplicate_count'].sum() + agg['duplicate_frequency_percent'] = (agg['duplicate_count'] / total * 100).round(6) + agg['sequence_id'] = agg['junction_aa'] + agg['sequence'] = agg['junction_aa'] # placeholder; NT not available from per-cell table + + out_cols = [ + 'sequence_id', 'junction_aa', 'v_call', 'j_call', + 'duplicate_count', 'duplicate_frequency_percent', 'sequence' + ] + out_path = f'bulk_samples/{sample}_bulk.tsv' + agg[out_cols].to_csv(out_path, sep='\t', index=False) + + row = {'sample': sample, 'file': out_path} + for col in meta_cols: + if col in grp.columns: + row[col] = grp[col].iloc[0] + samplesheet_rows.append(row) + + ss = pd.DataFrame(samplesheet_rows) + ss.to_csv('synthetic_samplesheet.csv', index=False) + + print(f"[Bridge 1] Created {len(samplesheet_rows)} bulk sample files in bulk_samples/") + print(f"[Bridge 1] Synthetic samplesheet written to synthetic_samplesheet.csv") + + +if __name__ == '__main__': + main() diff --git a/bin/sc_to_cdr3.py b/bin/sc_to_cdr3.py new file mode 100755 index 0000000..34308c6 --- /dev/null +++ b/bin/sc_to_cdr3.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +""" +Bridge: SC_TO_CDR3 +Converts TCELL_INTEGRATION per-cell export_cells.tsv into the canonical TCRtoolkit +AIRR clonotype schema (per pseudobulk unit), bypassing ANNOTATE_PROCESS. + +Pooling policy (integration design decision): + * Default — pool by SAMPLE. Each sample is one unit; clonotypes are grouped by + (junction_aa, v_call, j_call). Patient is carried as metadata so the shared + PATIENT step pools per-patient for GIANA/GLIPH2/tcrdist clustering, and + phenotype (annot) enters downstream via CLUSTER_TO_SC / CONGA / repertoire. + * Optional (--by-phenotype) — secondary per-cell-type view. Units become + (sample x annot), id = "{sample}__{annot}". Under-powered units are dropped + later by PSEUDOBULK_QC (min clones/cells). Off by default. + +Schema conformance: output columns + ORDER match ANNOTATE_PROCESS so the shared +bulk engine runs unmodified (positional sort/dedup: $1=junction_aa, $2=v_call): + + junction_aa, v_call, d_call, j_call, duplicate_count, + junction_aa_length, duplicate_frequency_percent, + sequence, sequence_id, junction, sample + +Outputs: + per_sample/{unit}_cdr3.tsv — one file per pseudobulk unit + concat_cdr3.tsv — all units concatenated (input for ANNOTATE_SORT_CDR3) + unit_map.csv — unit -> sample/patient/phenotype/file (for meta building) +""" + +import sys +import os +import argparse +import pandas as pd + +OUTPUT_COLS = [ + "junction_aa", "v_call", "d_call", "j_call", + "duplicate_count", "junction_aa_length", "duplicate_frequency_percent", + "sequence", "sequence_id", "junction", "sample", +] + + +def main(): + ap = argparse.ArgumentParser(description="SC_TO_CDR3 bridge") + ap.add_argument("export_cells", help="tcr_export_cells_with_embedding.tsv") + ap.add_argument("--by-phenotype", action="store_true", + help="stratify pseudobulk units by cell-type (annot); off by default") + ap.add_argument("--pheno-col", default="annot", help="phenotype column name") + args = ap.parse_args() + + df = pd.read_csv(args.export_cells, sep='\t', low_memory=False) + + # Resolve CDR3b amino-acid sequence → junction_aa + if 'cdr3b' in df.columns: + df['junction_aa'] = df['cdr3b'] + elif 'junction_aa' in df.columns: + df['junction_aa'] = df['junction_aa'] + else: + raise ValueError("Export file must contain 'cdr3b' or 'junction_aa'.") + + # Resolve V / J gene calls, ensuring an IMGT allele suffix (e.g. TRBV10-3 -> TRBV10-3*01). + # tcrdist3's reference db is keyed by allele and cannot map CDR1/CDR2 without one. + def add_allele(g): + if g is None or pd.isna(g) or str(g) in ('', 'None'): + return g + s = str(g) + return s if '*' in s else f"{s}*01" + + df['v_call'] = (df['trbv'].apply(add_allele) if 'trbv' in df.columns else None) + df['j_call'] = (df['trbj'].apply(add_allele) if 'trbj' in df.columns else None) + + if 'sample' not in df.columns: + raise ValueError("Export file must contain a 'sample' column.") + + # Patient metadata (carried so PATIENT can pool per-patient); default to sample if absent. + df['patient'] = df['patient'] if 'patient' in df.columns else df['sample'] + + # Phenotype stratification (optional) + by_pheno = args.by_phenotype and args.pheno_col in df.columns + if args.by_phenotype and not by_pheno: + print(f"[SC_TO_CDR3] --by-phenotype requested but column '{args.pheno_col}' " + f"not found; falling back to sample-level pooling.") + df['phenotype'] = df[args.pheno_col].astype(str) if by_pheno else '' + df['unit'] = (df['sample'].astype(str) + '__' + df['phenotype']) if by_pheno else df['sample'].astype(str) + + # Keep only cells with a valid beta CDR3 + df = df[df['junction_aa'].notna() & (df['junction_aa'] != '') & (df['junction_aa'] != 'None')] + print(f"[SC_TO_CDR3] {len(df)} cells with valid junction_aa across " + f"{df['unit'].nunique()} unit(s) (by_phenotype={by_pheno}).") + + os.makedirs('per_sample', exist_ok=True) + all_frames = [] + unit_rows = [] + + for unit, grp in df.groupby('unit'): + agg = ( + grp.groupby(['junction_aa', 'v_call', 'j_call'], dropna=False) + .size() + .reset_index(name='duplicate_count') + ) + agg['d_call'] = '' + total = agg['duplicate_count'].sum() + agg['duplicate_frequency_percent'] = (agg['duplicate_count'] / total * 100).round(6) if total else 0.0 + agg['junction_aa_length'] = agg['junction_aa'].astype(str).str.len() + agg['sequence'] = '' + agg['sequence_id'] = agg['junction_aa'] + agg['junction'] = '' + agg['sample'] = unit + agg = agg[OUTPUT_COLS] + + out_path = f'per_sample/{unit}_cdr3.tsv' + agg.to_csv(out_path, sep='\t', index=False) + all_frames.append(agg) + + # First non-null patient / phenotype for this unit + patient = grp['patient'].dropna().astype(str) + patient = patient.iloc[0] if len(patient) else '' + phenotype = grp['phenotype'].iloc[0] if len(grp) else '' + unit_rows.append({'sample': unit, 'patient': patient, + 'phenotype': phenotype, 'file': os.path.abspath(out_path)}) + print(f"[SC_TO_CDR3] {unit}: {len(agg)} clonotypes (patient={patient}).") + + if not all_frames: + raise ValueError("[SC_TO_CDR3] No clonotypes found after filtering.") + + concat = pd.concat(all_frames, ignore_index=True) + concat.to_csv('concat_cdr3.tsv', sep='\t', index=False) + pd.DataFrame(unit_rows).to_csv('unit_map.csv', index=False) + print(f"[SC_TO_CDR3] concat_cdr3.tsv ({len(concat)} rows), unit_map.csv ({len(unit_rows)} units) written.") + + +if __name__ == '__main__': + main() diff --git a/bin/tcrdist3_matrix.py b/bin/tcrdist3_matrix.py index 40386d8..b725891 100755 --- a/bin/tcrdist3_matrix.py +++ b/bin/tcrdist3_matrix.py @@ -199,6 +199,16 @@ def find_matching_gene(row, db): # If allele information is not specified (as indicated by *00), replace with *01 df['v_b_gene'] = df['v_b_gene'].apply(lambda x: x.replace('*00', '*01')) + # Single-cell pseudobulk carries no CDR3 nucleotide sequence. tcrdist3 uses cdr3_b_nucseq + # as a clone grouping/index column, so an all-empty nucseq would drop every clone + # ("N of N were not captured" -> empty matrix). Beta-chain tcrdist distances are computed + # from cdr3_b_aa and V-gene-derived CDR1/CDR2, not the nucleotide sequence, so drop the + # column when it is empty. (Bulk / AIRR input has real nucseq and is unaffected.) + if 'cdr3_b_nucseq' in df.columns: + _ns = df['cdr3_b_nucseq'].astype('string').fillna('').str.strip() + if (_ns == '').all(): + df = df.drop(columns=['cdr3_b_nucseq']) + # --- 2. Calculate distance matrix --- # Levenshtein distance matrix if args.distance_metric == "levenshtein": diff --git a/bin/vdj_to_bulk.py b/bin/vdj_to_bulk.py new file mode 100755 index 0000000..559dc01 --- /dev/null +++ b/bin/vdj_to_bulk.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +""" +Bridge 3: VDJ_TO_BULK +Converts VDJ_QC's contigs_after_qc.tsv (per-contig, one row per chain per cell) +into per-sample AIRR-format TSVs for TCRtoolkit — used when there is no GEX object +and TCELL_INTEGRATION is skipped. + +Input columns expected (from VDJ_QC notebook): + sample, barcode, chain, cdr3, v_gene, j_gene + optionally: patient_id, condition, timepoint, cdr3_nt + +Only TRB (beta) chain rows are used. + +Usage: + vdj_to_bulk.py [] + +Outputs: + bulk_samples/_bulk.tsv — one AIRR TSV per sample + synthetic_samplesheet.csv — TCRtoolkit-compatible samplesheet +""" + +import sys +import os +import pandas as pd + + +META_COLS = ['patient_id', 'condition', 'timepoint'] + + +def main(): + import argparse + ap = argparse.ArgumentParser(description="VDJ_TO_BULK bridge") + ap.add_argument("contigs_after_qc") + ap.add_argument("sample_col", nargs="?", default="sample") + ap.add_argument("--sample-sheet", default=None, + help="user SC sample sheet (CSV); patient_id/condition/timepoint are " + "joined from here by sample when the contigs lack them") + args = ap.parse_args() + + contigs_file = args.contigs_after_qc + sample_col = args.sample_col + + # Optional per-sample metadata from the user's sample sheet (keyed by 'sample'). + meta_lookup = {} + if args.sample_sheet: + ss_in = pd.read_csv(args.sample_sheet) + key = 'sample' if 'sample' in ss_in.columns else ss_in.columns[0] + for _, r in ss_in.iterrows(): + meta_lookup[str(r[key])] = r + + df = pd.read_csv(contigs_file, sep='\t', low_memory=False) + + required = {'chain', 'cdr3', sample_col} + missing = required - set(df.columns) + if missing: + raise ValueError(f"contigs_after_qc.tsv is missing columns: {missing}. " + f"Available: {list(df.columns)}") + + # keep beta chain only, drop rows with no CDR3 + df = df[df['chain'] == 'TRB'].copy() + df = df[df['cdr3'].notna() & (df['cdr3'].astype(str) != 'NA')] + + if df.empty: + raise ValueError("No TRB rows found in contigs_after_qc.tsv after filtering.") + + # Ensure gene calls carry an IMGT allele suffix (e.g. TRBV10-3 -> TRBV10-3*01). + # Cell Ranger reports bare gene names, but tcrdist3's reference db is keyed by allele + # and cannot map CDR1/CDR2 without it (empty clones -> crash). *01 is the safe default, + # matching tcrdist3_matrix.py's own *00 -> *01 fallback. + def add_allele(g): + if g is None or pd.isna(g) or str(g) == '' or str(g) == 'None': + return g + s = str(g) + return s if '*' in s else f"{s}*01" + + df['junction_aa'] = df['cdr3'].str.upper() + df['v_call'] = (df['v_gene'] if 'v_gene' in df.columns else None) + df['d_call'] = df['d_gene'] if 'd_gene' in df.columns else None + df['j_call'] = df['j_gene'] if 'j_gene' in df.columns else None + df['sequence'] = df['cdr3_nt'] if 'cdr3_nt' in df.columns else df['junction_aa'] + + if 'v_gene' in df.columns: + df['v_call'] = df['v_call'].apply(add_allele) + if 'j_gene' in df.columns: + df['j_call'] = df['j_call'].apply(add_allele) + + os.makedirs('bulk_samples', exist_ok=True) + samplesheet_rows = [] + + for sample, grp in df.groupby(sample_col): + agg = ( + grp.groupby(['junction_aa', 'v_call', 'd_call', 'j_call']) + .agg( + duplicate_count=('barcode', 'count'), + sequence=('sequence', 'first') + ) + .reset_index() + ) + total = agg['duplicate_count'].sum() + agg['duplicate_frequency_percent'] = (agg['duplicate_count'] / total * 100).round(6) + agg['sequence_id'] = agg['junction_aa'] + agg['junction'] = agg['sequence'] + agg['junction_aa_length'] = agg['junction_aa'].str.len() + agg['sample'] = sample + + # Canonical schema + ORDER matching ANNOTATE_PROCESS so the shared engine's + # positional sort/dedup ($1=junction_aa, $2=v_call) works. See sc_to_cdr3.py. + out_cols = [ + 'junction_aa', 'v_call', 'd_call', 'j_call', + 'duplicate_count', 'junction_aa_length', 'duplicate_frequency_percent', + 'sequence', 'sequence_id', 'junction', 'sample', + ] + out_path = f'bulk_samples/{sample}_bulk.tsv' + agg[out_cols].to_csv(out_path, sep='\t', index=False) + + row = {'sample': sample, 'file': os.path.abspath(out_path)} + smeta = meta_lookup.get(str(sample)) + for col in META_COLS: + val = '' + # Prefer the contigs, then the user sample sheet. + if col in grp.columns: + nn = grp[col].dropna() + val = nn.iloc[0] if len(nn) > 0 else '' + if (val == '' or pd.isna(val)) and smeta is not None and col in smeta.index: + val = smeta[col] + row[col] = '' if pd.isna(val) else val + # Never leave patient_id empty (it drives PATIENT grouping) — fall back to sample. + if row.get('patient_id', '') in ('', None) or pd.isna(row.get('patient_id', '')): + row['patient_id'] = sample + samplesheet_rows.append(row) + + ss = pd.DataFrame(samplesheet_rows) + ss.to_csv('synthetic_samplesheet.csv', index=False) + + print(f"[Bridge 3] Created {len(samplesheet_rows)} bulk sample files in bulk_samples/") + print(f"[Bridge 3] Synthetic samplesheet written to synthetic_samplesheet.csv") + + +if __name__ == '__main__': + main() diff --git a/conf/base.config b/conf/base.config index 6e5785a..e6daa66 100644 --- a/conf/base.config +++ b/conf/base.config @@ -72,4 +72,15 @@ process { maxRetries = 2 } + // ── Single-cell modality overrides (integration) ────────────────────── + // Cell-level SC processes run in the SCRATCH-TCR image (Seurat/scanpy/CONGA/ + // tcrdist3). The default container (params.container = tcrtoolkit:main) is kept + // for bulk and the shared engine (ANNOTATE/SAMPLE/PATIENT/COMPARE/GIANA/GLIPH2/ + // OLGA/TCRDIST3_MATRIX/SC_SAMPLE_STATS + the pandas bridges), which is correct. + withName: 'VDJ_QC|TCELL_INTEGRATION|CONGA|CONSENSUS_CLUSTERING|REPERTOIRE|MASTER_SUMMARY|CLUSTER_TO_SC|TCRI' { + container = "${params.sc_container}" + cpus = { 8 * task.attempt } + memory = { 60.GB * task.attempt } + time = { 24.h * task.attempt } + } } \ No newline at end of file diff --git a/main.nf b/main.nf index 2cf813a..0a97872 100644 --- a/main.nf +++ b/main.nf @@ -13,7 +13,8 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -include { TCRTOOLKIT } from './workflows/tcrtoolkit.nf' +include { TCRTOOLKIT } from './workflows/tcrtoolkit.nf' +include { SINGLECELL_WORKFLOW } from './workflows/singlecell.nf' /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -22,7 +23,17 @@ include { TCRTOOLKIT } from './workflows/tcrtoolkit.nf' */ workflow { - TCRTOOLKIT() + // Modality dispatch. Default is 'bulk' so existing bulk runs are unaffected + // when --mode is omitted. + def mode = (params.mode ?: 'bulk').toLowerCase() + + if (mode == 'bulk') { + TCRTOOLKIT() + } else if (mode == 'singlecell') { + SINGLECELL_WORKFLOW() + } else { + error "Unknown --mode '${mode}'. Valid options: bulk | singlecell" + } } /* diff --git a/modules/bridges/bulk_to_export.nf b/modules/bridges/bulk_to_export.nf new file mode 100644 index 0000000..a20cc0b --- /dev/null +++ b/modules/bridges/bulk_to_export.nf @@ -0,0 +1,27 @@ +/* + * Bridge: BULK_TO_EXPORT + * + * Builds a per-cell export table from pseudobulk clonotype data so REPERTOIRE and + * MASTER_SUMMARY can run in the VDJ-only route (no GEX Seurat object). Each clonotype + * is expanded into duplicate_count synthetic "cells"; annotation is constant + * ("Unannotated") since there is no gene-expression data. + */ +process BULK_TO_EXPORT { + tag "bulk → export_cells" + label 'process_low' + container "${params.container}" + publishDir "${params.outdir}/bridge/bulk_to_export", mode: 'copy', overwrite: true + + input: + path concat_cdr3 + path samplesheet + + output: + path "export_cells.tsv", emit: export_cells + + script: + def ss = (samplesheet && samplesheet.name != 'NO_FILE') ? "--samplesheet ${samplesheet}" : "" + """ + bulk_to_export.py ${concat_cdr3} ${ss} --out export_cells.tsv + """ +} diff --git a/modules/bridges/cluster_to_sc.nf b/modules/bridges/cluster_to_sc.nf new file mode 100644 index 0000000..e1af5af --- /dev/null +++ b/modules/bridges/cluster_to_sc.nf @@ -0,0 +1,49 @@ +/* + * Bridge 2+: CLUSTER_TO_SC + * + * Maps bulk GIANA / GLIPH2 / TCRdist3 cluster assignments back onto single + * cells via CDR3b as the join key. + * + * All cluster files are discovered by pattern in the working directory: + * *_giana.txt - GIANA per-patient output + * cluster_member_details*.txt - GLIPH2 per-patient output + * *_clone_df.csv - TCRdist3 per-sample clone table + * *_distance_matrix.* - TCRdist3 per-sample distance matrix + * + * Outputs: + * enriched_seurat.rds - Seurat with giana/gliph2/tcrdist cluster columns + * giana_export_cells.tsv - per-cell TSV with giana_cluster column + * gliph2_export_cells.tsv - per-cell TSV with gliph2_cluster column + * tcrdist_export_cells.tsv - per-cell TSV with tcrdist_cluster column + */ + +process CLUSTER_TO_SC { + tag "Bridge 2+ - Cluster to SC annotation" + label 'process_medium' + container "${params.container}" + + publishDir "${params.outdir}/bridge/cluster_to_sc", mode: 'copy', overwrite: true + + input: + path seurat_rds + path export_cells + path giana_files // collected *_giana.txt files (one per patient) + path gliph2_files // collected cluster_member_details dirs/files + path tcrdist_clone_dfs // collected *_clone_df.csv files (one per sample) + path tcrdist_matrices // collected *_distance_matrix.* files (one per sample) + val tcrdist_radius + + output: + path "enriched_seurat.rds", emit: enriched_seurat + path "giana_export_cells.tsv", emit: giana_export, optional: true + path "gliph2_export_cells.tsv", emit: gliph2_export, optional: true + path "tcrdist_export_cells.tsv", emit: tcrdist_export, optional: true + + script: + """ + Rscript ${projectDir}/bin/enrich_seurat.R \\ + --seurat_rds "${seurat_rds}" \\ + --export_cells "${export_cells}" \\ + --tcrdist_radius ${tcrdist_radius} + """ +} diff --git a/modules/bridges/sc_sample_stats.nf b/modules/bridges/sc_sample_stats.nf new file mode 100644 index 0000000..1db3f22 --- /dev/null +++ b/modules/bridges/sc_sample_stats.nf @@ -0,0 +1,40 @@ +/* + * SC_SAMPLE_STATS (bridge) + * + * Synthesizes the pre-filter-stats sidecar that main's SAMPLE_CALC expects, for + * single-cell-derived pseudobulk data. SC pseudobulk is productive-only by + * construction (junction rebuilt from paired VDJ / GEX-annotated cells), so: + * total_clones = productive_clones = n rows ; nonproductive_clones = 0 + * + * This lets the single-cell modality reuse main's unmodified 4-arg SAMPLE + * subworkflow (Option A). Format matches ANNOTATE_PROCESS's pre_filter_stats: + * columns: sample,total_clones,productive_clones,nonproductive_clones (one row). + */ +process SC_SAMPLE_STATS { + tag "${sample_meta.sample}" + label 'process_low' + publishDir enabled: false + + input: + tuple val(sample_meta), path(count_table) + + output: + tuple val(sample_meta), path("${sample_meta.sample}_pre_filter_stats.csv"), emit: "pre_filter_stats" + + script: + """ + python - < ${patient}/local_similarities.txt + + # Copy to patient-prefixed top-level names to avoid basename collisions + # when multiple patients' outputs are staged together downstream. + cp ${patient}/all_motifs.txt ${patient}_all_motifs.txt + cp ${patient}/clone_network.txt ${patient}_clone_network.txt + cp ${patient}/cluster_member_details.txt ${patient}_cluster_member_details.txt + cp ${patient}/global_similarities.txt ${patient}_global_similarities.txt """ } diff --git a/modules/local/pseudobulk_qc/main.nf b/modules/local/pseudobulk_qc/main.nf new file mode 100644 index 0000000..c7594e5 --- /dev/null +++ b/modules/local/pseudobulk_qc/main.nf @@ -0,0 +1,71 @@ +/* + * PSEUDOBULK_QC + * + * TCRtoolkit QC gate for single-cell → bulk (pseudobulk) TCR data. + * + * PSEUDOBULK_QC_CALC computes per-sample n_clones / n_cells and a + * PASS/FAIL flag against the configured thresholds, + * plus non-redundant V gene-family usage. + * + * PSEUDOBULK_QC_AGGREGATE concatenates per-sample CSVs into a single table + * (QC summary or V-family usage) for reporting. + * + * Diversity metrics (Shannon/Simpson/etc.) are deliberately NOT computed here — + * the REPERTOIRE module already reports them on the same cell counts. + */ + +process PSEUDOBULK_QC_CALC { + tag "${meta.sample}" + label 'process_single' + container "${params.container}" + publishDir enabled: false + + input: + tuple val(meta), path(sample_file) + val min_clones + val min_cells + + output: + tuple val(meta), path(sample_file), env('QC_PASS'), env('N_CLONES'), env('N_CELLS'), emit: scored + path "qc_${meta.sample}.csv", emit: qc_csv + path "vfamily_${meta.sample}.csv", emit: v_family_csv + + script: + """ + pseudobulk_qc.py ${sample_file} '${meta.sample}' ${min_clones} ${min_cells} + + QC_PASS=\$(awk -F, 'NR==2{print \$6}' 'qc_${meta.sample}.csv') + N_CLONES=\$(awk -F, 'NR==2{print \$2}' 'qc_${meta.sample}.csv') + N_CELLS=\$(awk -F, 'NR==2{print \$3}' 'qc_${meta.sample}.csv') + """ +} + +process PSEUDOBULK_QC_AGGREGATE { + tag "${output_file}" + label 'process_low' + container "${params.container}" + publishDir "${params.outdir}/pseudobulk_qc", mode: params.publish_dir_mode, overwrite: true + + input: + path csv_files + val output_file + + output: + path output_file, emit: aggregated_csv + + script: + """ + cat > aggregate.py < + "mkdir -p \"\$(dirname '${dest}')\"; ln -sf \"\$PWD/${src}\" '${dest}'" + }.join('\n ') + def project_dir_arg = staged_layout ? "-P project_dir:'.'" : '' """ + ${stage_cmds} ## render qmd report to html quarto render ${notebook} \\ -P project_name:${project_name} \\ -P workflow_cmd:'${workflow_cmd}' \\ -P sample_table:${file(params.samplesheet)} \\ + -P subject_col:'${params.subject_col}' \\ + -P timepoint_col:'${params.timepoint_col}' \\ + -P timepoint_order_col:'${params.timepoint_order_col}' \\ + -P timepoint_order:'${params.timepoint_order}' \\ + -P alias_col:'${params.alias_col}' \\ + ${project_dir_arg} \\ --to html """ diff --git a/modules/local/sample/tcrdist3.nf b/modules/local/sample/tcrdist3.nf index f592675..7cdcad2 100644 --- a/modules/local/sample/tcrdist3.nf +++ b/modules/local/sample/tcrdist3.nf @@ -159,10 +159,15 @@ process TCRDIST3_HISTOGRAM_PLOT { plt.xlabel("Pairwise Distance") plt.ylabel("Frequency (log scale)") plt.yscale("log") - plt.ylim(0.9, max(10, ${y_max})) # avoid zero on log scale + # A degenerate/empty histogram (too few cells to form any pairs) makes + # y_max 0 - log10(0) is -inf, and int(-inf) crashes. Floor it the same + # way ylim already does below, so the log scale always has something to + # scale against. + y_max_display = max(10, ${y_max}) + plt.ylim(0.9, y_max_display) # avoid zero on log scale # Standardized ticks at 10^0, 10^1, etc. - yticks = np.logspace(0, int(np.ceil(np.log10(${y_max}))), base=10) + yticks = np.logspace(0, int(np.ceil(np.log10(y_max_display))), base=10) plt.yticks(yticks) plt.gca().yaxis.set_major_locator(ticker.LogLocator(base=10.0, subs=(1.0,), numticks=10)) diff --git a/modules/local/sample/tcrspecificity.nf b/modules/local/sample/tcrspecificity.nf index 30ce184..fd8c514 100644 --- a/modules/local/sample/tcrspecificity.nf +++ b/modules/local/sample/tcrspecificity.nf @@ -20,9 +20,9 @@ process VDJDB_VDJMATCH { path(ref_db) output: - path("${sample_meta.sample}.vdjmatch.txt") - path("${sample_meta.sample}.annot.summary.txt") - path "logs/${sample_meta.sample}.vdjmatch.log" + path("${sample_meta.sample}.vdjmatch.txt"), emit: 'vdjmatch_txt' + path("${sample_meta.sample}.annot.summary.txt"), emit: 'annot_summary' + path "logs/${sample_meta.sample}.vdjmatch.log", emit: 'log' script: def memGb = (task.memory.toMega() * 0.8 / 1024).intValue() diff --git a/modules/scratch/CONGA/CoNGA_Report.qmd b/modules/scratch/CONGA/CoNGA_Report.qmd new file mode 100644 index 0000000..355dc86 --- /dev/null +++ b/modules/scratch/CONGA/CoNGA_Report.qmd @@ -0,0 +1,1739 @@ +--- +title: "SCRATCH-TCR: CoNGA Report" +author: "Syed Shujaat Ali Zaidi" +format: + html: + toc: true + toc-depth: 3 + number-sections: true + code-fold: true + code-summary: "Show code" + embed-resources: true + theme: cosmo + df-print: paged +execute: + echo: false + warning: false + message: false +params: + + # ====================================================== + # 1. Inputs & Software Paths + # ====================================================== + seurat_rds: "seurat_tcells_with_TCR.rds" + tcr_export_cells_file: "tcr_export_cells_with_embedding.tsv" + filtered_contig_annotations_csvfile: "tcr_export_cells_with_embedding.tsv" + conga_repo_dir: "/opt/tools/conga" + conga_python_bin: "/opt/conda/envs/tcrenv/bin/python" + outdir: "CoNGA_Report" + data_dir: "data" + tables_dir: "tables" + figures_dir: "figures" + conga_results_file: "" + conga_edges_file: "" + conga_cluster_summary_file: "" + + # ====================================================== + # 2. Thresholds & Analysis Logic + # ====================================================== + conga_high_cutoff: 0.8 + conga_mid_cutoff: 0.5 + use_quantile_cutoffs_if_score_not_bounded: true + high_quantile: 0.9 + mid_quantile: 0.5 + min_cells_per_group: 10 + max_edges_to_plot: 5000 + min_cluster_size_plot: 5 + top_n_clusters: 20 + organism: "human" + outfile_prefix: "conga_out" + run_all: true + gex_data_type: "10x_mtx" + + # ====================================================== + # 3. Metadata Mapping (Crucial Fixes Below) + # ====================================================== + label_col: "predicted_labels" + sample_col: "orig.ident" + patient_col: "patient_id" + condition_col: "condition" + timepoint_col: "timepoint" + batch_col: "batch" + + # NEW: Clonotype column mapping + clone_id_col: "clone_id" + clone_id_candidates: ["clone_id", "CTaa", "clonotype", "strict"] + clone_size_col: "clone_size" + clone_size_candidates: ["clone_size", "Frequency", "cloneSize"] + has_tcr_col: "has_tcr" + has_tcr_candidates: ["has_tcr", "TCR_status"] + paired_tcr_col: "paired_tcr" + paired_tcr_candidates: ["paired_tcr", "is_paired"] + + # NEW: TCRi score mapping for correlation plots + tcri_score_col: "tcri_score" + tcri_score_candidates: ["tcri_score", "TCRi"] + tcri_group_col: "tcri_group" + tcri_group_candidates: ["tcri_group", "TCRi_group"] + + # Standardized candidate lists for samples/labels + sample_candidates: ["META_SAMPLE", "orig.ident", "sample"] + patient_candidates: ["META_PATIENT", "patient_id"] + condition_candidates: ["META_TIMECOND", "condition"] + timepoint_candidates: ["META_TIMECOND", "timepoint"] + batch_candidates: ["META_BATCH", "batch"] + label_candidates: ["predicted_labels", "celltype", "Annotation"] + + # ====================================================== + # 4. Global Analysis & Plotting + # ====================================================== + reduction_use: "umap" + make_umap_if_missing: true + umap_dims_max: 30 + umap_nfeatures: 3000 + raster_large_umap: true + base_size: 12 + figure_format: "png" + report_label: "CoNGA Analysis" + + # Visualization Toggles + show_density_plot: true + show_feature_plot: true + show_cluster_umap: true + show_violin_by_annotation: true + show_boxplots_by_sample: true + show_cluster_composition: true + show_cluster_annotation_heatmap: true + show_cluster_sample_heatmap: true + show_tcri_vs_conga: true + show_summary_tables: true + top_n_states_heatmap: 15 + save_figures: true + save_tables: true + save_updated_seurat: true + figure_width: 10 + figure_height: 7 + figure_dpi: 300 + +--- + + +# =================================================================== +# 1. exports Seurat GEX in a CoNGA-readable format +# 2. runs `setup_10x_for_conga.py` +# 3. runs `run_conga.py` +# 4. reads the real `*_final.h5ad` +# 5. extracts actual CoNGA outputs +# 6. merges them back into Seurat +# =================================================================== + + +# setup +```{r} +#| label: setup +suppressPackageStartupMessages({ + library(Seurat) + library(SeuratObject) + library(Matrix) + library(data.table) + library(dplyr) + library(tidyr) + library(stringr) + library(ggplot2) + library(forcats) + library(scales) + library(glue) + library(knitr) + library(kableExtra) + library(ComplexHeatmap) + library(circlize) + library(patchwork) + library(reticulate) + # library(DropletUtils) +}) +options(stringsAsFactors = FALSE) + +dir.create(params$outdir, recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$data_dir), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$tables_dir), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$figures_dir), recursive = TRUE, showWarnings = FALSE) + +`%||%` <- function(a, b) if (!is.null(a) && length(a) > 0 && !all(is.na(a))) a else b + +theme_scratch_pub <- function(base_size = 12) { + theme_bw(base_size = base_size) + + theme( + plot.title = element_text(face = "bold", size = base_size + 2, hjust = 0), + plot.subtitle = element_text(size = base_size, hjust = 0), + axis.title = element_text(face = "bold"), + axis.text = element_text(color = "black"), + panel.grid.minor = element_blank(), + panel.grid.major = element_line(linewidth = 0.2, color = "grey90"), + strip.background = element_rect(fill = "grey95", color = "grey80"), + strip.text = element_text(face = "bold"), + legend.title = element_text(face = "bold"), + legend.key = element_blank(), + plot.caption = element_text(size = base_size - 2, color = "grey40") + ) +} + +save_plot_safe <- function(plot_obj, filename, + width = params$figure_width, + height = params$figure_height, + dpi = params$figure_dpi) { + if (!isTRUE(params$save_figures)) return(invisible(NULL)) + ggsave( + filename = file.path(params$outdir, params$figures_dir, filename), + plot = plot_obj, + width = width, + height = height, + dpi = dpi, + bg = "white", + limitsize = FALSE + ) +} + +save_table_safe <- function(df, filename) { + if (!isTRUE(params$save_tables)) return(invisible(NULL)) + fwrite(df, file.path(params$outdir, params$tables_dir, filename), sep = "\t") +} + +save_rds_safe <- function(obj, filename) { + if (!isTRUE(params$save_updated_seurat)) return(invisible(NULL)) + saveRDS(obj, file.path(params$outdir, params$data_dir, filename)) +} +# +# first_existing_col <- function(df, preferred, candidates = character()) { +# if (!is.null(preferred) && preferred != "" && preferred %in% colnames(df)) return(preferred) +# hits <- intersect(candidates, colnames(df)) +# if (length(hits) > 0) return(hits[[1]]) +# NULL +# } + +safe_percent <- function(x, denom) ifelse(denom > 0, x / denom, NA_real_) + +safe_make_umap <- function(seu, reduction_name = "umap", dims = 1:30, nfeatures = 3000) { + if ("RNA" %in% names(seu@assays)) DefaultAssay(seu) <- "RNA" + npcs <- min(max(dims), max(2, ncol(seu) - 1), 50) + nf <- min(nfeatures, nrow(seu)) + + if (!"pca" %in% Reductions(seu)) { + seu <- FindVariableFeatures(seu, nfeatures = nf, verbose = FALSE) + seu <- ScaleData(seu, verbose = FALSE) + seu <- RunPCA(seu, npcs = npcs, verbose = FALSE) + } + + use_dims <- dims[dims <= npcs] + if (length(use_dims) < 2) use_dims <- 1:min(10, npcs) + + seu <- FindNeighbors(seu, dims = use_dims, verbose = FALSE) + seu <- RunUMAP(seu, dims = use_dims, reduction.name = reduction_name, verbose = FALSE) + seu +} + +plot_box_by_group <- function(df, group_col, value_col, title_txt, filename) { + tmp <- df %>% + filter(!is.na(.data[[value_col]]), !is.na(.data[[group_col]])) %>% + group_by(.data[[group_col]]) %>% + mutate(group_n = n()) %>% + ungroup() %>% + filter(group_n >= params$min_cells_per_group) + + if (nrow(tmp) == 0) return(NULL) + + group_summary <- tmp %>% + group_by(.data[[group_col]]) %>% + summarise(mean_val = mean(.data[[value_col]], na.rm = TRUE), .groups = "drop") + + group_levels <- group_summary %>% + arrange(desc(mean_val)) %>% + pull(.data[[group_col]]) + + p <- tmp %>% + mutate(.group = factor(.data[[group_col]], levels = group_levels)) %>% + ggplot(aes(x = .group, y = .data[[value_col]], fill = .group)) + + geom_boxplot(outlier.size = 0.3, alpha = 0.85) + + coord_flip() + + guides(fill = "none") + + labs(title = title_txt, x = NULL, y = value_col) + + theme_scratch_pub(params$base_size) + + print(p) + save_plot_safe(p, filename) + invisible(p) +} + +sanitize_param_string <- function(x) { + if (is.null(x) || length(x) == 0) return(NULL) + trimws(gsub("\\u00A0", " ", as.character(x))) +} + +normalize_colnames <- function(df) { + colnames(df) <- trimws(gsub("\\u00A0", " ", colnames(df))) + df +} + +first_existing_col <- function(df, preferred, candidates = character()) { + df <- normalize_colnames(df) + preferred <- sanitize_param_string(preferred) + candidates <- trimws(gsub("\\u00A0", " ", as.character(candidates))) + + if (!is.null(preferred) && preferred != "" && preferred %in% colnames(df)) return(preferred) + hits <- intersect(candidates, colnames(df)) + if (length(hits) > 0) return(hits[[1]]) + NULL +} + +choose_col <- function(df, primary = NULL, fallback = NULL) { + nm <- colnames(df) + if (!is.null(primary) && primary != "" && primary %in% nm) return(primary) + if (!is.null(fallback) && fallback != "" && fallback %in% nm) return(fallback) + NULL +} + +``` + +# load-and-resolve +```{r} +#| label: load-and-resolve + +# load-and-resolve +seu <- readRDS(params$seurat_rds) +md <- normalize_colnames(seu@meta.data) +seu@meta.data <- md + +label_col <- first_existing_col(md, params$label_col, params$label_candidates) +sample_col <- first_existing_col(md, params$sample_col, params$sample_candidates) +patient_col <- first_existing_col(md, params$patient_col, params$patient_candidates) +condition_col <- first_existing_col(md, params$condition_col, params$condition_candidates) +timepoint_col <- first_existing_col(md, params$timepoint_col, params$timepoint_candidates) +batch_col <- first_existing_col(md, params$batch_col, params$batch_candidates) +clone_id_col <- first_existing_col(md, params$clone_id_col, params$clone_id_candidates) +clone_size_col <- first_existing_col(md, params$clone_size_col, params$clone_size_candidates) +paired_tcr_col <- first_existing_col(md, params$paired_tcr_col, params$paired_tcr_candidates) +has_tcr_col <- first_existing_col(md, params$has_tcr_col, params$has_tcr_candidates) +tcri_score_col_md <- first_existing_col(md, params$tcri_score_col, params$tcri_score_candidates) +tcri_group_col_md <- first_existing_col(md, params$tcri_group_col, params$tcri_group_candidates) + +if (is.null(label_col)) stop("Could not resolve annotation label column.") +if (is.null(sample_col)) stop("Could not resolve sample column.") +if (is.null(patient_col)) stop("Could not resolve patient column.") +if (is.null(condition_col)) stop("Could not resolve condition column.") +if (is.null(timepoint_col)) stop("Could not resolve timepoint column.") +if (is.null(clone_id_col)) stop("Could not resolve clone_id column.") +if (is.null(clone_size_col)) stop("Could not resolve clone_size column.") +if (is.null(has_tcr_col)) stop("Could not resolve has_tcr column.") +if (is.null(paired_tcr_col)) stop("Could not resolve paired_tcr column.") + +if (!"META_SAMPLE" %in% colnames(seu@meta.data) && !is.null(sample_col)) { + seu$META_SAMPLE <- as.character(seu@meta.data[[sample_col]]) +} +if (!"META_PATIENT" %in% colnames(seu@meta.data) && !is.null(patient_col)) { + seu$META_PATIENT <- as.character(seu@meta.data[[patient_col]]) +} +if (!"META_TIMECOND" %in% colnames(seu@meta.data) && !is.null(condition_col)) { + seu$META_TIMECOND <- as.character(seu@meta.data[[condition_col]]) +} +if (!"META_TIMEPOINT" %in% colnames(seu@meta.data) && !is.null(timepoint_col)) { + seu$META_TIMEPOINT <- as.character(seu@meta.data[[timepoint_col]]) +} +if (!"META_BATCH" %in% colnames(seu@meta.data) && !is.null(batch_col)) { + seu$META_BATCH <- as.character(seu@meta.data[[batch_col]]) +} + +print(list( + resolved_label_col = label_col, + resolved_sample_col = sample_col, + resolved_patient_col = patient_col, + resolved_condition_col = condition_col, + resolved_timepoint_col = timepoint_col, + resolved_batch_col = batch_col, + resolved_tcri_score_col = tcri_score_col_md, + resolved_tcri_group_col = tcri_group_col_md +)) + +# stopifnot(file.exists(params$seurat_rds)) +# stopifnot(file.exists(params$filtered_contig_annotations_csvfile)) +# if (params$conga_repo_dir == "") stop("Please provide params$conga_repo_dir.") +# if (params$conga_python_bin == "") stop("Please provide params$conga_python_bin.") +# +# seu <- readRDS(params$seurat_rds) +# md <- seu@meta.data +# +# label_col <- first_existing_col(md, params$label_col, params$label_candidates) +# sample_col <- first_existing_col(md, params$sample_col, params$sample_candidates) +# patient_col <- first_existing_col(md, params$patient_col, params$patient_candidates) +# condition_col <- first_existing_col(md, params$condition_col, params$condition_candidates) +# timepoint_col <- first_existing_col(md, params$timepoint_col, params$timepoint_candidates) +# batch_col <- first_existing_col(md, params$batch_col, params$batch_candidates) +# clone_id_col <- first_existing_col(md, params$clone_id_col, params$clone_id_candidates) +# clone_size_col <- first_existing_col(md, params$clone_size_col, params$clone_size_candidates) +# paired_tcr_col <- first_existing_col(md, params$paired_tcr_col, params$paired_tcr_candidates) +# has_tcr_col <- first_existing_col(md, params$has_tcr_col, params$has_tcr_candidates) +# tcri_score_col_md <- first_existing_col(md, params$tcri_score_col, params$tcri_score_candidates) +# tcri_group_col_md <- first_existing_col(md, params$tcri_group_col, params$tcri_group_candidates) +# +# if (is.null(label_col)) stop("Could not resolve annotation label column.") +# if (is.null(clone_id_col)) stop("Could not resolve clone_id column.") +# if (is.null(clone_size_col)) stop("Could not resolve clone_size column.") +# if (is.null(has_tcr_col)) stop("Could not resolve has_tcr column.") +# if (is.null(paired_tcr_col)) stop("Could not resolve paired_tcr column.") +``` + +# export-gex-for-conga +```{r} +#| label: export-gex-for-conga +message("Exporting GEX matrix in 10X format (Manual Method)...") + +DefaultAssay(seu) <- "RNA" + +# 1. Extract Counts +counts <- tryCatch( + SeuratObject::LayerData(seu, assay = "RNA", layer = "counts"), + error = function(e) NULL +) +if (is.null(counts)) { + counts <- tryCatch( + GetAssayData(seu, assay = "RNA", slot = "counts"), + error = function(e) NULL + ) +} +if (is.null(counts)) stop("Could not retrieve RNA counts matrix.") + +# 2. Create Directory +gex_dir <- file.path(params$outdir, params$data_dir, "gex_10x_mtx") +dir.create(gex_dir, recursive = TRUE, showWarnings = FALSE) + +# 3. Write Matrix (mtx) +# CoNGA expects 'matrix.mtx' +Matrix::writeMM(counts, file = file.path(gex_dir, "matrix.mtx")) + +# 4. Write Barcodes +# CoNGA expects a single column of barcodes +write.table( + data.frame(colnames(counts)), + file = file.path(gex_dir, "barcodes.tsv"), + sep = "\t", quote = FALSE, row.names = FALSE, col.names = FALSE +) + +# 5. Write Genes/Features +# CoNGA expects two columns: GeneID and GeneSymbol +# We will use the rownames for both to ensure compatibility +genes_df <- data.frame(ID = rownames(counts), Symbol = rownames(counts)) +write.table( + genes_df, + file = file.path(gex_dir, "genes.tsv"), + sep = "\t", quote = FALSE, row.names = FALSE, col.names = FALSE +) + +# Optional: Gzip them if your version of CoNGA is strictly looking for .gz files +# system(paste("gzip -f", file.path(gex_dir, "*.tsv"))) +# system(paste("gzip -f", file.path(gex_dir, "*.mtx"))) + +message(glue("GEX successfully exported to: {gex_dir}")) + +``` + +# prepare-conga-input +```{r} +#| label: prepare-conga-input + +message("Formatting TCR contigs with required raw_clonotype_id for CoNGA...") + +md_tmp <- seu@meta.data %>% + tibble::rownames_to_column("barcode_id") + +required_meta_cols <- c(has_tcr_col, clone_id_col, "cdr3a", "cdr3b", "trav", "traj", "trbv", "trbj") +missing_meta_cols <- required_meta_cols[!required_meta_cols %in% colnames(md_tmp)] +if (length(missing_meta_cols) > 0) { + stop("Missing required metadata columns for CoNGA input: ", paste(missing_meta_cols, collapse = ", ")) +} + +clean_conga_genes <- function(x) { + x <- as.character(x) + x <- toupper(x) + x <- sub("\\*.*$", "", x) + x <- gsub("/.*", "", x) + x <- gsub("\\s+", "", x) + + x <- dplyr::case_when( + x %in% c("TRAV36") ~ "TRAV36DV7", + x %in% c("TRAV38-2") ~ "TRAV38-2DV8", + TRUE ~ x + ) + + x[is.na(x) | x == "NONE" | x == "" | x == "NA" | x == ""] <- "None" + x +} + +clean_clone_id <- function(x) { + x <- as.character(x) + x[is.na(x) | x == "" | x == "NA" | x == "" | x == "None"] <- NA_character_ + x +} + +dummy_cdr3_nt <- function(cdr3_aa) { + cdr3_aa <- as.character(cdr3_aa) + cdr3_aa[is.na(cdr3_aa)] <- "" + vapply( + nchar(cdr3_aa), + function(n) paste(rep("N", 3 * n), collapse = ""), + character(1) + ) +} + +md_tmp <- md_tmp %>% + mutate( + barcode_id = as.character(barcode_id), + clone_id_clean = clean_clone_id(.data[[clone_id_col]]) + ) + +df_alpha <- md_tmp %>% + filter(!is.na(.data[[has_tcr_col]]), .data[[has_tcr_col]] == TRUE) %>% + filter(!is.na(clone_id_clean)) %>% + filter(!is.na(cdr3a), cdr3a != "", cdr3a != "None") %>% + transmute( + barcode = barcode_id, + is_cell = "True", + contig_id = paste0(barcode_id, "_alpha"), + high_confidence = "True", + length = 500L, + chain = "TRA", + v_gene = clean_conga_genes(trav), + d_gene = "None", + j_gene = clean_conga_genes(traj), + c_gene = "TRAC", + full_length = "True", + productive = "True", + cdr3 = as.character(cdr3a), + cdr3_nt = dummy_cdr3_nt(cdr3a), + reads = 1000L, + umis = 10L, + raw_clonotype_id = paste0("clonotype_", clone_id_clean) + ) %>% + filter(v_gene != "None", j_gene != "None") + +df_beta <- md_tmp %>% + filter(!is.na(.data[[has_tcr_col]]), .data[[has_tcr_col]] == TRUE) %>% + filter(!is.na(clone_id_clean)) %>% + filter(!is.na(cdr3b), cdr3b != "", cdr3b != "None") %>% + transmute( + barcode = barcode_id, + is_cell = "True", + contig_id = paste0(barcode_id, "_beta"), + high_confidence = "True", + length = 500L, + chain = "TRB", + v_gene = clean_conga_genes(trbv), + d_gene = "None", + j_gene = clean_conga_genes(trbj), + c_gene = "TRBC", + full_length = "True", + productive = "True", + cdr3 = as.character(cdr3b), + cdr3_nt = dummy_cdr3_nt(cdr3b), + reads = 1000L, + umis = 10L, + raw_clonotype_id = paste0("clonotype_", clone_id_clean) + ) %>% + filter(v_gene != "None", j_gene != "None") + +conga_compat_df <- dplyr::bind_rows(df_alpha, df_beta) %>% + distinct() + +conga_diag <- tibble::tibble( + metric = c( + "input_cells", + "alpha_rows", + "beta_rows", + "total_rows", + "unique_barcodes", + "unique_clonotypes" + ), + value = c( + nrow(md_tmp), + nrow(df_alpha), + nrow(df_beta), + nrow(conga_compat_df), + dplyr::n_distinct(conga_compat_df$barcode), + dplyr::n_distinct(conga_compat_df$raw_clonotype_id) + ) +) +save_table_safe(conga_diag, "conga_prepare_diagnostics.tsv") + +if (nrow(conga_compat_df) == 0) { + stop( + "CoNGA pseudo-contig input is empty after filtering. ", + "Check has_tcr, clone_id, cdr3a/cdr3b, and V/J gene columns." + ) +} + +conga_input_csv <- file.path(params$outdir, params$data_dir, "conga_pseudo_contigs.csv") +write.csv(conga_compat_df, conga_input_csv, row.names = FALSE, quote = FALSE) + +message(glue( + "Exported {nrow(conga_compat_df)} pseudo-contigs across ", + "{dplyr::n_distinct(conga_compat_df$barcode)} cells and ", + "{dplyr::n_distinct(conga_compat_df$raw_clonotype_id)} clonotypes." +)) +``` + +# run-conga +```{r} +#| label: run-conga + +Sys.setenv(MPLCONFIGDIR = ".") + +repo_base <- params$conga_repo_dir %||% "/opt/tools/conga" + +find_script_recursive <- function(base, script_name) { + standard_locs <- c( + file.path(base, script_name), + file.path(base, "scripts", script_name), + file.path(base, "conga", "scripts", script_name) + ) + found_standard <- standard_locs[file.exists(standard_locs)] + if (length(found_standard) > 0) return(found_standard[1]) + + all_files <- list.files( + base, + pattern = paste0("^", script_name, "$"), + recursive = TRUE, + full.names = TRUE + ) + if (length(all_files) == 0) return(NULL) + all_files[1] +} + +setup_script <- find_script_recursive(repo_base, "setup_10x_for_conga.py") +run_script <- find_script_recursive(repo_base, "run_conga.py") + +if (is.null(setup_script) || !file.exists(setup_script)) { + stop("Could not locate setup_10x_for_conga.py under conga_repo_dir: ", repo_base) +} +if (is.null(run_script) || !file.exists(run_script)) { + stop("Could not locate run_conga.py under conga_repo_dir: ", repo_base) +} + +input_csv <- file.path(params$outdir, params$data_dir, "conga_pseudo_contigs.csv") +if (!file.exists(input_csv)) { + stop("Missing CoNGA pseudo-contig input CSV: ", input_csv) +} + +local_contigs <- "conga_pseudo_contigs.csv" +ok_copy <- file.copy(input_csv, local_contigs, overwrite = TRUE) +if (!ok_copy || !file.exists(local_contigs)) { + stop("Failed to copy CoNGA pseudo-contig CSV into working directory.") +} + +setup_args <- c( + setup_script, + "--filtered_contig_annotations_csvfile", local_contigs, + "--organism", params$organism +) + +message("Step 1: Running CoNGA setup...") +setup_log <- system2( + params$conga_python_bin, + args = setup_args, + stdout = TRUE, + stderr = TRUE +) +setup_status <- attr(setup_log, "status") +if (is.null(setup_status)) setup_status <- 0L +writeLines(setup_log, "conga_setup.log.txt") + +candidate_clones <- c( + "conga_pseudo_contigs_tcrdist_clones.tsv", + "conga_pseudo_contigs_clones.tsv" +) +candidate_bcmap <- c( + "conga_pseudo_contigs_bcmap.txt", + "conga_pseudo_contigs_barcode_mapping.tsv", + "conga_pseudo_contigs_barcode_map.tsv" +) + +find_first_existing <- function(paths) { + x <- paths[file.exists(paths)] + if (length(x) > 0) x[1] else NULL +} + +clones_file <- find_first_existing(candidate_clones) +bcmap_file <- find_first_existing(candidate_bcmap) + +if (is.null(clones_file)) { + found <- list.files(".", pattern = "clones.*tsv$|tcrdist_clones.*tsv$", recursive = TRUE, full.names = TRUE) + if (length(found) > 0) clones_file <- found[1] +} +if (is.null(bcmap_file)) { + found <- list.files(".", pattern = "bcmap|barcode.*map", recursive = TRUE, full.names = TRUE) + if (length(found) > 0) bcmap_file <- found[1] +} + +if (setup_status != 0L || is.null(clones_file) || is.null(bcmap_file)) { + stop( + paste0( + "CRITICAL: CoNGA setup failed to create either the clones file or the barcode map file.\n", + "setup_script: ", setup_script, "\n", + "setup_status: ", setup_status, "\n", + "clones_file found: ", ifelse(is.null(clones_file), "NO", clones_file), "\n", + "bcmap_file found: ", ifelse(is.null(bcmap_file), "NO", bcmap_file), "\n", + "See conga_setup.log.txt for details." + ) + ) +} + +dir.create(file.path(params$outdir, params$data_dir), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$tables_dir), recursive = TRUE, showWarnings = FALSE) + +file.copy(clones_file, file.path(params$outdir, params$data_dir, basename(clones_file)), overwrite = TRUE) +file.copy(bcmap_file, file.path(params$outdir, params$data_dir, basename(bcmap_file)), overwrite = TRUE) +file.copy("conga_setup.log.txt", file.path(params$outdir, params$tables_dir, "conga_setup.log.txt"), overwrite = TRUE) + +outfile_prefix <- file.path(params$outdir, params$data_dir, params$outfile_prefix) +gex_dir <- normalizePath(file.path(params$outdir, params$data_dir, "gex_10x_mtx"), mustWork = FALSE) + +if (!dir.exists(gex_dir)) { + stop("CoNGA expected GEX directory does not exist: ", gex_dir) +} + +# detect whether this CoNGA version supports --bcmap_file +help_out <- system2( + params$conga_python_bin, + args = c(run_script, "--help"), + stdout = TRUE, + stderr = TRUE +) +help_txt <- paste(help_out, collapse = "\n") +supports_bcmap_flag <- grepl("--bcmap_file", help_txt, fixed = TRUE) + +# run_args <- c( +# run_script, +# if (isTRUE(params$run_all)) "--all" else "--graph_vs_graph", +# "--organism", params$organism, +# "--clones_file", normalizePath(clones_file, mustWork = TRUE), +# "--gex_data", gex_dir, +# "--gex_data_type", params$gex_data_type, +# "--outfile_prefix", outfile_prefix +# ) +# Define the set of flags that avoid on-the-fly C++ compilation +safe_conga_flags <- c( + "--graph_vs_graph", + "--graph_vs_graph_stats", + "--graph_vs_features", + "--cluster_vs_cluster", + "--find_hotspot_features", + "--find_gex_cluster_degs" +) + +run_args <- c( + run_script, + safe_conga_flags, # Use the safe list instead of --all + "--organism", params$organism, + "--clones_file", normalizePath(clones_file, mustWork = TRUE), + "--gex_data", gex_dir, + "--gex_data_type", params$gex_data_type, + "--outfile_prefix", outfile_prefix +) + +# If your version supports the bcmap flag (calculated in your previous logic) +# if (supports_bcmap_flag) { +# run_args <- c(run_args, "--bcmap_file", normalizePath(bcmap_file, mustWork = TRUE)) +# } + + +if (supports_bcmap_flag) { + run_args <- c( + run_args, + "--bcmap_file", normalizePath(bcmap_file, mustWork = TRUE) + ) +} + +message("Step 2: Starting main CoNGA run...") +message("run_script: ", run_script) +message("clones_file: ", normalizePath(clones_file, mustWork = TRUE)) +message("bcmap_file: ", normalizePath(bcmap_file, mustWork = TRUE)) +message("supports_bcmap_flag: ", supports_bcmap_flag) +message("gex_dir: ", gex_dir) +message("outfile_prefix: ", outfile_prefix) +message("run_args:") +print(run_args) + +run_log <- system2( + params$conga_python_bin, + args = run_args, + stdout = TRUE, + stderr = TRUE +) +run_status <- attr(run_log, "status") +if (is.null(run_status)) run_status <- 0L +writeLines(run_log, "conga_run.log.txt") +file.copy("conga_run.log.txt", file.path(params$outdir, params$tables_dir, "conga_run.log.txt"), overwrite = TRUE) + +final_h5ad <- paste0(outfile_prefix, "_final.h5ad") +if (run_status != 0L || !file.exists(final_h5ad)) { + cat("---- Begin conga_run.log.txt ----\n") + cat(paste(run_log, collapse = "\n")) + cat("\n---- End conga_run.log.txt ----\n") + + stop( + paste0( + "Final CoNGA h5ad object was not created.\n", + "run_status: ", run_status, "\n", + "Expected final_h5ad: ", final_h5ad, "\n", + "Check conga_run.log.txt." + ) + ) +} + +``` + + +# extract-conga-results +```{r} +#| label: extract-conga-results +reticulate::use_python(params$conga_python_bin, required = TRUE) +anndata <- reticulate::import("anndata", delay_load = FALSE) +pd <- reticulate::import("pandas", delay_load = FALSE) +np <- reticulate::import("numpy", delay_load = FALSE) + +py$final_h5ad <- final_h5ad +py$out_tsv <- file.path(params$outdir, params$tables_dir, "conga_results.tsv") +py$out_edges <- file.path(params$outdir, params$tables_dir, "conga_edges.tsv") + +reticulate::py_run_string(" +import anndata as ad +import pandas as pd +import numpy as np + +adata = ad.read_h5ad(final_h5ad) +obs = adata.obs.copy() +obs['cell_id'] = obs.index.astype(str) + +# Build a clonotype key compatible with SCRATCH clone_id = A:cdr3a|B:cdr3b +if all(x in obs.columns for x in ['cdr3a','cdr3b']): + obs['clone_id'] = 'A:' + obs['cdr3a'].astype(str) + '|B:' + obs['cdr3b'].astype(str) +else: + obs['clone_id'] = np.nan + +# pull CoNGA core outputs where available +def maybe(col): + return obs[col] if col in obs.columns else np.nan + +out = pd.DataFrame({ + 'cell_id': obs['cell_id'], + 'clone_id': obs['clone_id'], + 'conga_score': maybe('conga_scores'), + 'conga_gex_cluster': maybe('clusters_gex'), + 'conga_tcr_cluster': maybe('clusters_tcr'), + 'conga_clone_size': maybe('clone_sizes'), + 'nndists_tcr': maybe('nndists_tcr'), + 'nndists_gex': maybe('nndists_gex'), + 'is_invariant': maybe('is_invariant') +}) + +if 'X_gex_2d' in adata.obsm.keys(): + xy = np.asarray(adata.obsm['X_gex_2d']) + out['conga_gex_2d_1'] = xy[:,0] + out['conga_gex_2d_2'] = xy[:,1] +else: + out['conga_gex_2d_1'] = np.nan + out['conga_gex_2d_2'] = np.nan + +if 'X_tcr_2d' in adata.obsm.keys(): + xy = np.asarray(adata.obsm['X_tcr_2d']) + out['conga_tcr_2d_1'] = xy[:,0] + out['conga_tcr_2d_2'] = xy[:,1] +else: + out['conga_tcr_2d_1'] = np.nan + out['conga_tcr_2d_2'] = np.nan + +out.to_csv(out_tsv, sep='\\t', index=False) + +# optional edge extraction if neighbors exist in uns +edge_rows = [] +if hasattr(adata, 'uns') and 'all_nbrs' in adata.uns: + all_nbrs = adata.uns['all_nbrs'] + try: + for nbr_frac, pair in all_nbrs.items(): + gex_nbrs, tcr_nbrs = pair + for i in range(len(gex_nbrs)): + for j in np.asarray(gex_nbrs[i]).tolist(): + edge_rows.append((str(obs.index[i]), str(obs.index[j]), 'gex', str(nbr_frac))) + for j in np.asarray(tcr_nbrs[i]).tolist(): + edge_rows.append((str(obs.index[i]), str(obs.index[j]), 'tcr', str(nbr_frac))) + except Exception: + pass + +edges = pd.DataFrame(edge_rows, columns=['source','target','edge_type','nbr_frac']) +edges.to_csv(out_edges, sep='\\t', index=False) +") +``` + +# merge-conga-into-seurat +```{r} +#| label: merge-conga-into-seurat +conga_tbl <- fread(file.path(params$outdir, params$tables_dir, "conga_results.tsv")) %>% as.data.frame() + +# CoNGA is clonotype-level after reduction to one representative cell per clone. +# Merge back to all Seurat cells by clone_id. +conga_clone_tbl <- conga_tbl %>% + filter(!is.na(clone_id), clone_id != "") %>% + group_by(clone_id) %>% + summarise( + conga_score = suppressWarnings(as.numeric(first(conga_score))), + conga_gex_cluster = as.character(first(conga_gex_cluster)), + conga_tcr_cluster = as.character(first(conga_tcr_cluster)), + conga_clone_size = suppressWarnings(as.numeric(first(conga_clone_size))), + nndists_tcr = suppressWarnings(as.numeric(first(nndists_tcr))), + nndists_gex = suppressWarnings(as.numeric(first(nndists_gex))), + is_invariant = as.logical(first(is_invariant)), + conga_gex_2d_1 = suppressWarnings(as.numeric(first(conga_gex_2d_1))), + conga_gex_2d_2 = suppressWarnings(as.numeric(first(conga_gex_2d_2))), + conga_tcr_2d_1 = suppressWarnings(as.numeric(first(conga_tcr_2d_1))), + conga_tcr_2d_2 = suppressWarnings(as.numeric(first(conga_tcr_2d_2))), + .groups = "drop" + ) + +seu_md <- seu@meta.data %>% + tibble::rownames_to_column("cell_id") + +seu_md2 <- seu_md %>% + left_join(conga_clone_tbl, by = setNames("clone_id", clone_id_col)) + +rownames(seu_md2) <- seu_md2$cell_id +seu_md2$cell_id <- NULL +seu@meta.data <- as.data.frame(seu_md2) + +save_rds_safe(seu, "seurat_with_CoNGA.rds") +save_table_safe(conga_clone_tbl, "conga_clone_level_results.tsv") +``` + +# ensure-embedding +```{r} +#| label: ensure-embedding +reduction_to_use <- params$reduction_use +if (!(reduction_to_use %in% names(seu@reductions)) && isTRUE(params$make_umap_if_missing)) { + seu <- safe_make_umap( + seu, + reduction_name = params$reduction_use, + dims = 1:params$umap_dims_max, + nfeatures = params$umap_nfeatures + ) +} +if (!(reduction_to_use %in% names(seu@reductions))) { + reduction_to_use <- if ("umap" %in% names(seu@reductions)) "umap" else if ("tsne" %in% names(seu@reductions)) "tsne" else if ("pca" %in% names(seu@reductions)) "pca" else stop("No usable embedding found.") +} +``` + +# build-export +```{r} +#| label: build-export + +# build-export +embed_df <- as.data.frame(Embeddings(seu, reduction_to_use)) %>% + tibble::rownames_to_column("cell_id") + +sample_col_export <- choose_col(seu@meta.data, "META_SAMPLE", sample_col) +patient_col_export <- choose_col(seu@meta.data, "META_PATIENT", patient_col) +condition_col_export <- choose_col(seu@meta.data, "META_TIMECOND", condition_col) +timepoint_col_export <- choose_col(seu@meta.data, "META_TIMEPOINT", timepoint_col) +batch_col_export <- choose_col(seu@meta.data, "META_BATCH", batch_col) + +export_cells <- seu@meta.data %>% + tibble::rownames_to_column("cell_id") %>% + transmute( + cell_id, + sample = if (!is.null(sample_col_export)) as.character(.data[[sample_col_export]]) else NA_character_, + patient = if (!is.null(patient_col_export)) as.character(.data[[patient_col_export]]) else NA_character_, + condition = if (!is.null(condition_col_export)) as.character(.data[[condition_col_export]]) else NA_character_, + timepoint = if (!is.null(timepoint_col_export)) as.character(.data[[timepoint_col_export]]) else NA_character_, + batch = if (!is.null(batch_col_export)) as.character(.data[[batch_col_export]]) else NA_character_, + annot = as.character(.data[[label_col]]), + clone_id = as.character(.data[[clone_id_col]]), + clone_size = suppressWarnings(as.numeric(.data[[clone_size_col]])), + has_tcr = as.logical(.data[[has_tcr_col]]), + paired_tcr = as.logical(.data[[paired_tcr_col]]), + tcri_score = if (!is.null(tcri_score_col_md)) suppressWarnings(as.numeric(.data[[tcri_score_col_md]])) else NA_real_, + tcri_group = if (!is.null(tcri_group_col_md)) as.character(.data[[tcri_group_col_md]]) else NA_character_, + conga_score = suppressWarnings(as.numeric(.data[["conga_score"]])), + conga_gex_cluster = as.character(.data[["conga_gex_cluster"]]), + conga_tcr_cluster = as.character(.data[["conga_tcr_cluster"]]), + conga_clone_size = suppressWarnings(as.numeric(.data[["conga_clone_size"]])), + nndists_tcr = suppressWarnings(as.numeric(.data[["nndists_tcr"]])), + nndists_gex = suppressWarnings(as.numeric(.data[["nndists_gex"]])), + is_invariant = as.logical(.data[["is_invariant"]]) + ) %>% + left_join(embed_df, by = "cell_id") + +save_table_safe(export_cells, "conga_export_cells.tsv") + +print(colSums(!is.na(export_cells[, c("sample", "patient", "condition", "timepoint")]))) + + +# embed_df <- as.data.frame(Embeddings(seu, reduction_to_use)) %>% +# tibble::rownames_to_column("cell_id") +# +# export_cells <- seu@meta.data %>% +# tibble::rownames_to_column("cell_id") %>% +# transmute( +# cell_id, +# sample = if (!is.null(sample_col)) .data[[sample_col]] else NA_character_, +# patient = if (!is.null(patient_col)) .data[[patient_col]] else NA_character_, +# condition = if (!is.null(condition_col)) .data[[condition_col]] else NA_character_, +# timepoint = if (!is.null(timepoint_col)) .data[[timepoint_col]] else NA_character_, +# batch = if (!is.null(batch_col)) .data[[batch_col]] else NA_character_, +# annot = .data[[label_col]], +# clone_id = .data[[clone_id_col]], +# clone_size = suppressWarnings(as.numeric(.data[[clone_size_col]])), +# has_tcr = as.logical(.data[[has_tcr_col]]), +# paired_tcr = as.logical(.data[[paired_tcr_col]]), +# tcri_score = if (!is.null(tcri_score_col_md)) suppressWarnings(as.numeric(.data[[tcri_score_col_md]])) else NA_real_, +# tcri_group = if (!is.null(tcri_group_col_md)) as.character(.data[[tcri_group_col_md]]) else NA_character_, +# conga_score = suppressWarnings(as.numeric(.data[["conga_score"]])), +# conga_gex_cluster = as.character(.data[["conga_gex_cluster"]]), +# conga_tcr_cluster = as.character(.data[["conga_tcr_cluster"]]), +# conga_clone_size = suppressWarnings(as.numeric(.data[["conga_clone_size"]])), +# nndists_tcr = suppressWarnings(as.numeric(.data[["nndists_tcr"]])), +# nndists_gex = suppressWarnings(as.numeric(.data[["nndists_gex"]])), +# is_invariant = as.logical(.data[["is_invariant"]]) +# ) %>% +# left_join(embed_df, by = "cell_id") +# +# save_table_safe(export_cells, "conga_export_cells.tsv") +``` + +# Merge diagnostics +```{r} +#| label: merge-diagnostics +merge_diagnostics <- tibble( + metric = c( + "Cells in Seurat", + "Representative clonotypes in CoNGA", + "Cells with merged CoNGA score", + "Cells with merged CoNGA GEX cluster", + "Fraction with CoNGA score", + "Fraction with CoNGA GEX cluster" + ), + value = c( + ncol(seu), + nrow(conga_tbl), + sum(!is.na(export_cells$conga_score)), + sum(!is.na(export_cells$conga_gex_cluster)), + sprintf("%.2f%%", 100 * mean(!is.na(export_cells$conga_score))), + sprintf("%.2f%%", 100 * mean(!is.na(export_cells$conga_gex_cluster))) + ) +) +save_table_safe(merge_diagnostics, "conga_merge_diagnostics.tsv") +``` + +# summary-tables +```{r} +#| label: summary-tables + +summary_rollup <- tibble( + metric = c( + "Report label", + "Cells in Seurat", + "Cells with CoNGA scores", + "TCR-positive cells", + "Paired TCR cells", + "Unique CoNGA GEX clusters", + "Unique CoNGA TCR clusters", + "Unique annotations", + "Unique samples", + "Selected embedding" + ), + value = c( + params$report_label, + nrow(export_cells), + sum(!is.na(export_cells$conga_score)), + sum(export_cells$has_tcr, na.rm = TRUE), + sum(export_cells$paired_tcr, na.rm = TRUE), + length(unique(na.omit(export_cells$conga_gex_cluster))), + length(unique(na.omit(export_cells$conga_tcr_cluster))), + length(unique(na.omit(export_cells$annot))), + length(unique(na.omit(export_cells$sample))), + reduction_to_use + ) +) +save_table_safe(summary_rollup, "conga_summary_rollup.tsv") + +cluster_summary <- export_cells %>% + filter(!is.na(conga_gex_cluster)) %>% + count(conga_gex_cluster, name = "n_cells") %>% + mutate(frac = n_cells / sum(n_cells)) %>% + arrange(desc(n_cells)) +save_table_safe(cluster_summary, "conga_cluster_summary.tsv") + +annotation_conga_summary <- export_cells %>% + filter(!is.na(conga_score), !is.na(annot)) %>% + group_by(annot) %>% + summarise( + n_cells = n(), + mean_conga = mean(conga_score, na.rm = TRUE), + median_conga = median(conga_score, na.rm = TRUE), + sd_conga = sd(conga_score, na.rm = TRUE), + n_clustered = sum(!is.na(conga_gex_cluster)), + frac_clustered = n_clustered / n_cells, + .groups = "drop" + ) %>% + arrange(desc(mean_conga)) +save_table_safe(annotation_conga_summary, "annotation_conga_summary.tsv") + +sample_conga_summary <- export_cells %>% + filter(!is.na(sample)) %>% + group_by(sample) %>% + summarise( + n_cells = n(), + n_clustered = sum(!is.na(conga_gex_cluster)), + frac_clustered = n_clustered / n_cells, + mean_conga = mean(conga_score, na.rm = TRUE), + median_conga = median(conga_score, na.rm = TRUE), + .groups = "drop" + ) %>% + arrange(desc(frac_clustered)) +save_table_safe(sample_conga_summary, "sample_conga_summary.tsv") + +cluster_annotation_tbl <- export_cells %>% + filter(!is.na(conga_gex_cluster), !is.na(annot)) %>% + count(conga_gex_cluster, annot, name = "n_cells") +save_table_safe(cluster_annotation_tbl, "conga_cluster_annotation_composition.tsv") + +cluster_sample_tbl <- export_cells %>% + filter(!is.na(conga_gex_cluster), !is.na(sample)) %>% + count(conga_gex_cluster, sample, name = "n_cells") +save_table_safe(cluster_sample_tbl, "conga_cluster_sample_composition.tsv") + +# Safe TCRi ↔ CoNGA association summary +assoc_df <- export_cells %>% + dplyr::select( + dplyr::any_of(c( + "cell_id", + "sample", + "annot", + "clone_id", + "tcri_score", + "conga_score" + )) + ) %>% + dplyr::filter( + !is.na(tcri_score), + !is.na(conga_score) + ) + +save_table_safe( + tibble::tibble( + metric = c( + "cells_with_tcri_and_conga", + "unique_samples", + "unique_clones" + ), + value = c( + nrow(assoc_df), + dplyr::n_distinct(assoc_df$sample), + dplyr::n_distinct(assoc_df$clone_id) + ) + ), + "tcri_conga_overlap_diagnostics.tsv" +) + +if (nrow(assoc_df) >= 3) { + + pearson_cor <- tryCatch( + stats::cor( + assoc_df$tcri_score, + assoc_df$conga_score, + use = "pairwise.complete.obs", + method = "pearson" + ), + error = function(e) NA_real_ + ) + + spearman_cor <- tryCatch( + stats::cor( + assoc_df$tcri_score, + assoc_df$conga_score, + use = "pairwise.complete.obs", + method = "spearman" + ), + error = function(e) NA_real_ + ) + + pearson_p <- tryCatch( + stats::cor.test( + assoc_df$tcri_score, + assoc_df$conga_score, + method = "pearson" + )$p.value, + error = function(e) NA_real_ + ) + + spearman_p <- tryCatch( + stats::cor.test( + assoc_df$tcri_score, + assoc_df$conga_score, + method = "spearman", + exact = FALSE + )$p.value, + error = function(e) NA_real_ + ) + + tcri_conga_assoc_tbl <- tibble::tibble( + n_cells = nrow(assoc_df), + pearson_cor = pearson_cor, + pearson_p = pearson_p, + spearman_cor = spearman_cor, + spearman_p = spearman_p + ) + +} else { + tcri_conga_assoc_tbl <- tibble::tibble( + n_cells = nrow(assoc_df), + pearson_cor = NA_real_, + pearson_p = NA_real_, + spearman_cor = NA_real_, + spearman_p = NA_real_, + note = "Too few overlapping non-missing TCRi and CoNGA scores to compute correlation." + ) +} + +save_table_safe(tcri_conga_assoc_tbl, "tcri_conga_association.tsv") + + +``` + +# overview +```{r} +#| label: overview +kable(summary_rollup, caption = "High-level overview of CoNGA analysis.") %>% + kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed")) +``` + +# CoNGA score distributions density-plot +```{r} +#| label: density-plot +if (isTRUE(params$show_density_plot) && any(!is.na(export_cells$conga_score))) { + p_density <- export_cells %>% + filter(!is.na(conga_score)) %>% + ggplot(aes(x = conga_score)) + + geom_density(fill = "steelblue", alpha = 0.5, linewidth = 0.8) + + labs( + title = "Distribution of CoNGA scores", + x = "CoNGA score", + y = "Density" + ) + + theme_scratch_pub(params$base_size) + + p_density + save_plot_safe(p_density, glue("conga_score_density.{params$figure_format}")) +} +``` + +# Embedding overlays +```{r} +#| label: conga-feature-plot +if (isTRUE(params$show_feature_plot) && any(!is.na(export_cells$conga_score))) { + p_feature <- FeaturePlot( + seu, + reduction = reduction_to_use, + features = "conga_score", + raster = isTRUE(params$raster_large_umap) + ) + + ggtitle("Embedding overlay: CoNGA score") + + theme_scratch_pub(params$base_size) + + p_feature + save_plot_safe(p_feature, glue("conga_feature_plot.{params$figure_format}"), width = 8, height = 6) +} +``` + +# conga-cluster-umap +```{r} +#| label: conga-cluster-umap +if (isTRUE(params$show_cluster_umap) && any(!is.na(export_cells$conga_gex_cluster))) { + p_cluster <- DimPlot( + seu, + reduction = reduction_to_use, + group.by = "conga_gex_cluster", + label = FALSE, + raster = isTRUE(params$raster_large_umap) + ) + + ggtitle("Embedding overlay: CoNGA GEX cluster") + + theme_scratch_pub(params$base_size) + + p_cluster + save_plot_safe(p_cluster, glue("conga_cluster_umap.{params$figure_format}"), width = 8, height = 6) +} +``` + +# CoNGA by annotation +```{r} +#| label: violin-by-annotation +if (isTRUE(params$show_violin_by_annotation) && any(!is.na(export_cells$conga_score))) { + annot_order <- annotation_conga_summary %>% + arrange(desc(mean_conga)) %>% + pull(annot) + + p_violin_annot <- export_cells %>% + filter(!is.na(conga_score), !is.na(annot)) %>% + mutate(annot = factor(annot, levels = annot_order)) %>% + ggplot(aes(x = annot, y = conga_score, fill = annot)) + + geom_violin(scale = "width", trim = TRUE, alpha = 0.8) + + geom_boxplot(width = 0.12, outlier.size = 0.2, fill = "white") + + coord_flip() + + guides(fill = "none") + + labs(title = "CoNGA score by annotation", x = NULL, y = "CoNGA score") + + theme_scratch_pub(params$base_size) + + p_violin_annot + save_plot_safe(p_violin_annot, glue("conga_violin_by_annotation.{params$figure_format}"), width = 10, height = 8) +} +``` + +# CoNGA by sample / condition / patient / timepoint +```{r} +#| label: boxplots-by-group +if (isTRUE(params$show_boxplots_by_sample) && any(!is.na(export_cells$sample))) { + plot_box_by_group(export_cells, "sample", "conga_score", "CoNGA score by sample", glue("conga_boxplot_by_sample.{params$figure_format}")) +} +if (isTRUE(params$show_boxplots_by_condition) && any(!is.na(export_cells$condition))) { + plot_box_by_group(export_cells, "condition", "conga_score", "CoNGA score by condition", glue("conga_boxplot_by_condition.{params$figure_format}")) +} +if (isTRUE(params$show_boxplots_by_patient) && any(!is.na(export_cells$patient))) { + plot_box_by_group(export_cells, "patient", "conga_score", "CoNGA score by patient", glue("conga_boxplot_by_patient.{params$figure_format}")) +} +if (isTRUE(params$show_boxplots_by_timepoint) && any(!is.na(export_cells$timepoint))) { + plot_box_by_group(export_cells, "timepoint", "conga_score", "CoNGA score by timepoint", glue("conga_boxplot_by_timepoint.{params$figure_format}")) +} +``` + +# cluster-composition +```{r} +#| label: cluster-composition +if (isTRUE(params$show_cluster_composition) && nrow(cluster_summary) > 0) { + top_clusters <- cluster_summary %>% + slice_head(n = params$top_n_clusters) %>% + pull(conga_gex_cluster) + + p_cluster_bar <- cluster_summary %>% + filter(conga_gex_cluster %in% top_clusters) %>% + mutate(conga_gex_cluster = fct_reorder(conga_gex_cluster, n_cells)) %>% + ggplot(aes(x = conga_gex_cluster, y = n_cells)) + + geom_col(fill = "steelblue") + + coord_flip() + + scale_y_continuous(labels = comma) + + labs(title = "Top CoNGA GEX clusters", x = NULL, y = "Cells") + + theme_scratch_pub(params$base_size) + + p_cluster_bar + save_plot_safe(p_cluster_bar, glue("conga_cluster_size_barplot.{params$figure_format}")) +} +``` + +# cluster heatmaps +```{r} +#| label: annotation-heatmap +if (isTRUE(params$show_cluster_annotation_heatmap) && nrow(cluster_annotation_tbl) > 0) { + top_clusters <- cluster_summary %>% + filter(n_cells >= params$min_cluster_size_plot) %>% + slice_head(n = params$top_n_clusters) %>% + pull(conga_gex_cluster) + + # mat_df <- cluster_annotation_tbl %>% + # filter(conga_gex_cluster %in% top_clusters) %>% + # pivot_wider(names_from = annot, values_from = n_cells, values_fill = 0) %>% + # as.data.frame() + # + # rownames(mat_df) <- paste0("C", sprintf("%02d", mat_df$conga_gex_cluster)) + # mat_df$conga_gex_cluster <- NULL + # mat <- as.matrix(mat_df) + mat_df <- cluster_annotation_tbl %>% + filter(conga_gex_cluster %in% top_clusters) %>% + pivot_wider(names_from = annot, values_from = n_cells, values_fill = 0) %>% + as.data.frame() + + rownames(mat_df) <- paste0( + "C", + stringr::str_pad(as.character(mat_df$conga_gex_cluster), width = 2, side = "left", pad = "0") + ) + mat_df$conga_gex_cluster <- NULL + mat <- as.matrix(mat_df) + + + max_mat <- max(mat, na.rm = TRUE) + if (!is.finite(max_mat) || max_mat <= 0) max_mat <- 1 + + ht <- ComplexHeatmap::Heatmap( + mat, + name = "Cells", + col = circlize::colorRamp2( + c(0, max_mat / 2, max_mat), + c("white", "gold", "firebrick") + ), + cluster_rows = TRUE, + cluster_columns = TRUE, + row_names_side = "left", + row_names_gp = grid::gpar(fontsize = 9), + row_names_max_width = grid::unit(18, "mm"), + column_names_gp = grid::gpar(fontsize = 10), + column_names_rot = 45, + column_title = "Annotation composition of CoNGA clusters", + column_title_gp = grid::gpar(fontsize = 13, fontface = "bold"), + heatmap_legend_param = list(title = "Cells") + ) + + ComplexHeatmap::draw( + ht, + heatmap_legend_side = "right", + padding = grid::unit(c(5, 5, 5, 20), "mm") + ) +} + + +# #| label: annotation-heatmap +# if (isTRUE(params$show_cluster_annotation_heatmap) && nrow(cluster_annotation_tbl) > 0) { +# top_clusters <- cluster_summary %>% +# filter(n_cells >= params$min_cluster_size_plot) %>% +# slice_head(n = params$top_n_clusters) %>% +# pull(conga_gex_cluster) +# +# mat_df <- cluster_annotation_tbl %>% +# filter(conga_gex_cluster %in% top_clusters) %>% +# pivot_wider(names_from = annot, values_from = n_cells, values_fill = 0) %>% +# as.data.frame() +# +# rownames(mat_df) <- mat_df$conga_gex_cluster +# mat_df$conga_gex_cluster <- NULL +# mat <- as.matrix(mat_df) +# +# ht <- Heatmap( +# mat, +# name = "Cells", +# col = colorRamp2(c(0, max(mat, na.rm = TRUE) / 2, max(mat, na.rm = TRUE)), +# c("white", "gold", "firebrick")), +# cluster_rows = TRUE, +# cluster_columns = TRUE, +# row_names_side = "left", +# column_title = "Annotation composition of CoNGA clusters", +# heatmap_legend_param = list(title = "Cells") +# ) +# draw(ht) +# } +``` + +# sample heatmaps +```{r} +#| label: sample-heatmap +if (isTRUE(params$show_cluster_sample_heatmap) && nrow(cluster_sample_tbl) > 0) { + top_clusters <- cluster_summary %>% + filter(n_cells >= params$min_cluster_size_plot) %>% + slice_head(n = params$top_n_clusters) %>% + pull(conga_gex_cluster) + + mat_df <- cluster_sample_tbl %>% + filter(conga_gex_cluster %in% top_clusters) %>% + pivot_wider(names_from = sample, values_from = n_cells, values_fill = 0) %>% + as.data.frame() + + rownames(mat_df) <- mat_df$conga_gex_cluster + mat_df$conga_gex_cluster <- NULL + mat <- as.matrix(mat_df) + + ht <- Heatmap( + mat, + name = "Cells", + col = colorRamp2(c(0, max(mat, na.rm = TRUE) / 2, max(mat, na.rm = TRUE)), + c("white", "skyblue", "navy")), + cluster_rows = TRUE, + cluster_columns = TRUE, + row_names_side = "left", + column_title = "Sample composition of CoNGA clusters", + heatmap_legend_param = list(title = "Cells") + ) + draw(ht) +} +``` + +# TCRi and CoNGA association +```{r} +#| label: tcri-vs-conga-assoc-plot +if (isTRUE(params$show_tcri_vs_conga) && any(!is.na(export_cells$tcri_score)) && any(!is.na(export_cells$conga_score))) { + p_assoc <- export_cells %>% + filter(!is.na(tcri_score), !is.na(conga_score)) %>% + ggplot(aes(x = tcri_score, y = conga_score)) + + geom_point(alpha = 0.35, size = 1.1) + + geom_smooth(method = "lm", se = TRUE, color = "firebrick") + + labs( + title = "Association: TCRi vs CoNGA scores", + x = "TCRi score", + y = "CoNGA score" + ) + + theme_scratch_pub(params$base_size) + + print(p_assoc) + save_plot_safe(p_assoc, glue("tcri_vs_conga_scatter.{params$figure_format}")) +} +``` + + +# clone-size-vs-conga-plot +```{r} +#| label: clone-size-vs-conga-plot +if (any(!is.na(export_cells$clone_size)) && any(!is.na(export_cells$conga_score))) { + p_size_conga <- export_cells %>% + filter(!is.na(clone_size), !is.na(conga_score)) %>% + ggplot(aes(x = log10(clone_size), y = conga_score)) + + geom_point(alpha = 0.2, size = 1) + + geom_smooth(method = "lm", color = "darkblue") + + labs( + title = "Clone Size vs CoNGA score", + x = "log10(Clone Size)", + y = "CoNGA score" + ) + + theme_scratch_pub(params$base_size) + + print(p_size_conga) + save_plot_safe(p_size_conga, glue("clonesize_vs_conga_scatter.{params$figure_format}")) +} +``` + +# tcri-conga-association-summary +```{r} +#| label: tcri-conga-association-summary + +# Build TCRi ↔ CoNGA association table safely +tcri_conga_assoc_tbl <- tibble::tibble() + +if (exists("export_cells")) { + + assoc_df <- export_cells %>% + dplyr::select( + dplyr::any_of(c( + "cell_id", + "sample", + "annot", + "clone_id", + "tcri_score", + "conga_score" + )) + ) %>% + dplyr::filter( + !is.na(tcri_score), + !is.na(conga_score) + ) + + save_table_safe( + tibble::tibble( + metric = c( + "cells_with_tcri_and_conga", + "unique_samples", + "unique_clones" + ), + value = c( + nrow(assoc_df), + dplyr::n_distinct(assoc_df$sample), + dplyr::n_distinct(assoc_df$clone_id) + ) + ), + "tcri_conga_overlap_diagnostics.tsv" + ) + + if (nrow(assoc_df) >= 3) { + + pearson_cor <- tryCatch( + stats::cor( + assoc_df$tcri_score, + assoc_df$conga_score, + use = "pairwise.complete.obs", + method = "pearson" + ), + error = function(e) NA_real_ + ) + + spearman_cor <- tryCatch( + stats::cor( + assoc_df$tcri_score, + assoc_df$conga_score, + use = "pairwise.complete.obs", + method = "spearman" + ), + error = function(e) NA_real_ + ) + + pearson_p <- tryCatch( + stats::cor.test( + assoc_df$tcri_score, + assoc_df$conga_score, + method = "pearson" + )$p.value, + error = function(e) NA_real_ + ) + + spearman_p <- tryCatch( + stats::cor.test( + assoc_df$tcri_score, + assoc_df$conga_score, + method = "spearman", + exact = FALSE + )$p.value, + error = function(e) NA_real_ + ) + + tcri_conga_assoc_tbl <- tibble::tibble( + n_cells = nrow(assoc_df), + pearson_cor = pearson_cor, + pearson_p = pearson_p, + spearman_cor = spearman_cor, + spearman_p = spearman_p + ) + + } else { + tcri_conga_assoc_tbl <- tibble::tibble( + n_cells = nrow(assoc_df), + pearson_cor = NA_real_, + pearson_p = NA_real_, + spearman_cor = NA_real_, + spearman_p = NA_real_, + note = "Too few overlapping non-missing TCRi and CoNGA scores to compute correlation." + ) + } + +} else { + tcri_conga_assoc_tbl <- tibble::tibble( + n_cells = 0, + pearson_cor = NA_real_, + pearson_p = NA_real_, + spearman_cor = NA_real_, + spearman_p = NA_real_, + note = "export_cells object not found." + ) +} + +save_table_safe(tcri_conga_assoc_tbl, "tcri_conga_association_summary.tsv") +``` + + +##Summary Display +```{r} +#| label: summary-tables-display +if (isTRUE(params$show_summary_tables)) { + kable(cluster_summary %>% slice_head(n = params$top_n_clusters), + caption = "Top CoNGA GEX clusters by cell count.") %>% + kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed")) + + kable(annotation_conga_summary %>% slice_head(n = params$top_n_states_heatmap), + caption = "Top annotations ranked by mean CoNGA score.") %>% + kable_styling(full_width = TRUE, bootstrap_options = c("striped", "hover", "condensed", "responsive")) + + kable(merge_diagnostics, + caption = "CoNGA merge diagnostics.") %>% + kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed")) + + if (exists("tcri_conga_assoc_tbl") && + nrow(tcri_conga_assoc_tbl) > 0 && + any(!is.na(tcri_conga_assoc_tbl$pearson_cor) | + !is.na(tcri_conga_assoc_tbl$spearman_cor) | + !is.na(tcri_conga_assoc_tbl$note))) { + kable(tcri_conga_assoc_tbl, + caption = "Association between TCRi and CoNGA scores.") %>% + kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed")) + } +} + +``` + +# warnings +```{r} +#| label: warnings + +# warnings +condition_missing <- is.null(condition_col_export) || all(is.na(export_cells$condition)) +patient_missing <- is.null(patient_col_export) || all(is.na(export_cells$patient)) +timepoint_missing <- is.null(timepoint_col_export) || all(is.na(export_cells$timepoint)) + +warn_tbl <- tibble( + warning = c( + "Few cells with CoNGA scores", + "Few cells with CoNGA clusters", + "Condition metadata unresolved or empty after export", + "Patient metadata unresolved or empty after export", + "Timepoint metadata unresolved or empty after export" + ), + triggered = c( + mean(!is.na(export_cells$conga_score)) < 0.5, + mean(!is.na(export_cells$conga_gex_cluster)) < 0.5, + condition_missing, + patient_missing, + timepoint_missing + ), + interpretation = c( + "Less than half the cells in Seurat received a propagated CoNGA score.", + "Less than half the cells in Seurat received a propagated CoNGA cluster.", + "Condition metadata were present in the input object but were not resolved or retained correctly in the exported CoNGA table.", + "Patient metadata were present in the input object but were not resolved or retained correctly in the exported CoNGA table.", + "Timepoint metadata were present in the input object but were not resolved or retained correctly in the exported CoNGA table." + ) +) %>% + filter(triggered) + +if (nrow(warn_tbl) == 0) { + cat("No major automatic warnings were triggered under the current CoNGA settings.") +} else { + print( + kable( + warn_tbl %>% select(-triggered), + caption = "Automatically generated CoNGA warnings and notes." + ) %>% + kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed") + ) + ) +} + +# warn_tbl <- tibble( +# warning = c( +# "Few cells with CoNGA scores", +# "Few cells with CoNGA clusters", +# "No condition metadata", +# "No patient metadata", +# "No timepoint metadata" +# ), +# triggered = c( +# mean(!is.na(export_cells$conga_score)) < 0.5, +# mean(!is.na(export_cells$conga_gex_cluster)) < 0.5, +# all(is.na(export_cells$condition)), +# all(is.na(export_cells$patient)), +# all(is.na(export_cells$timepoint)) +# ), +# interpretation = c( +# "Less than half the cells in Seurat received a propagated CoNGA score.", +# "Less than half the cells in Seurat received a propagated CoNGA cluster.", +# "Condition-level summaries were skipped because condition metadata were unavailable.", +# "Patient-level summaries were skipped because patient metadata were unavailable.", +# "Timepoint-level summaries were skipped because timepoint metadata were unavailable." +# ) +# ) %>% +# filter(triggered) +# +# if (nrow(warn_tbl) == 0) { +# cat("No major automatic warnings were triggered under the current CoNGA settings.") +# } else { +# kable(warn_tbl %>% select(-triggered), caption = "Automatically generated CoNGA warnings and notes.") %>% +# kable_styling(full_width = TRUE, bootstrap_options = c("striped", "hover", "condensed")) +# } +``` + +# session-info +```{r} +#| label: session-info +writeLines(capture.output(sessionInfo()), file.path(params$outdir, "sessionInfo.conga.txt")) +sessionInfo() +``` + + + + + + diff --git a/modules/scratch/CONGA/main.nf b/modules/scratch/CONGA/main.nf new file mode 100644 index 0000000..b70136d --- /dev/null +++ b/modules/scratch/CONGA/main.nf @@ -0,0 +1,63 @@ +process CONGA { + tag "${project_name}" + label 'process_medium' + // container "/rsrch8/home/genomic_med/sazaidi/Softwares/SCRATCH_TCR_2025/scratch-tcranalysis2.sif" + container "${params.container}" + + publishDir "${params.outdir}/CoNGA", mode: 'copy', overwrite: true +// publishDir "${params.outdir}/CoNGA", mode: params.publish_dir_mode ?: 'copy' + + input: + path seurat_rds + path export_cells + path qmd + val project_name + + output: + path "CoNGA_Report.html", emit: report_html + path "CoNGA_Report/data/seurat_with_CoNGA.rds", emit: seurat_with_conga + path "CoNGA_Report/data/*", emit: data + path "CoNGA_Report/tables/conga_export_cells.tsv", emit: export_cells + path "CoNGA_Report/tables/*", emit: tables + path "CoNGA_Report/figures/*", emit: figures + + script: + def repo_dir = params.conga_repo_dir ?: "/opt/tools/conga" + def python_bin = params.conga_python_bin ?: "/opt/conda/envs/tcrenv/bin/python" + def clone_col = params.clone_id_col ?: "clone_id" + + """ + mkdir -p CoNGA_Report + + /opt/quarto/bin/quarto render ${qmd} \ + -P seurat_rds="${seurat_rds}" \ + -P tcr_export_cells_file="${export_cells}" \ + -P filtered_contig_annotations_csvfile="${export_cells}" \ + -P conga_repo_dir="${repo_dir}" \ + -P conga_python_bin="${python_bin}" \ + -P clone_id_col="${clone_col}" \ + -P outdir="CoNGA_Report" \ + -P label_col="${params.label_col}" \ + -P sample_col="${params.sample_col}" \ + -P patient_col="${params.patient_col}" \ + -P condition_col="${params.condition_col}" \ + -P timepoint_col="${params.timepoint_col}" \ + -P batch_col="${params.batch_col}" \ + -P reduction_use="${params.reduction_use}" \ + -P make_umap_if_missing=${params.make_umap_if_missing} \ + -P umap_dims_max=${params.umap_dims_max} \ + -P umap_nfeatures=${params.umap_nfeatures} \ + -P raster_large_umap=${params.raster_large_umap} \ + -P conga_high_cutoff=${params.conga_high_cutoff ?: 0.8} \ + -P conga_mid_cutoff=${params.conga_mid_cutoff ?: 0.5} \ + -P use_quantile_cutoffs_if_score_not_bounded=${params.conga_use_quantile_cutoffs ?: true} \ + -P high_quantile=${params.conga_high_quantile ?: 0.9} \ + -P mid_quantile=${params.conga_mid_quantile ?: 0.5} \ + -P min_cells_per_group=${params.conga_min_cells_per_group ?: 10} \ + -P min_cluster_size_plot=${params.conga_min_cluster_size_plot ?: 5} \ + -P top_n_clusters=${params.conga_top_n_clusters ?: 20} \ + -P max_edges_to_plot=${params.conga_max_edges_to_plot ?: 5000} \ + -P report_label="${project_name} CoNGA" + """ +} + diff --git a/modules/scratch/CONSENSUS_CLUSTERING/Clonotype_Clustering_Consensus_Report.qmd b/modules/scratch/CONSENSUS_CLUSTERING/Clonotype_Clustering_Consensus_Report.qmd new file mode 100644 index 0000000..db6da84 --- /dev/null +++ b/modules/scratch/CONSENSUS_CLUSTERING/Clonotype_Clustering_Consensus_Report.qmd @@ -0,0 +1,1173 @@ +--- +title: "SCRATCH-TCR: Clonotype Clustering Consensus Report" +author: "Syed Shujaat Ali Zaidi" +format: + html: + toc: true + toc-depth: 3 + number-sections: true + code-fold: true + code-summary: "Show code" + embed-resources: true + theme: cosmo + df-print: paged +execute: + echo: false + warning: false + message: false + +params: + # ====================================================== + # Inputs + # ====================================================== + seurat_rds: "data/seurat_with_GIANA.rds" + export_cells_file: "data/giana_export_cells.tsv" + outdir: "Clonotype_Clustering_Consensus_Report" + + # Optional method-specific exports + gliph_export_cells_file: "" + tcrdist_export_cells_file: "" + giana_export_cells_file: "" + + # Optional previous outputs + metadata_file: "" + previous_summary_file: "" + + # ====================================================== + # Output controls + # ====================================================== + data_dir: "data" + tables_dir: "tables" + figures_dir: "figures" + save_tables: true + save_figures: true + save_updated_seurat: true + figure_format: "png" + figure_width: 10 + figure_height: 7 + figure_dpi: 300 + base_size: 12 + + # ====================================================== + # Metadata mapping in Seurat/export + # ====================================================== + label_col: "" + sample_col: "sample" + patient_col: "patient" + condition_col: "condition" + timepoint_col: "timepoint" + batch_col: "batch" + clone_id_col: "clone_id" + clone_size_col: "clone_size" + paired_tcr_col: "paired_tcr" + has_tcr_col: "has_tcr" + + label_candidates: "annot;Annotation;celltype;CellType;predicted_labels;predicted.celltype.l2;predicted.celltype.l1;celltypist;celltypist_label;azimuth_labels;seurat_clusters" + sample_candidates: "sample;META_SAMPLE;orig.ident;sample_id;Sample;SampleID" + patient_candidates: "patient;META_PATIENT;patient_id;Patient;subject;donor;case_id" + condition_candidates: "condition;META_TIMECOND;Condition;group;Group;status" + timepoint_candidates: "timepoint;META_TIMEPOINT;Timepoint;visit;Visit;day;Day" + batch_candidates: "batch;META_BATCH;Batch;library;Library;run;Run;lane;Lane" + clone_id_candidates: "clone_id;CTaa;clonotype;clone" + clone_size_candidates: "clone_size;CloneSize;clone_n" + paired_tcr_candidates: "paired_tcr;paired;is_paired" + has_tcr_candidates: "has_tcr;hasTCR;tcr_positive" + + # ====================================================== + # Method-specific columns + # ====================================================== + gliph_cluster_col: "gliph_cluster" + gliph_pattern_col: "gliph_pattern" + tcrdist_cluster_col: "tcrdist_cluster" + tcrdist_group_col: "tcrdist_group" + giana_cluster_col: "giana_cluster" + giana_group_col: "giana_group" + + gliph_cluster_candidates: "gliph_cluster;cluster;gliph2_cluster" + gliph_pattern_candidates: "gliph_pattern;pattern;motif" + tcrdist_cluster_candidates: "tcrdist_cluster;cluster;tcrdist_cluster_id" + tcrdist_group_candidates: "tcrdist_group;group;metaclonotype" + giana_cluster_candidates: "giana_cluster;cluster;giana_cluster_id" + giana_group_candidates: "giana_group;group;meta_group" + + # ====================================================== + # Consensus controls + # ====================================================== + consensus_min_methods: 2 + prefer_method_order: "gliph;tcrdist;giana" + use_majority_vote: true + consensus_label_prefix: "CONS" + assign_singleton_consensus: false + min_cells_per_group: 10 + min_cluster_size_plot: 3 + top_n_clusters: 20 + top_n_states_heatmap: 30 + + # ====================================================== + # Embedding controls + # ====================================================== + reduction_use: "umap" + make_umap_if_missing: false + umap_dims_max: 30 + umap_nfeatures: 3000 + raster_large_umap: true + label_clusters: true + + # ====================================================== + # Figure/report toggles + # ====================================================== + show_consensus_umap: true + show_consensus_size_barplot: true + show_method_overlap_heatmap: true + show_method_cluster_count_barplot: true + show_consensus_annotation_heatmap: true + show_consensus_sample_heatmap: true + show_fraction_by_annotation: true + show_fraction_by_sample: true + show_fraction_by_condition: true + show_fraction_by_patient: true + show_method_presence_barplot: true + show_summary_tables: true + + report_label: "Clonotype Clustering Consensus" +--- + +############################################################################### +# Integrates clonotype clustering results from GLIPH2, TCRdist3, and GIANA into a unified comparison framework within the TCR–GEX Seurat object. +# Quantifies overlap across methods and derives a consensus clonotype cluster assignment using configurable multi-method support rules. +# Supports flexible operation when all three methods are available or when only a subset of methods could be run. +# Generates publication-quality visualizations of method overlap, consensus cluster structure, and cluster distribution across cell states and samples. +# Exports an updated Seurat object and downstream-ready tables for final repertoire analysis and reporting. +############################################################################### + +# setup +```{r} +#| label: setup +suppressPackageStartupMessages({ + library(Seurat) + library(SeuratObject) + library(data.table) + library(dplyr) + library(tidyr) + library(stringr) + library(ggplot2) + library(forcats) + library(scales) + library(glue) + library(knitr) + library(kableExtra) + library(ComplexHeatmap) + library(circlize) + library(patchwork) +}) + +options(stringsAsFactors = FALSE) + +dir.create(params$outdir, recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$data_dir), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$tables_dir), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$figures_dir), recursive = TRUE, showWarnings = FALSE) + +`%||%` <- function(a, b) if (!is.null(a) && length(a) > 0 && !all(is.na(a))) a else b + +theme_scratch_pub <- function(base_size = 12) { + theme_bw(base_size = base_size) + + theme( + plot.title = element_text(face = "bold", size = base_size + 2, hjust = 0), + plot.subtitle = element_text(size = base_size, hjust = 0), + axis.title = element_text(face = "bold"), + axis.text = element_text(color = "black"), + panel.grid.minor = element_blank(), + panel.grid.major = element_line(linewidth = 0.2, color = "grey90"), + strip.background = element_rect(fill = "grey95", color = "grey80"), + strip.text = element_text(face = "bold"), + legend.title = element_text(face = "bold"), + legend.key = element_blank(), + plot.caption = element_text(size = base_size - 2, color = "grey40") + ) +} + +save_plot_safe <- function(plot_obj, filename, + width = params$figure_width, + height = params$figure_height, + dpi = params$figure_dpi) { + if (!isTRUE(params$save_figures)) return(invisible(NULL)) + ggsave( + filename = file.path(params$outdir, params$figures_dir, filename), + plot = plot_obj, + width = width, + height = height, + dpi = dpi, + bg = "white", + limitsize = FALSE + ) +} + +save_table_safe <- function(df, filename) { + if (!isTRUE(params$save_tables)) return(invisible(NULL)) + fwrite(df, file.path(params$outdir, params$tables_dir, filename), sep = "\t") +} + +save_rds_safe <- function(obj, filename) { + if (!isTRUE(params$save_updated_seurat)) return(invisible(NULL)) + saveRDS(obj, file.path(params$outdir, params$data_dir, filename)) +} + +safe_read_table <- function(path) { + if (is.null(path) || is.na(path) || path == "" || !file.exists(path)) return(NULL) + ext <- tolower(tools::file_ext(path)) + if (ext %in% c("tsv", "tab", "txt")) { + fread(path, sep = "\t") + } else { + fread(path) + } +} + +require_file <- function(path, msg = NULL) { + if (!file.exists(path)) stop(msg %||% paste("Missing required file:", path)) + path +} +# +# first_existing_col <- function(df, preferred, candidates = character()) { +# if (!is.null(preferred) && preferred != "" && preferred %in% colnames(df)) return(preferred) +# hits <- intersect(candidates, colnames(df)) +# if (length(hits) > 0) return(hits[[1]]) +# NULL +# } + + +safe_make_umap <- function(seu, reduction_name = "umap", dims = 1:30, nfeatures = 3000, seed = 1234) { + if ("RNA" %in% names(seu@assays)) DefaultAssay(seu) <- "RNA" + npcs <- min(max(dims), max(2, ncol(seu) - 1), 50) + nf <- min(nfeatures, nrow(seu)) + + if (!"pca" %in% Reductions(seu)) { + seu <- FindVariableFeatures(seu, nfeatures = nf, verbose = FALSE) + seu <- ScaleData(seu, verbose = FALSE) + seu <- RunPCA(seu, npcs = npcs, verbose = FALSE) + } + + use_dims <- dims[dims <= npcs] + if (length(use_dims) < 2) use_dims <- 1:min(10, npcs) + + seu <- FindNeighbors(seu, dims = use_dims, verbose = FALSE) + seu <- RunUMAP(seu, dims = use_dims, reduction.name = reduction_name, verbose = FALSE) + seu +} + +make_method_signature <- function(gliph, tcrdist, giana) { + paste0( + "GLIPH:", ifelse(is.na(gliph) | gliph == "", "NA", gliph), + "|TCRdist:", ifelse(is.na(tcrdist) | tcrdist == "", "NA", tcrdist), + "|GIANA:", ifelse(is.na(giana) | giana == "", "NA", giana) + ) +} + +safe_n_unique <- function(x) length(unique(na.omit(x))) + +sanitize_param_string <- function(x) { + if (is.null(x) || length(x) == 0) return(NULL) + trimws(gsub("\\u00A0", " ", as.character(x))) +} + +normalize_colnames <- function(df) { + colnames(df) <- trimws(gsub("\\u00A0", " ", colnames(df))) + df +} + +split_candidate_string <- function(x) { + if (is.null(x) || length(x) == 0 || is.na(x) || x == "") return(character()) + x <- gsub('"', "", as.character(x)) + x <- trimws(unlist(strsplit(x, ";", fixed = TRUE))) + x[nzchar(x)] +} + +first_existing_col <- function(df, preferred, candidates = character()) { + df <- normalize_colnames(df) + preferred <- sanitize_param_string(preferred) + + if (length(candidates) == 1 && is.character(candidates)) { + candidates <- split_candidate_string(candidates) + } + candidates <- trimws(gsub("\\u00A0", " ", as.character(candidates))) + + if (!is.null(preferred) && preferred != "" && preferred %in% colnames(df)) return(preferred) + + hits <- intersect(candidates, colnames(df)) + if (length(hits) > 0) return(hits[[1]]) + NULL +} + + +``` + +# Input Loading +```{r} +#| label: read-inputs +require_file(params$seurat_rds, sprintf("Seurat RDS not found: %s", params$seurat_rds)) + +seu <- readRDS(params$seurat_rds) +export_cells_main <- safe_read_table(params$export_cells_file) +gliph_export_tbl <- safe_read_table(params$gliph_export_cells_file) +tcrdist_export_tbl <- safe_read_table(params$tcrdist_export_cells_file) +giana_export_tbl <- safe_read_table(params$giana_export_cells_file) +metadata_tbl <- safe_read_table(params$metadata_file) +previous_summary_tbl <- safe_read_table(params$previous_summary_file) + +if (is.null(export_cells_main)) { + export_cells_main <- seu@meta.data %>% tibble::rownames_to_column("cell_id") +} +``` + +# Resolve Columns +```{r} +#| label: resolve-columns +md <- normalize_colnames(seu@meta.data) +seu@meta.data <- md + +label_col <- first_existing_col(md, params$label_col, params$label_candidates) +sample_col <- first_existing_col(md, params$sample_col, params$sample_candidates) +patient_col <- first_existing_col(md, params$patient_col, params$patient_candidates) +condition_col <- first_existing_col(md, params$condition_col, params$condition_candidates) +timepoint_col <- first_existing_col(md, params$timepoint_col, params$timepoint_candidates) +batch_col <- first_existing_col(md, params$batch_col, params$batch_candidates) +clone_id_col <- first_existing_col(md, params$clone_id_col, params$clone_id_candidates) +clone_size_col <- first_existing_col(md, params$clone_size_col, params$clone_size_candidates) +paired_tcr_col <- first_existing_col(md, params$paired_tcr_col, params$paired_tcr_candidates) +has_tcr_col <- first_existing_col(md, params$has_tcr_col, params$has_tcr_candidates) + +gliph_cluster_col <- first_existing_col(md, params$gliph_cluster_col, params$gliph_cluster_candidates) +gliph_pattern_col <- first_existing_col(md, params$gliph_pattern_col, params$gliph_pattern_candidates) +tcrdist_cluster_col <- first_existing_col(md, params$tcrdist_cluster_col, params$tcrdist_cluster_candidates) +tcrdist_group_col <- first_existing_col(md, params$tcrdist_group_col, params$tcrdist_group_candidates) +giana_cluster_col <- first_existing_col(md, params$giana_cluster_col, params$giana_cluster_candidates) +giana_group_col <- first_existing_col(md, params$giana_group_col, params$giana_group_candidates) + +if (is.null(label_col)) stop("Could not resolve an annotation label column in Seurat metadata.") +if (is.null(sample_col)) stop("Could not resolve sample column in Seurat metadata.") +if (is.null(patient_col)) stop("Could not resolve patient column in Seurat metadata.") +if (is.null(condition_col)) stop("Could not resolve condition column in Seurat metadata.") +if (is.null(timepoint_col)) stop("Could not resolve timepoint column in Seurat metadata.") +if (is.null(clone_id_col)) stop("Could not resolve a clone ID column in Seurat metadata.") +if (is.null(clone_size_col)) stop("Could not resolve a clone size column in Seurat metadata.") +if (is.null(has_tcr_col)) stop("Could not resolve a has_tcr column in Seurat metadata.") +if (is.null(paired_tcr_col)) stop("Could not resolve a paired_tcr column in Seurat metadata.") + +if (!"META_SAMPLE" %in% colnames(seu@meta.data) && !is.null(sample_col)) { + seu$META_SAMPLE <- as.character(seu@meta.data[[sample_col]]) +} +if (!"META_PATIENT" %in% colnames(seu@meta.data) && !is.null(patient_col)) { + seu$META_PATIENT <- as.character(seu@meta.data[[patient_col]]) +} +if (!"META_TIMECOND" %in% colnames(seu@meta.data) && !is.null(condition_col)) { + seu$META_TIMECOND <- as.character(seu@meta.data[[condition_col]]) +} +if (!"META_TIMEPOINT" %in% colnames(seu@meta.data) && !is.null(timepoint_col)) { + seu$META_TIMEPOINT <- as.character(seu@meta.data[[timepoint_col]]) +} +if (!"META_BATCH" %in% colnames(seu@meta.data) && !is.null(batch_col)) { + seu$META_BATCH <- as.character(seu@meta.data[[batch_col]]) +} + +print(list( + resolved_label_col = label_col, + resolved_sample_col = sample_col, + resolved_patient_col = patient_col, + resolved_condition_col = condition_col, + resolved_timepoint_col = timepoint_col, + resolved_batch_col = batch_col, + resolved_clone_id_col = clone_id_col, + resolved_clone_size_col = clone_size_col, + resolved_paired_tcr_col = paired_tcr_col, + resolved_has_tcr_col = has_tcr_col, + resolved_gliph_cluster_col = gliph_cluster_col, + resolved_gliph_pattern_col = gliph_pattern_col, + resolved_tcrdist_cluster_col = tcrdist_cluster_col, + resolved_tcrdist_group_col = tcrdist_group_col, + resolved_giana_cluster_col = giana_cluster_col, + resolved_giana_group_col = giana_group_col +)) + +``` + +# Ensure Embedding +```{r} +#| label: ensure-embedding +reduction_to_use <- params$reduction_use +if (!(reduction_to_use %in% names(seu@reductions)) && isTRUE(params$make_umap_if_missing)) { + seu <- safe_make_umap( + seu, + reduction_name = params$reduction_use, + dims = 1:params$umap_dims_max, + nfeatures = params$umap_nfeatures + ) +} +if (!(reduction_to_use %in% names(seu@reductions))) { + reduction_to_use <- if ("umap" %in% names(seu@reductions)) "umap" else if ("tsne" %in% names(seu@reductions)) "tsne" else if ("pca" %in% names(seu@reductions)) "pca" else stop("No usable embedding found.") +} +``` + +# Build Export table +```{r} +# Build Export table +```{r} +#| label: build-export +embed_df <- as.data.frame(Embeddings(seu, reduction_to_use)) %>% + tibble::rownames_to_column("cell_id") + +export_cells <- seu@meta.data %>% + tibble::rownames_to_column("cell_id") %>% + transmute( + cell_id, + sample = if (!is.null(sample_col)) as.character(.data[[sample_col]]) else NA_character_, + patient = if (!is.null(patient_col)) as.character(.data[[patient_col]]) else NA_character_, + condition = if (!is.null(condition_col)) as.character(.data[[condition_col]]) else NA_character_, + timepoint = if (!is.null(timepoint_col)) as.character(.data[[timepoint_col]]) else NA_character_, + batch = if (!is.null(batch_col)) as.character(.data[[batch_col]]) else NA_character_, + annot = as.character(.data[[label_col]]), + clone_id = as.character(.data[[clone_id_col]]), + clone_size = suppressWarnings(as.numeric(.data[[clone_size_col]])), + has_tcr = as.logical(.data[[has_tcr_col]]), + paired_tcr = as.logical(.data[[paired_tcr_col]]), + gliph_cluster = if (!is.null(gliph_cluster_col)) as.character(.data[[gliph_cluster_col]]) else NA_character_, + gliph_pattern = if (!is.null(gliph_pattern_col)) as.character(.data[[gliph_pattern_col]]) else NA_character_, + tcrdist_cluster = if (!is.null(tcrdist_cluster_col)) as.character(.data[[tcrdist_cluster_col]]) else NA_character_, + tcrdist_group = if (!is.null(tcrdist_group_col)) as.character(.data[[tcrdist_group_col]]) else NA_character_, + giana_cluster = if (!is.null(giana_cluster_col)) as.character(.data[[giana_cluster_col]]) else NA_character_, + giana_group = if (!is.null(giana_group_col)) as.character(.data[[giana_group_col]]) else NA_character_ + ) %>% + left_join(embed_df, by = "cell_id") + +if (!is.null(export_cells_main) && "cell_id" %in% colnames(export_cells_main)) { + export_cells_main2 <- export_cells_main %>% + mutate(cell_id = as.character(cell_id)) %>% + transmute( + cell_id = cell_id, + sample.main = if ("sample" %in% colnames(.)) as.character(sample) else NA_character_, + patient.main = if ("patient" %in% colnames(.)) as.character(patient) else NA_character_, + condition.main = if ("condition" %in% colnames(.)) as.character(condition) else NA_character_, + timepoint.main = if ("timepoint" %in% colnames(.)) as.character(timepoint) else NA_character_, + batch.main = if ("batch" %in% colnames(.)) as.character(batch) else NA_character_, + annot.main = if ("annot" %in% colnames(.)) as.character(annot) else NA_character_, + clone_id.main = if ("clone_id" %in% colnames(.)) as.character(clone_id) else NA_character_, + clone_size.main = if ("clone_size" %in% colnames(.)) suppressWarnings(as.numeric(clone_size)) else NA_real_, + has_tcr.main = if ("has_tcr" %in% colnames(.)) as.logical(has_tcr) else NA, + paired_tcr.main = if ("paired_tcr" %in% colnames(.)) as.logical(paired_tcr) else NA + ) + + export_cells <- export_cells %>% + left_join(export_cells_main2, by = "cell_id") %>% + mutate( + sample = coalesce(sample, sample.main), + patient = coalesce(patient, patient.main), + condition = coalesce(condition, condition.main), + timepoint = coalesce(timepoint, timepoint.main), + batch = coalesce(batch, batch.main), + annot = coalesce(annot, annot.main), + clone_id = coalesce(clone_id, clone_id.main), + clone_size = coalesce(clone_size, clone_size.main), + has_tcr = coalesce(has_tcr, has_tcr.main), + paired_tcr = coalesce(paired_tcr, paired_tcr.main) + ) %>% + select(-ends_with(".main")) +} + +if (!is.null(gliph_export_tbl) && "cell_id" %in% colnames(gliph_export_tbl)) { + gliph_tbl2 <- gliph_export_tbl %>% + mutate(cell_id = as.character(cell_id)) %>% + transmute( + cell_id = cell_id, + gliph_cluster.gliph = if ("gliph_cluster" %in% colnames(.)) as.character(gliph_cluster) else NA_character_, + gliph_pattern.gliph = if ("gliph_pattern" %in% colnames(.)) as.character(gliph_pattern) else NA_character_, + gliph_group.gliph = if ("gliph_group" %in% colnames(.)) as.character(gliph_group) else NA_character_, + gliph_motif.gliph = if ("gliph_motif" %in% colnames(.)) as.character(gliph_motif) else NA_character_ + ) + + export_cells <- export_cells %>% + left_join(gliph_tbl2, by = "cell_id") %>% + mutate( + gliph_cluster = coalesce(gliph_cluster, gliph_cluster.gliph), + gliph_pattern = coalesce(gliph_pattern, gliph_pattern.gliph, gliph_group.gliph, gliph_motif.gliph) + ) %>% + select(-gliph_cluster.gliph, -gliph_pattern.gliph, -gliph_group.gliph, -gliph_motif.gliph) +} + +if (!is.null(tcrdist_export_tbl) && "cell_id" %in% colnames(tcrdist_export_tbl)) { + tcrdist_tbl2 <- tcrdist_export_tbl %>% + mutate(cell_id = as.character(cell_id)) %>% + transmute( + cell_id = cell_id, + tcrdist_cluster.tcrdist = if ("tcrdist_cluster" %in% colnames(.)) as.character(tcrdist_cluster) else NA_character_, + tcrdist_group.tcrdist = if ("tcrdist_group" %in% colnames(.)) as.character(tcrdist_group) else NA_character_, + metaclonotype.tcrdist = if ("metaclonotype" %in% colnames(.)) as.character(metaclonotype) else NA_character_ + ) + + export_cells <- export_cells %>% + left_join(tcrdist_tbl2, by = "cell_id") %>% + mutate( + tcrdist_cluster = coalesce(tcrdist_cluster, tcrdist_cluster.tcrdist), + tcrdist_group = coalesce(tcrdist_group, tcrdist_group.tcrdist, metaclonotype.tcrdist) + ) %>% + select(-tcrdist_cluster.tcrdist, -tcrdist_group.tcrdist, -metaclonotype.tcrdist) +} + +if (!is.null(giana_export_tbl) && "cell_id" %in% colnames(giana_export_tbl)) { + giana_tbl2 <- giana_export_tbl %>% + mutate(cell_id = as.character(cell_id)) %>% + transmute( + cell_id = cell_id, + giana_cluster.giana = if ("giana_cluster" %in% colnames(.)) as.character(giana_cluster) else NA_character_, + giana_group.giana = if ("giana_group" %in% colnames(.)) as.character(giana_group) else NA_character_ + ) + + export_cells <- export_cells %>% + left_join(giana_tbl2, by = "cell_id") %>% + mutate( + giana_cluster = coalesce(giana_cluster, giana_cluster.giana), + giana_group = coalesce(giana_group, giana_group.giana) + ) %>% + select(-giana_cluster.giana, -giana_group.giana) +} + +save_table_safe(export_cells, "consensus_export_cells.tsv") + +``` + +# Method presence and Overlap +```{r} +#| label: method-presence +export_cells <- export_cells %>% + mutate( + has_gliph = !is.na(gliph_cluster) | !is.na(gliph_pattern), + has_tcrdist = !is.na(tcrdist_cluster) | !is.na(tcrdist_group), + has_giana = !is.na(giana_cluster) | !is.na(giana_group), + n_methods = as.integer(has_gliph) + as.integer(has_tcrdist) + as.integer(has_giana), + method_signature = make_method_signature(gliph_cluster, tcrdist_cluster, giana_cluster) + ) + +method_presence_summary <- tibble( + method = c("GLIPH2", "TCRdist3", "GIANA"), + n_cells = c( + sum(export_cells$has_gliph, na.rm = TRUE), + sum(export_cells$has_tcrdist, na.rm = TRUE), + sum(export_cells$has_giana, na.rm = TRUE) + ), + frac_cells = c( + mean(export_cells$has_gliph, na.rm = TRUE), + mean(export_cells$has_tcrdist, na.rm = TRUE), + mean(export_cells$has_giana, na.rm = TRUE) + ) +) +save_table_safe(method_presence_summary, "method_presence_summary.tsv") + +pairwise_overlap_tbl <- tibble( + comparison = c("GLIPH2_vs_TCRdist3", "GLIPH2_vs_GIANA", "TCRdist3_vs_GIANA"), + n_overlap = c( + sum(export_cells$has_gliph & export_cells$has_tcrdist, na.rm = TRUE), + sum(export_cells$has_gliph & export_cells$has_giana, na.rm = TRUE), + sum(export_cells$has_tcrdist & export_cells$has_giana, na.rm = TRUE) + ) +) +save_table_safe(pairwise_overlap_tbl, "pairwise_method_overlap.tsv") +``` + +# Build consensus labels +```{r} +#| label: build-consensus +export_cells <- export_cells %>% + mutate( + gliph_use = ifelse(!is.na(gliph_cluster) & gliph_cluster != "", gliph_cluster, + ifelse(!is.na(gliph_pattern) & gliph_pattern != "", paste0("PAT:", gliph_pattern), NA_character_)), + tcrdist_use = ifelse(!is.na(tcrdist_cluster) & tcrdist_cluster != "", tcrdist_cluster, + ifelse(!is.na(tcrdist_group) & tcrdist_group != "", tcrdist_group, NA_character_)), + giana_use = ifelse(!is.na(giana_cluster) & giana_cluster != "", giana_cluster, + ifelse(!is.na(giana_group) & giana_group != "", giana_group, NA_character_)) + ) + +consensus_base <- export_cells %>% + mutate( + n_methods_nonmissing = as.integer(!is.na(gliph_use)) + as.integer(!is.na(tcrdist_use)) + as.integer(!is.na(giana_use)) + ) + +if (isTRUE(params$use_majority_vote)) { + consensus_base <- consensus_base %>% + rowwise() %>% + mutate( + consensus_raw = { + vals <- c(gliph_use, tcrdist_use, giana_use) + vals <- vals[!is.na(vals) & vals != ""] + if (length(vals) < params$consensus_min_methods) { + if (isTRUE(params$assign_singleton_consensus) && length(vals) == 1) vals[1] else NA_character_ + } else { + tab <- sort(table(vals), decreasing = TRUE) + names(tab)[1] + } + } + ) %>% + ungroup() +} else { + consensus_base <- consensus_base %>% + mutate( + consensus_raw = ifelse( + n_methods_nonmissing >= params$consensus_min_methods, + method_signature, + ifelse(isTRUE(params$assign_singleton_consensus), coalesce(gliph_use, tcrdist_use, giana_use), NA_character_) + ) + ) +} + +consensus_map <- consensus_base %>% + filter(!is.na(consensus_raw), consensus_raw != "") %>% + count(consensus_raw, name = "n_cells") %>% + arrange(desc(n_cells)) %>% + mutate(consensus_cluster = paste0(params$consensus_label_prefix, "_", row_number())) + +export_cells <- consensus_base %>% + left_join(consensus_map, by = "consensus_raw") + +save_table_safe(consensus_map, "consensus_cluster_map.tsv") +``` + +# Merge consensus back into Seurat +```{r} +#| label: merge-consensus-into-seurat +seu_md <- seu@meta.data %>% + tibble::rownames_to_column("cell_id") %>% + left_join( + export_cells %>% + select(cell_id, consensus_raw, consensus_cluster, n_methods_nonmissing, method_signature), + by = "cell_id" + ) + +rownames(seu_md) <- seu_md$cell_id +seu_md$cell_id <- NULL +seu@meta.data <- as.data.frame(seu_md) + +save_rds_safe(seu, "seurat_with_consensus_clonotype_clusters.rds") +``` + +# Summary Tables +```{r} +#| label: summary-tables +summary_rollup <- tibble( + metric = c( + "Report label", + "Cells in Seurat", + "Cells with GLIPH2 annotation", + "Cells with TCRdist3 annotation", + "Cells with GIANA annotation", + "Cells with >=2 methods", + "Cells with consensus cluster", + "Unique consensus clusters", + "Unique annotations", + "Unique samples", + "Selected embedding" + ), + value = c( + params$report_label, + nrow(export_cells), + sum(export_cells$has_gliph, na.rm = TRUE), + sum(export_cells$has_tcrdist, na.rm = TRUE), + sum(export_cells$has_giana, na.rm = TRUE), + sum(export_cells$n_methods_nonmissing >= 2, na.rm = TRUE), + sum(!is.na(export_cells$consensus_cluster)), + safe_n_unique(export_cells$consensus_cluster), + safe_n_unique(export_cells$annot), + safe_n_unique(export_cells$sample), + reduction_to_use + ) +) +save_table_safe(summary_rollup, "consensus_summary_rollup.tsv") + +consensus_cluster_summary <- export_cells %>% + filter(!is.na(consensus_cluster)) %>% + count(consensus_cluster, consensus_raw, n_methods_nonmissing, name = "n_cells") %>% + mutate(frac = n_cells / sum(n_cells)) %>% + arrange(desc(n_cells)) +save_table_safe(consensus_cluster_summary, "consensus_cluster_summary.tsv") + +annotation_consensus_summary <- export_cells %>% + filter(!is.na(annot)) %>% + group_by(annot) %>% + summarise( + n_cells = n(), + n_consensus = sum(!is.na(consensus_cluster)), + frac_consensus = n_consensus / n_cells, + .groups = "drop" + ) %>% + arrange(desc(frac_consensus)) +save_table_safe(annotation_consensus_summary, "annotation_consensus_summary.tsv") + +sample_consensus_summary <- export_cells %>% + filter(!is.na(sample)) %>% + group_by(sample) %>% + summarise( + n_cells = n(), + n_consensus = sum(!is.na(consensus_cluster)), + frac_consensus = n_consensus / n_cells, + .groups = "drop" + ) %>% + arrange(desc(frac_consensus)) +save_table_safe(sample_consensus_summary, "sample_consensus_summary.tsv") + +if (any(!is.na(export_cells$condition))) { + condition_consensus_summary <- export_cells %>% + filter(!is.na(condition)) %>% + group_by(condition) %>% + summarise( + n_cells = n(), + n_consensus = sum(!is.na(consensus_cluster)), + frac_consensus = n_consensus / n_cells, + .groups = "drop" + ) + save_table_safe(condition_consensus_summary, "condition_consensus_summary.tsv") +} + +if (any(!is.na(export_cells$patient))) { + patient_consensus_summary <- export_cells %>% + filter(!is.na(patient)) %>% + group_by(patient) %>% + summarise( + n_cells = n(), + n_consensus = sum(!is.na(consensus_cluster)), + frac_consensus = n_consensus / n_cells, + .groups = "drop" + ) + save_table_safe(patient_consensus_summary, "patient_consensus_summary.tsv") +} + +consensus_annotation_tbl <- export_cells %>% + filter(!is.na(consensus_cluster), !is.na(annot)) %>% + count(consensus_cluster, annot, name = "n_cells") +save_table_safe(consensus_annotation_tbl, "consensus_cluster_annotation_composition.tsv") + +consensus_sample_tbl <- export_cells %>% + filter(!is.na(consensus_cluster), !is.na(sample)) %>% + count(consensus_cluster, sample, name = "n_cells") +save_table_safe(consensus_sample_tbl, "consensus_cluster_sample_composition.tsv") +``` + +# Method Overlap matrix +```{r} +#| label: overlap-matrix + +print(table(export_cells$has_gliph, useNA = "ifany")) +print(table(export_cells$has_tcrdist, useNA = "ifany")) +print(table(export_cells$has_giana, useNA = "ifany")) + +print(colnames(export_cells)) +print(sum(!is.na(export_cells$giana_cluster))) +print(sum(!is.na(export_cells$giana_group))) + +overlap_matrix <- matrix( + c( + sum(export_cells$has_gliph, na.rm = TRUE), + sum(export_cells$has_gliph & export_cells$has_tcrdist, na.rm = TRUE), + sum(export_cells$has_gliph & export_cells$has_giana, na.rm = TRUE), + sum(export_cells$has_tcrdist & export_cells$has_gliph, na.rm = TRUE), + sum(export_cells$has_tcrdist, na.rm = TRUE), + sum(export_cells$has_tcrdist & export_cells$has_giana, na.rm = TRUE), + sum(export_cells$has_giana & export_cells$has_gliph, na.rm = TRUE), + sum(export_cells$has_giana & export_cells$has_tcrdist, na.rm = TRUE), + sum(export_cells$has_giana, na.rm = TRUE) + ), + nrow = 3, + byrow = TRUE +) +rownames(overlap_matrix) <- c("GLIPH2", "TCRdist3", "GIANA") +colnames(overlap_matrix) <- c("GLIPH2", "TCRdist3", "GIANA") +save_table_safe(as.data.frame(overlap_matrix) %>% tibble::rownames_to_column("method"), "method_overlap_matrix.tsv") +``` + +# Report Overview +```{r} +#| label: overview +kable(summary_rollup, caption = "High-level overview of consensus clonotype clustering.") %>% + kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed")) +``` + +# Method Presence Plots +```{r} +#| label: method-presence-plot +if (isTRUE(params$show_method_presence_barplot)) { + p_presence <- method_presence_summary %>% + mutate(method = fct_reorder(method, frac_cells)) %>% + ggplot(aes(x = method, y = frac_cells)) + + geom_col(fill = "steelblue") + + coord_flip() + + scale_y_continuous(labels = percent_format(accuracy = 1)) + + labs( + title = "Method coverage across cells", + x = NULL, + y = "Fraction of cells annotated" + ) + + theme_scratch_pub(params$base_size) + + p_presence + save_plot_safe(p_presence, glue("method_presence_barplot.{params$figure_format}")) +} +``` + +# method-overlap-heatmap +```{r} +#| label: method-overlap-heatmap +#| fig-width: 7 +#| fig-height: 6 +if (isTRUE(params$show_method_overlap_heatmap)) { + + finite_vals <- as.numeric(overlap_matrix) + finite_vals <- finite_vals[is.finite(finite_vals)] + + if (length(finite_vals) == 0) { + cat("No method-overlap heatmap was produced because the overlap matrix contained no finite values.\n") + } else { + min_val <- min(finite_vals, na.rm = TRUE) + max_val <- max(finite_vals, na.rm = TRUE) + + if (max_val <= min_val) { + cat("Method-overlap heatmap was skipped because all overlap values were identical.\n\n") + + print( + kableExtra::kbl( + as.data.frame(overlap_matrix) %>% tibble::rownames_to_column("method"), + caption = "Method overlap matrix." + ) %>% + kableExtra::kable_styling( + full_width = FALSE, + bootstrap_options = c("striped", "hover", "condensed") + ) + ) + } else { + mid_val <- (min_val + max_val) / 2 + ht <- Heatmap( + overlap_matrix, + name = "Cells", + col = colorRamp2( + c(min_val, mid_val, max_val), + c("white", "gold", "firebrick") + ), + cluster_rows = FALSE, + cluster_columns = FALSE, + row_names_side = "left", + row_names_gp = grid::gpar(fontsize = 10), + column_names_gp = grid::gpar(fontsize = 10), + rect_gp = grid::gpar(col = "grey85"), + column_title = "Pairwise overlap between clustering methods", + heatmap_legend_param = list(title = "Cells") + ) + + + ComplexHeatmap::draw(ht) + } + } +} +``` + +# Consensus UMAP and cluster sizes +```{r} +#| label: consensus-cluster-plots +#| fig-width: 12 +#| fig-height: 9 + +if (isTRUE(params$show_consensus_umap) && any(!is.na(export_cells$consensus_cluster))) { + p_consensus <- DimPlot( + seu, + reduction = reduction_to_use, + group.by = "consensus_cluster", + label = FALSE, + raster = isTRUE(params$raster_large_umap) + ) + + ggtitle("Embedding overlay: consensus clonotype clusters") + + theme_scratch_pub(params$base_size) + + theme( + legend.position = "none" + ) + + print(p_consensus) + save_plot_safe( + p_consensus, + glue("consensus_cluster_umap.{params$figure_format}"), + width = 10, + height = 8 + ) +} else { + cat("No consensus UMAP was produced because no cells received a consensus cluster assignment.\n") +} + +if (isTRUE(params$show_consensus_size_barplot) && nrow(consensus_cluster_summary) > 0) { + top_clusters_display <- params$top_n_clusters + if (is.null(top_clusters_display) || length(top_clusters_display) == 0 || is.na(top_clusters_display)) { + top_clusters_display <- 20 + } + top_clusters_display <- as.integer(top_clusters_display) + + top_clusters <- consensus_cluster_summary %>% + slice_head(n = top_clusters_display) + + p_bar <- top_clusters %>% + mutate(consensus_cluster = fct_reorder(consensus_cluster, n_cells)) %>% + ggplot(aes(x = consensus_cluster, y = n_cells)) + + geom_col(fill = "steelblue") + + coord_flip() + + scale_y_continuous(labels = scales::comma) + + labs( + title = "Top consensus clonotype clusters", + x = NULL, + y = "Cells" + ) + + theme_scratch_pub(params$base_size) + + print(p_bar) + save_plot_safe( + p_bar, + glue("consensus_cluster_size_barplot.{params$figure_format}") + ) +} else { + cat("No consensus cluster-size barplot was produced because no consensus clusters were available.\n") +} + +``` + +## Method cluster count comparison +```{r} +#| label: method-cluster-count-plot +if (isTRUE(params$show_method_cluster_count_barplot)) { + method_cluster_counts <- tibble( + method = c("GLIPH2", "TCRdist3", "GIANA", "Consensus"), + n_clusters = c( + safe_n_unique(export_cells$gliph_cluster), + safe_n_unique(export_cells$tcrdist_cluster), + safe_n_unique(export_cells$giana_cluster), + safe_n_unique(export_cells$consensus_cluster) + ) + ) + save_table_safe(method_cluster_counts, "method_cluster_counts.tsv") + + p_counts <- method_cluster_counts %>% + mutate(method = fct_reorder(method, n_clusters)) %>% + ggplot(aes(x = method, y = n_clusters)) + + geom_col(fill = "steelblue") + + coord_flip() + + scale_y_continuous(labels = comma) + + labs( + title = "Number of clusters detected by method", + x = NULL, + y = "Number of clusters" + ) + + theme_scratch_pub(params$base_size) + + p_counts + save_plot_safe(p_counts, glue("method_cluster_count_barplot.{params$figure_format}")) +} +``` + +# Consensus heatmaps +```{r} +#| label: consensus-annotation-heatmap +if (isTRUE(params$show_consensus_annotation_heatmap) && nrow(consensus_annotation_tbl) > 0) { + top_clusters <- consensus_cluster_summary %>% + filter(n_cells >= params$min_cluster_size_plot) %>% + slice_head(n = params$top_n_clusters) %>% + pull(consensus_cluster) + + mat_df <- consensus_annotation_tbl %>% + filter(consensus_cluster %in% top_clusters) %>% + pivot_wider(names_from = annot, values_from = n_cells, values_fill = 0) %>% + as.data.frame() + + rownames(mat_df) <- mat_df$consensus_cluster + mat_df$consensus_cluster <- NULL + mat <- as.matrix(mat_df) + + max_mat <- max(mat, na.rm = TRUE) + if (!is.finite(max_mat) || max_mat <= 0) max_mat <- 1 + + ht <- Heatmap( + mat, + name = "Cells", + col = circlize::colorRamp2( + c(0, max_mat / 2, max_mat), + c("white", "gold", "firebrick") + ), + cluster_rows = TRUE, + cluster_columns = TRUE, + row_names_side = "left", + row_names_gp = grid::gpar(fontsize = 8), + column_names_gp = grid::gpar(fontsize = 9), + column_names_rot = 45, + rect_gp = grid::gpar(col = "grey85"), + column_title = "Annotation composition of consensus clusters", + heatmap_legend_param = list(title = "Cells") + ) + + ComplexHeatmap::draw(ht) +} + +``` + +# consensus-sample-heatmap +```{r} +#| label: consensus-sample-heatmap +if (isTRUE(params$show_consensus_sample_heatmap) && nrow(consensus_sample_tbl) > 0) { + top_clusters <- consensus_cluster_summary %>% + filter(n_cells >= params$min_cluster_size_plot) %>% + slice_head(n = params$top_n_clusters) %>% + pull(consensus_cluster) + + mat_df <- consensus_sample_tbl %>% + filter(consensus_cluster %in% top_clusters) %>% + pivot_wider(names_from = sample, values_from = n_cells, values_fill = 0) %>% + as.data.frame() + + rownames(mat_df) <- mat_df$consensus_cluster + mat_df$consensus_cluster <- NULL + mat <- as.matrix(mat_df) + + max_mat <- max(mat, na.rm = TRUE) + if (!is.finite(max_mat) || max_mat <= 0) max_mat <- 1 + + ht <- Heatmap( + mat, + name = "Cells", + col = circlize::colorRamp2( + c(0, max_mat / 2, max_mat), + c("white", "skyblue", "navy") + ), + cluster_rows = TRUE, + cluster_columns = TRUE, + row_names_side = "left", + row_names_gp = grid::gpar(fontsize = 8), + column_names_gp = grid::gpar(fontsize = 8), + column_names_rot = 45, + rect_gp = grid::gpar(col = "grey85"), + column_title = "Sample composition of consensus clusters", + heatmap_legend_param = list(title = "Cells") + ) + + + ComplexHeatmap::draw(ht) +} + +``` + +```{r} +#| label: fraction-plots +plot_frac_by_group <- function(df, group_col, title_txt, filename) { + tmp <- df %>% + filter(!is.na(.data[[group_col]])) %>% + group_by(.data[[group_col]]) %>% + summarise( + n_cells = n(), + n_consensus = sum(!is.na(consensus_cluster)), + frac_consensus = n_consensus / n_cells, + .groups = "drop" + ) %>% + filter(n_cells >= params$min_cells_per_group) + + if (nrow(tmp) == 0) return(NULL) + + p <- tmp %>% + mutate(.group = fct_reorder(.data[[group_col]], frac_consensus)) %>% + ggplot(aes(x = .group, y = frac_consensus)) + + geom_col(fill = "steelblue") + + coord_flip() + + scale_y_continuous(labels = percent_format(accuracy = 1)) + + labs( + title = title_txt, + x = NULL, + y = "Cells with consensus cluster" + ) + + theme_scratch_pub(params$base_size) + + print(p) + save_plot_safe(p, filename) + invisible(p) +} + +if (isTRUE(params$show_fraction_by_annotation)) { + plot_frac_by_group(export_cells %>% rename(group_annot = annot), "group_annot", + "Fraction of cells with consensus cluster by cell state", + glue("consensus_fraction_by_annotation.{params$figure_format}")) +} +if (isTRUE(params$show_fraction_by_sample) && any(!is.na(export_cells$sample))) { + plot_frac_by_group(export_cells, "sample", + "Fraction of cells with consensus cluster by sample", + glue("consensus_fraction_by_sample.{params$figure_format}")) +} +if (isTRUE(params$show_fraction_by_condition) && any(!is.na(export_cells$condition))) { + plot_frac_by_group(export_cells, "condition", + "Fraction of cells with consensus cluster by condition", + glue("consensus_fraction_by_condition.{params$figure_format}")) +} +if (isTRUE(params$show_fraction_by_patient) && any(!is.na(export_cells$patient))) { + plot_frac_by_group(export_cells, "patient", + "Fraction of cells with consensus cluster by patient", + glue("consensus_fraction_by_patient.{params$figure_format}")) +} +``` + +# Summary tables display +```{r} +#| label: summary-tables-display +if (isTRUE(params$show_summary_tables)) { + kable(consensus_cluster_summary %>% slice_head(n = params$top_n_clusters), + caption = "Top consensus clonotype clusters by cell count.") %>% + kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed")) + + kable(annotation_consensus_summary %>% slice_head(n = params$top_n_states_heatmap), + caption = "Top annotations ranked by consensus-clustered fraction.") %>% + kable_styling(full_width = TRUE, bootstrap_options = c("striped", "hover", "condensed", "responsive")) + + kable(method_presence_summary, + caption = "Method-level cell coverage summary.") %>% + kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed")) +} +``` + +# Warnings +```{r} +#| label: warnings +warn_tbl <- tibble( + warning = c( + "Only one method available", + "Few cells with consensus cluster", + "No condition metadata", + "No patient metadata", + "Consensus requires >=2 methods", + "Singleton consensus disabled" + ), + triggered = c( + sum(c(any(export_cells$has_gliph, na.rm = TRUE), + any(export_cells$has_tcrdist, na.rm = TRUE), + any(export_cells$has_giana, na.rm = TRUE))) < 2, + mean(!is.na(export_cells$consensus_cluster)) < 0.3, + all(is.na(export_cells$condition)), + all(is.na(export_cells$patient)), + params$consensus_min_methods > 1, + !isTRUE(params$assign_singleton_consensus) + ), + interpretation = c( + "Fewer than two clustering methods are available, so this module is acting as a method summary rather than a true consensus comparison.", + "Less than 30% of cells received a consensus cluster annotation under the current rules.", + "Condition-level summaries were skipped because condition metadata were unavailable.", + "Patient-level summaries were skipped because patient metadata were unavailable.", + "Consensus assignment requires support from at least the specified number of methods.", + "Cells supported by only one method are excluded from consensus assignment under current settings." + ) +) %>% + filter(triggered) + +if (nrow(warn_tbl) == 0) { + cat("No major automatic warnings were triggered under the current consensus clustering settings.") +} else { + kable(warn_tbl %>% select(-triggered), caption = "Automatically generated consensus clustering warnings and notes.") %>% + kable_styling(full_width = TRUE, bootstrap_options = c("striped", "hover", "condensed")) +} +``` + +# session info +```{r} +#| label: session-info +writeLines(capture.output(sessionInfo()), file.path(params$outdir, "sessionInfo.consensus_clustering.txt")) +sessionInfo() +``` diff --git a/modules/scratch/CONSENSUS_CLUSTERING/main.nf b/modules/scratch/CONSENSUS_CLUSTERING/main.nf new file mode 100644 index 0000000..0abdca6 --- /dev/null +++ b/modules/scratch/CONSENSUS_CLUSTERING/main.nf @@ -0,0 +1,55 @@ +process CONSENSUS_CLUSTERING { + tag "${project_name}" + label 'process_medium' + container "${params.container}" + + publishDir "${params.outdir}/Consensus_Clustering", mode: 'copy', overwrite: true + + input: + path seurat_rds + path export_cells + path gliph_export_cells + path tcrdist_export_cells + path giana_export_cells + path qmd + val project_name + + output: + path "Clonotype_Clustering_Consensus_Report.html", emit: report_html + path "Clonotype_Clustering_Consensus_Report/data/seurat_with_consensus_clonotype_clusters.rds", emit: seurat_with_consensus + path "Clonotype_Clustering_Consensus_Report/tables/consensus_export_cells.tsv", emit: export_cells + path "Clonotype_Clustering_Consensus_Report/tables/*", emit: tables + path "Clonotype_Clustering_Consensus_Report/figures/*", emit: figures + + script: + """ + mkdir -p Clonotype_Clustering_Consensus_Report + + quarto render ${qmd} \\ + -P seurat_rds=${seurat_rds} \\ + -P export_cells_file=${export_cells} \\ + -P gliph_export_cells_file=${gliph_export_cells} \\ + -P tcrdist_export_cells_file=${tcrdist_export_cells} \\ + -P giana_export_cells_file=${giana_export_cells} \\ + -P outdir=Clonotype_Clustering_Consensus_Report \\ + -P label_col="${params.label_col}" \\ + -P sample_col="${params.sample_col}" \\ + -P patient_col="${params.patient_col}" \\ + -P condition_col="${params.condition_col}" \\ + -P timepoint_col="${params.timepoint_col}" \\ + -P batch_col="${params.batch_col}" \\ + -P reduction_use="${params.reduction_use}" \\ + -P make_umap_if_missing=${params.make_umap_if_missing} \\ + -P umap_dims_max=${params.umap_dims_max} \\ + -P umap_nfeatures=${params.umap_nfeatures} \\ + -P raster_large_umap=${params.raster_large_umap} \\ + -P consensus_min_methods=${params.consensus_min_methods} \\ + -P use_majority_vote=${params.consensus_use_majority_vote} \\ + -P assign_singleton_consensus=${params.consensus_assign_singleton} \\ + -P consensus_label_prefix="${params.consensus_label_prefix}" \\ + -P min_cells_per_group=${params.consensus_min_cells_per_group} \\ + -P min_cluster_size_plot=${params.consensus_min_cluster_size_plot} \\ + -P top_n_clusters=${params.consensus_top_n_clusters} \\ + -P report_label="${project_name} Consensus Clustering" + """ +} diff --git a/modules/scratch/MASTER_SUMMARY/Master_Summary_Report.qmd b/modules/scratch/MASTER_SUMMARY/Master_Summary_Report.qmd new file mode 100644 index 0000000..ef08f82 --- /dev/null +++ b/modules/scratch/MASTER_SUMMARY/Master_Summary_Report.qmd @@ -0,0 +1,613 @@ +--- +title: "SCRATCH-TCR: Master Summary Report" +author: "Syed Shujaat Ali Zaidi" +format: + html: + toc: true + toc-depth: 3 + number-sections: true + code-fold: true + code-summary: "Show code" + embed-resources: true + theme: defualt + df-print: paged +execute: + echo: false + warning: false + message: false +params: + project_name: "SCRATCH-TCR Project" + outdir: "Master_Summary_Report" + + vdj_qc_per_sample_compact_file: "" + vdj_qc_before_after_summary_file: "" + vdj_qc_sample_sheet_resolved_file: "" + vdj_qc_clone_rank_abundance_file: "" + + vdj_qc_before_after_retention_fig: "" + vdj_qc_pairing_bar_fig: "" + vdj_qc_clone_rank_abundance_fig: "" + vdj_qc_multiple_chains_fig: "" + + # core inputs + seurat_rds: "data/seurat_with_consensus_clonotype_clusters.rds" + export_cells_file: "data/consensus_export_cells.tsv" + + # optional module rollups + vdj_qc_summary_file: "data/vdj_qc_per_sample_compact.tsv" + tcell_integration_summary_file: "data/tcell_summary_rollup.tsv" + tcri_summary_file: "data/tcri_summary_rollup.tsv" + conga_summary_file: "data/conga_summary_rollup.tsv" + gliph2_summary_file: "data/gliph2_summary_rollup.tsv" + tcrdist3_summary_file: "data/tcrdist3_summary_rollup.tsv" + giana_summary_file: "data/giana_summary_rollup.tsv" + consensus_summary_file: "data/consensus_summary_rollup.tsv" + repertoire_summary_file: "data/repertoire_summary_rollup.tsv" + + # optional richer inputs + diversity_by_sample_file: "data/diversity_by_sample.tsv" + clone_burden_file: "data/clone_burden_cells_by_sample.tsv" + sample_overlap_matrix_file: "data/sample_overlap_matrix.tsv" + method_presence_file: "data/method_presence_summary.tsv" + method_cluster_counts_file: "data/method_cluster_counts.tsv" + consensus_cluster_summary_file: "data/consensus_cluster_summary.tsv" + annotation_tcri_summary_file: "data/annotation_tcri_summary.tsv" + annotation_conga_summary_file: "data/annotation_conga_summary.tsv" + annotation_gliph2_summary_file: "data/annotation_gliph2_summary.tsv" + annotation_tcrdist3_summary_file: "data/annotation_tcrdist3_summary.tsv" + annotation_giana_summary_file: "data/annotation_giana_summary.tsv" + annotation_consensus_summary_file: "data/annotation_consensus_summary.tsv" + + # mapping + label_col: "" + sample_col: "sample" + patient_col: "patient" + condition_col: "condition" + timepoint_col: "timepoint" + clone_id_col: "clone_id" + clone_size_col: "clone_size" + consensus_cluster_col: "consensus_cluster" + + label_candidates: "annot;Annotation;celltype;CellType;predicted.celltype.l2;celltypist;azimuth_labels;seurat_clusters" + sample_candidates: "sample;META_SAMPLE;orig.ident;sample_id" + patient_candidates: "patient;META_PATIENT;patient_id;subject;donor" + condition_candidates: "condition;META_TIMECOND;group;status" + timepoint_candidates: "timepoint;visit;day;META_TIMECOND" + clone_id_candidates: "clone_id;CTaa;clonotype;clone" + clone_size_candidates: "clone_size;CloneSize;clone_n" + consensus_cluster_candidates: "consensus_cluster;consensus;consensus_id" + + + figure_format: "png" + figure_width: 10 + figure_height: 7 + figure_dpi: 300 + base_size: 12 + + top_n_annotations: 20 + top_n_consensus_clusters: 20 +--- + +# setup +```{r} +#| label: setup-chunk +suppressPackageStartupMessages({ + library(Seurat) + library(SeuratObject) + library(data.table) + library(dplyr) + library(tidyr) + library(ggplot2) + library(forcats) + library(scales) + library(knitr) + library(kableExtra) + library(ComplexHeatmap) + library(circlize) + library(patchwork) +}) + +dir.create(params$outdir, recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, "tables"), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, "figures"), recursive = TRUE, showWarnings = FALSE) + +`%||%` <- function(a, b) if (!is.null(a) && length(a) > 0 && !all(is.na(a))) a else b + +sanitize_param_string <- function(x) { + if (is.null(x) || length(x) == 0) return(NULL) + trimws(gsub("\\u00A0", " ", as.character(x))) +} + +normalize_colnames <- function(df) { + colnames(df) <- trimws(gsub("\\u00A0", " ", colnames(df))) + df +} + +split_candidate_string <- function(x) { + if (is.null(x) || length(x) == 0 || is.na(x) || x == "") return(character()) + x <- gsub('"', "", as.character(x)) + x <- trimws(unlist(strsplit(x, ";", fixed = TRUE))) + x[nzchar(x)] +} + +safe_read_table <- function(path) { + # Treat empty placeholder files (staged NO_FILE, 0 bytes) as absent. + if (is.null(path) || is.na(path) || path == "" || !file.exists(path)) return(NULL) + if (isTRUE(file.size(path) == 0)) return(NULL) + ext <- tolower(tools::file_ext(path)) + if (ext %in% c("tsv", "tab", "txt")) fread(path, sep = "\t") else fread(path) +} + +first_existing_col <- function(df, preferred, candidates = character()) { + df <- normalize_colnames(df) + preferred <- sanitize_param_string(preferred) + + if (length(candidates) == 1 && is.character(candidates)) { + candidates <- split_candidate_string(candidates) + } + candidates <- trimws(gsub("\\u00A0", " ", as.character(candidates))) + + if (!is.null(preferred) && preferred != "" && preferred %in% colnames(df)) return(preferred) + + hits <- intersect(candidates, colnames(df)) + if (length(hits) > 0) hits[[1]] else NULL +} + +save_plot_safe <- function(plot_obj, filename, + width = params$figure_width, + height = params$figure_height, + dpi = params$figure_dpi) { + ggsave( + filename = file.path(params$outdir, "figures", filename), + plot = plot_obj, + width = width, + height = height, + dpi = dpi, + bg = "white", + limitsize = FALSE + ) +} + +save_table_safe <- function(df, filename) { + fwrite(df, file.path(params$outdir, "tables", filename), sep = "\t") +} + +theme_scratch_pub <- function(base_size = 12) { + theme_bw(base_size = base_size) + + theme( + plot.title = element_text(face = "bold", size = base_size + 2, hjust = 0), + plot.subtitle = element_text(size = base_size, hjust = 0), + axis.title = element_text(face = "bold"), + axis.text = element_text(color = "black"), + panel.grid.minor = element_blank(), + panel.grid.major = element_line(linewidth = 0.2, color = "grey90"), + strip.background = element_rect(fill = "grey95", color = "grey80"), + strip.text = element_text(face = "bold"), + legend.title = element_text(face = "bold") + ) +} + +``` + +# input loading +```{r} +seu <- if (file.exists(params$seurat_rds) && isTRUE(file.size(params$seurat_rds) > 0)) readRDS(params$seurat_rds) else NULL +export_cells <- safe_read_table(params$export_cells_file) + +vdj_qc_per_sample_compact <- safe_read_table(params$vdj_qc_per_sample_compact_file) +vdj_qc_before_after_summary <- safe_read_table(params$vdj_qc_before_after_summary_file) +vdj_qc_sample_sheet_resolved <- safe_read_table(params$vdj_qc_sample_sheet_resolved_file) +vdj_qc_clone_rank_abundance <- safe_read_table(params$vdj_qc_clone_rank_abundance_file) +vdj_qc_summary <- safe_read_table(params$vdj_qc_summary_file) +tcell_integration_summary <- safe_read_table(params$tcell_integration_summary_file) +tcri_summary <- safe_read_table(params$tcri_summary_file) +conga_summary <- safe_read_table(params$conga_summary_file) +gliph2_summary <- safe_read_table(params$gliph2_summary_file) +tcrdist3_summary <- safe_read_table(params$tcrdist3_summary_file) +giana_summary <- safe_read_table(params$giana_summary_file) +consensus_summary <- safe_read_table(params$consensus_summary_file) +repertoire_summary <- safe_read_table(params$repertoire_summary_file) + +diversity_by_sample <- safe_read_table(params$diversity_by_sample_file) +clone_burden <- safe_read_table(params$clone_burden_file) +sample_overlap_matrix <- safe_read_table(params$sample_overlap_matrix_file) +method_presence <- safe_read_table(params$method_presence_file) +method_cluster_counts <- safe_read_table(params$method_cluster_counts_file) +consensus_cluster_summary <- safe_read_table(params$consensus_cluster_summary_file) + +annotation_tcri_summary <- safe_read_table(params$annotation_tcri_summary_file) +annotation_conga_summary <- safe_read_table(params$annotation_conga_summary_file) +annotation_gliph2_summary <- safe_read_table(params$annotation_gliph2_summary_file) +annotation_tcrdist3_summary <- safe_read_table(params$annotation_tcrdist3_summary_file) +annotation_giana_summary <- safe_read_table(params$annotation_giana_summary_file) +annotation_consensus_summary <- safe_read_table(params$annotation_consensus_summary_file) +``` + +# Resolving Columns +```{r} +if (is.null(export_cells) && !is.null(seu)) { + export_cells <- seu@meta.data %>% tibble::rownames_to_column("cell_id") +} + +label_col <- if (!is.null(export_cells)) first_existing_col(export_cells, params$label_col, params$label_candidates) else NULL +sample_col <- if (!is.null(export_cells)) first_existing_col(export_cells, params$sample_col, params$sample_candidates) else NULL +patient_col <- if (!is.null(export_cells)) first_existing_col(export_cells, params$patient_col, params$patient_candidates) else NULL +condition_col <- if (!is.null(export_cells)) first_existing_col(export_cells, params$condition_col, params$condition_candidates) else NULL +timepoint_col <- if (!is.null(export_cells)) first_existing_col(export_cells, params$timepoint_col, params$timepoint_candidates) else NULL +clone_id_col <- if (!is.null(export_cells)) first_existing_col(export_cells, params$clone_id_col, params$clone_id_candidates) else NULL +clone_size_col <- if (!is.null(export_cells)) first_existing_col(export_cells, params$clone_size_col, params$clone_size_candidates) else NULL +consensus_cluster_col <- if (!is.null(export_cells)) first_existing_col(export_cells, params$consensus_cluster_col, params$consensus_cluster_candidates) else NULL +``` + +# Overview +```{r} +#| label: overview +#| results: asis +overview_tbl <- tibble::tibble( + Metric = c( + "Project name", + "Cells in export table", + "Samples", + "Patients", + "Conditions", + "Timepoints", + "Unique clonotypes", + "Unique consensus clusters" + ), + Value = c( + params$project_name, + if (!is.null(export_cells)) nrow(export_cells) else NA, + if (!is.null(export_cells) && !is.null(sample_col)) length(unique(na.omit(export_cells[[sample_col]]))) else NA, + if (!is.null(export_cells) && !is.null(patient_col)) length(unique(na.omit(export_cells[[patient_col]]))) else NA, + if (!is.null(export_cells) && !is.null(condition_col)) length(unique(na.omit(export_cells[[condition_col]]))) else NA, + if (!is.null(export_cells) && !is.null(timepoint_col)) length(unique(na.omit(export_cells[[timepoint_col]]))) else NA, + if (!is.null(export_cells) && !is.null(clone_id_col)) length(unique(na.omit(export_cells[[clone_id_col]]))) else NA, + if (!is.null(export_cells) && !is.null(consensus_cluster_col)) length(unique(na.omit(export_cells[[consensus_cluster_col]]))) else NA + ) +) + +save_table_safe(overview_tbl, "master_overview.tsv") + +print( + kableExtra::kbl(overview_tbl, caption = "Master project overview.") %>% + kableExtra::kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover","condensed")) +) + +``` + +# Module Rollups +```{r} +#| label: module-rollups +#| results: asis +rollup_list <- list( + VDJ_QC = vdj_qc_summary, + TCell_Integration = tcell_integration_summary, + TCRi = tcri_summary, + CoNGA = conga_summary, + GLIPH2 = gliph2_summary, + TCRdist3 = tcrdist3_summary, + GIANA = giana_summary, + Consensus = consensus_summary, + Repertoire = repertoire_summary +) + +rollup_presence <- tibble::tibble( + module = names(rollup_list), + available = vapply(rollup_list, function(x) !is.null(x), logical(1)) +) + +save_table_safe(rollup_presence, "module_output_availability.tsv") + +print( + kableExtra::kbl(rollup_presence, caption = "Module output availability.") %>% + kableExtra::kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover","condensed")) +) +``` + +# VDJ QC +```{r} +#| label: vdj-qc-section +#| results: asis + +if (!is.null(vdj_qc_per_sample_compact) && nrow(vdj_qc_per_sample_compact) > 0) { + vdj_qc_per_sample_compact_master <- vdj_qc_per_sample_compact %>% + dplyr::select( + dplyr::any_of(c( + "sample", + "patient_id", + "timepoint", + "n_cells_total", + "n_cells_paired", + "pct_cells_paired", + "n_unique_clones" + )) + ) + + save_table_safe(vdj_qc_per_sample_compact, "master_vdj_qc_per_sample_compact.tsv") + + print( + kableExtra::kbl( + vdj_qc_per_sample_compact_master, + caption = "VDJ QC per-sample compact summary." + ) %>% + kableExtra::kable_styling( + full_width = FALSE, + bootstrap_options = c("striped","hover","condensed"), + font_size = 11 + ) %>% + kableExtra::scroll_box(width = "100%", height = "420px") + ) +} + +if (!is.null(vdj_qc_before_after_summary) && nrow(vdj_qc_before_after_summary) > 0) { + save_table_safe(vdj_qc_before_after_summary, "master_vdj_qc_before_after_summary.tsv") + + print( + kableExtra::kbl( + vdj_qc_before_after_summary, + caption = "VDJ QC contigs before/after filtering summary." + ) %>% + kableExtra::kable_styling( + full_width = FALSE, + bootstrap_options = c("striped","hover","condensed"), + font_size = 11 + ) %>% + kableExtra::scroll_box(width = "100%", height = "320px") + ) +} + +if (!is.null(vdj_qc_sample_sheet_resolved) && nrow(vdj_qc_sample_sheet_resolved) > 0) { + save_table_safe(vdj_qc_sample_sheet_resolved, "master_vdj_qc_sample_sheet_resolved.tsv") +} + +if (!is.null(vdj_qc_clone_rank_abundance) && nrow(vdj_qc_clone_rank_abundance) > 0) { + top_clone_rank <- vdj_qc_clone_rank_abundance %>% + dplyr::group_by(sample) %>% + dplyr::slice_head(n = 10) %>% + dplyr::ungroup() + + save_table_safe(top_clone_rank, "master_vdj_qc_clone_rank_abundance_top.tsv") + + print( + kableExtra::kbl( + top_clone_rank, + caption = "Top ranked clonotypes by sample." + ) %>% + kableExtra::kable_styling( + full_width = FALSE, + bootstrap_options = c("striped","hover","condensed"), + font_size = 11 + ) %>% + kableExtra::scroll_box(width = "100%", height = "320px") + ) +} + +fig_map <- list( + "QC retention before/after filtering" = params$vdj_qc_before_after_retention_fig, + "Pairing status by sample" = params$vdj_qc_pairing_bar_fig, + "Clone rank abundance" = params$vdj_qc_clone_rank_abundance_fig, + "Multiple chains by sample" = params$vdj_qc_multiple_chains_fig +) + +valid_figs <- fig_map[ + vapply(fig_map, function(x) !is.na(x) && nzchar(x) && file.exists(x) && isTRUE(file.size(x) > 0), logical(1)) +] + +if (length(valid_figs) > 0) { + cat("### Selected VDJ QC figures\n\n") + + for (nm in names(valid_figs)) { + fp <- valid_figs[[nm]] + out_name <- basename(fp) + file.copy(fp, file.path(params$outdir, "figures", out_name), overwrite = TRUE) + + cat("#### ", nm, "\n\n", sep = "") + print(knitr::include_graphics(fp)) + cat("\n\n") + } +} +``` + + +# Diversity +```{r} +if (!is.null(diversity_by_sample) && nrow(diversity_by_sample) > 0) { + div_long <- diversity_by_sample %>% + pivot_longer(cols = intersect(c("shannon","simpson","inv_simpson","richness"), colnames(diversity_by_sample)), + names_to = "metric", values_to = "value") + + p_div <- ggplot(div_long, aes(x = metric, y = value, fill = metric)) + + geom_boxplot(alpha = 0.85, outlier.size = 0.4) + + geom_jitter(width = 0.12, size = 1.2, alpha = 0.7) + + guides(fill = "none") + + labs( + title = "Repertoire diversity summary", + x = NULL, + y = "Metric value" + ) + + theme_scratch_pub(params$base_size) + + p_div + save_plot_safe(p_div, "master_diversity_boxplots.png") +} +``` + +# Clone Birden +```{r} +if (!is.null(clone_burden) && nrow(clone_burden) > 0) { + sample_col_burden <- if ("sample" %in% colnames(clone_burden)) "sample" else colnames(clone_burden)[1] + frac_col <- if ("frac_cells" %in% colnames(clone_burden)) "frac_cells" else if ("frac_clones" %in% colnames(clone_burden)) "frac_clones" else NULL + bin_col <- if ("clone_bin" %in% colnames(clone_burden)) "clone_bin" else NULL + + if (!is.null(frac_col) && !is.null(bin_col)) { + p_burden <- ggplot(clone_burden, aes_string(x = sample_col_burden, y = frac_col, fill = bin_col)) + + geom_col(position = "fill") + + coord_flip() + + scale_y_continuous(labels = percent_format(accuracy = 1)) + + labs( + title = "Clonal burden across samples", + x = NULL, + y = "Fraction", + fill = "Clone bin" + ) + + theme_scratch_pub(params$base_size) + + p_burden + save_plot_safe(p_burden, "master_clone_burden.png") + } +} +``` + +# Method coverage and cluster counts +```{r} +plot_list <- list() + +if (!is.null(method_presence) && nrow(method_presence) > 0) { + mp <- method_presence %>% + mutate(method = fct_reorder(method, frac_cells)) + p1 <- ggplot(mp, aes(x = method, y = frac_cells)) + + geom_col(fill = "steelblue") + + coord_flip() + + scale_y_continuous(labels = percent_format(accuracy = 1)) + + labs(title = "Method coverage", x = NULL, y = "Fraction of cells annotated") + + theme_scratch_pub(params$base_size) + plot_list$coverage <- p1 +} + +if (!is.null(method_cluster_counts) && nrow(method_cluster_counts) > 0) { + mc <- method_cluster_counts %>% + mutate(method = fct_reorder(method, n_clusters)) + p2 <- ggplot(mc, aes(x = method, y = n_clusters)) + + geom_col(fill = "darkorange") + + coord_flip() + + scale_y_continuous(labels = comma) + + labs(title = "Clusters detected by method", x = NULL, y = "Number of clusters") + + theme_scratch_pub(params$base_size) + plot_list$counts <- p2 +} + +if (length(plot_list) == 2) { + wrap_plots(plot_list$coverage, plot_list$counts, ncol = 2) + save_plot_safe(wrap_plots(plot_list$coverage, plot_list$counts, ncol = 2), "master_method_panels.png", width = 14, height = 6) +} else if (length(plot_list) == 1) { + print(plot_list[[1]]) +} +``` + +# Sample Overlap +```{r} +if (!is.null(sample_overlap_matrix) && nrow(sample_overlap_matrix) > 0) { + som <- as.data.frame(sample_overlap_matrix) + rownames(som) <- som[[1]] + som[[1]] <- NULL + som <- as.matrix(som) + + ht <- Heatmap( + som, + name = "Overlap", + col = colorRamp2(c(0, 0.5, 1), c("white", "gold", "firebrick")), + cluster_rows = TRUE, + cluster_columns = TRUE, + row_names_side = "left", + column_title = "Sample repertoire overlap", + heatmap_legend_param = list(title = "Overlap") + ) + draw(ht) +} +``` + +# Annotation-centered comparison +```{r} +annotation_panels <- list( + TCRi = annotation_tcri_summary, + CoNGA = annotation_conga_summary, + GLIPH2 = annotation_gliph2_summary, + TCRdist3 = annotation_tcrdist3_summary, + GIANA = annotation_giana_summary, + Consensus = annotation_consensus_summary +) + +anno_comp <- lapply(names(annotation_panels), function(nm) { + df <- annotation_panels[[nm]] + if (is.null(df) || nrow(df) == 0) return(NULL) + + annot_col <- if ("annot" %in% colnames(df)) "annot" else colnames(df)[1] + frac_col <- intersect(c("frac_high","frac_clustered","frac_gliph","frac_tcrdist","frac_giana","frac_consensus"), colnames(df)) + if (length(frac_col) == 0) return(NULL) + + df %>% + transmute( + method = nm, + annot = .data[[annot_col]], + frac = .data[[frac_col[1]]] + ) +}) + +anno_comp <- dplyr::bind_rows(anno_comp) + +if (nrow(anno_comp) > 0) { + anno_comp2 <- anno_comp %>% + group_by(method) %>% + slice_max(order_by = frac, n = params$top_n_annotations, with_ties = FALSE) %>% + ungroup() + + p_anno <- ggplot(anno_comp2, aes(x = reorder(annot, frac), y = frac, fill = method)) + + geom_col() + + coord_flip() + + facet_wrap(~method, scales = "free_y") + + scale_y_continuous(labels = percent_format(accuracy = 1)) + + labs( + title = "Top annotation enrichments across modules", + x = NULL, + y = "Fraction / enrichment summary" + ) + + theme_scratch_pub(params$base_size) + + p_anno + save_plot_safe(p_anno, "master_annotation_comparison.png", width = 14, height = 10) +} +``` + +# Concensus Cluster Burden +```{r} +if (!is.null(consensus_cluster_summary) && nrow(consensus_cluster_summary) > 0) { + ccs <- consensus_cluster_summary %>% + slice_head(n = params$top_n_consensus_clusters) %>% + mutate(consensus_cluster = fct_reorder(consensus_cluster, n_cells)) + + p_cons <- ggplot(ccs, aes(x = consensus_cluster, y = n_cells)) + + geom_col(fill = "steelblue") + + coord_flip() + + scale_y_continuous(labels = comma) + + labs( + title = "Top consensus clonotype clusters", + x = NULL, + y = "Cells" + ) + + theme_scratch_pub(params$base_size) + + p_cons + save_plot_safe(p_cons, "master_consensus_clusters.png") +} +``` + +# key tables +```{r} +if (!is.null(consensus_cluster_summary) && nrow(consensus_cluster_summary) > 0) { + kable(consensus_cluster_summary %>% slice_head(n = params$top_n_consensus_clusters), + caption = "Top consensus clusters.") %>% + kable_styling(full_width = TRUE, bootstrap_options = c("striped","hover","condensed","responsive")) +} + +if (!is.null(diversity_by_sample) && nrow(diversity_by_sample) > 0) { + kable(diversity_by_sample, + caption = "Diversity metrics by sample.") %>% + kable_styling(full_width = TRUE, bootstrap_options = c("striped","hover","condensed","responsive")) +} +``` + +# session info +```{r} +writeLines(capture.output(sessionInfo()), file.path(params$outdir, "sessionInfo.master_summary.txt")) +sessionInfo() +``` diff --git a/modules/scratch/MASTER_SUMMARY/main.nf b/modules/scratch/MASTER_SUMMARY/main.nf new file mode 100644 index 0000000..282ad04 --- /dev/null +++ b/modules/scratch/MASTER_SUMMARY/main.nf @@ -0,0 +1,61 @@ +process MASTER_SUMMARY { + tag "${project_name}" + label 'process_medium' + container "${params.container}" + + publishDir "${params.outdir}/Master_Summary", mode: 'copy', overwrite: true + + input: + // stageAs unique names: several of these are optional and default to the shared NO_FILE + // placeholder in VDJ-only mode. Without distinct staged names, Nextflow errors on + // "input file name collision" when two inputs resolve to the same NO_FILE. The .qmd + // treats any 0-byte staged file as absent. + path seurat_rds, stageAs: 'in_seurat_rds' + path export_cells, stageAs: 'in_export_cells' + + path vdj_qc_per_sample_compact, stageAs: 'in_vdj_qc_per_sample_compact' + path vdj_qc_before_after_summary, stageAs: 'in_vdj_qc_before_after_summary' + path vdj_qc_sample_sheet_resolved, stageAs: 'in_vdj_qc_sample_sheet_resolved' + path vdj_qc_clone_rank_abundance, stageAs: 'in_vdj_qc_clone_rank_abundance' + + path vdj_qc_before_after_retention_fig, stageAs: 'in_vdj_qc_before_after_retention_fig' + path vdj_qc_pairing_bar_fig, stageAs: 'in_vdj_qc_pairing_bar_fig' + path vdj_qc_clone_rank_abundance_fig, stageAs: 'in_vdj_qc_clone_rank_abundance_fig' + path vdj_qc_multiple_chains_fig, stageAs: 'in_vdj_qc_multiple_chains_fig' + + path qmd + val barrier_done + val project_name + + output: + path "Master_Summary_Report.html", emit: report_html + path "Master_Summary_Report/tables/*", emit: tables, optional: true + path "Master_Summary_Report/figures/*", emit: figures, optional: true + + script: + """ + mkdir -p Master_Summary_Report + mkdir -p Master_Summary_Report/data + mkdir -p Master_Summary_Report/tables + mkdir -p Master_Summary_Report/figures + + quarto render ${qmd} \\ + -P project_name="${project_name}" \\ + -P seurat_rds="${seurat_rds}" \\ + -P export_cells_file="${export_cells}" \\ + -P vdj_qc_per_sample_compact_file="${vdj_qc_per_sample_compact}" \\ + -P vdj_qc_before_after_summary_file="${vdj_qc_before_after_summary}" \\ + -P vdj_qc_sample_sheet_resolved_file="${vdj_qc_sample_sheet_resolved}" \\ + -P vdj_qc_clone_rank_abundance_file="${vdj_qc_clone_rank_abundance}" \\ + -P vdj_qc_before_after_retention_fig="${vdj_qc_before_after_retention_fig}" \\ + -P vdj_qc_pairing_bar_fig="${vdj_qc_pairing_bar_fig}" \\ + -P vdj_qc_clone_rank_abundance_fig="${vdj_qc_clone_rank_abundance_fig}" \\ + -P vdj_qc_multiple_chains_fig="${vdj_qc_multiple_chains_fig}" \\ + -P label_col="${params.label_col}" \\ + -P sample_col="${params.sample_col}" \\ + -P patient_col="${params.patient_col}" \\ + -P condition_col="${params.condition_col}" \\ + -P timepoint_col="${params.timepoint_col}" \\ + -P outdir="Master_Summary_Report" + """ +} \ No newline at end of file diff --git a/modules/scratch/REPERTOIRE/Repertoire_Report.qmd b/modules/scratch/REPERTOIRE/Repertoire_Report.qmd new file mode 100644 index 0000000..59d8157 --- /dev/null +++ b/modules/scratch/REPERTOIRE/Repertoire_Report.qmd @@ -0,0 +1,1338 @@ +--- +title: "SCRATCH-TCR: Repertoire Analysis Report" +author: "Syed Shujaat Ali Zaidi" +format: + html: + toc: true + toc-depth: 3 + number-sections: true + code-fold: true + code-summary: "Show code" + embed-resources: true + theme: cosmo + df-print: paged + css: repertoire_report.css +execute: + echo: false + warning: false + message: false + + +params: + # ====================================================== + # Inputs + # ====================================================== + seurat_rds: "data/seurat_with_consensus_clonotype_clusters.rds" + export_cells_file: "data/consensus_export_cells.tsv" + outdir: "Repertoire_Report" + + # Optional inputs + metadata_file: "" + previous_summary_file: "" + + # ====================================================== + # Output controls + # ====================================================== + data_dir: "data" + tables_dir: "tables" + figures_dir: "figures" + save_tables: true + save_figures: true + save_updated_seurat: false + figure_format: "png" + figure_width: 10 + figure_height: 7 + figure_dpi: 300 + base_size: 12 + + # ====================================================== + # Metadata mapping + # ====================================================== + label_col: "" + sample_col: "META_SAMPLE" + patient_col: "META_PATIENT" + condition_col: "META_TIMECOND" + timepoint_col: "META_TIMEPOINT" + batch_col: "META_BATCH" + clone_id_col: "clone_id" + clone_size_col: "clone_size" + paired_tcr_col: "paired_tcr" + has_tcr_col: "has_tcr" + consensus_cluster_col: "consensus_cluster" + + label_candidates: "annot;Annotation;celltype;CellType;predicted_labels;predicted.celltype.l2;predicted.celltype.l1;celltypist;celltypist_label;azimuth_labels;seurat_clusters" + sample_candidates: "META_SAMPLE;sample;orig.ident;sample_id;Sample;SampleID" + patient_candidates: "META_PATIENT;patient_id;patient;Patient;subject;donor;case_id" + condition_candidates: "META_TIMECOND;condition;Condition;group;Group;status" + timepoint_candidates: "META_TIMEPOINT;timepoint;Timepoint;visit;Visit;day;Day;META_TIMECOND" + batch_candidates: "META_BATCH;batch;Batch;library;Library;run;Run;lane;Lane" + clone_id_candidates: "clone_id;CTaa;clonotype;clone" + clone_size_candidates: "clone_size;CloneSize;clone_n" + paired_tcr_candidates: "paired_tcr;paired;is_paired" + has_tcr_candidates: "has_tcr;hasTCR;tcr_positive" + consensus_cluster_candidates: "consensus_cluster;consensus;consensus_id" + + # ====================================================== + # Repertoire / binning controls + # ====================================================== + min_cells_per_clone_plot: 2 + top_n_shared_clones: 50 + top_n_flux_clones: 30 + shareability_min_n_groups: 2 + min_cells_per_group: 10 + top_n_clusters: 20 + + + # Clone burden / homeostasis bins + clone_bin_breaks: "0;1e-4;0.001;0.01;0.1;1.0" + clone_bin_labels: "Rare;Small;Medium;Large;Hyperexpanded" + use_relative_clone_frequencies: true + + # Similarity / overlap + overlap_metric: "jaccard" + diversity_metrics: "shannon;simpson;inv_simpson;richness" + longitudinal_require_ordered_time: false + + # ====================================================== + # Figure/report toggles + # ====================================================== + show_diversity_boxplots: true + show_clone_burden_barplot: true + show_rank_abundance: true + show_shareability_barplot: true + show_shareability_heatmap: true + show_sample_overlap_heatmap: true + show_phenotypic_flux: true + show_longitudinal_clone_tracking: true + show_consensus_cluster_burden: true + show_summary_tables: true + + report_label: "Repertoire Analysis" +--- + + +############################################################################### +# Read the integrated Seurat object or exported cell-level table and standardize clonotype, metadata, and annotation fields. +# Compute repertoire summaries including clone frequencies, diversity metrics, clonal burden, and cross-sample shareability. +# Quantify sample-to-sample overlap and identify clonotypes spanning multiple biological groups or phenotypic states. +# Generate publication-quality figures for diversity, burden, shareability, overlap, phenotypic flux, and optional longitudinal tracking. +# Export repertoire summary tables and figures for the final master summary report. +############################################################################### +# setup +```{r} +#| label: setup +suppressPackageStartupMessages({ + library(Seurat) + library(SeuratObject) + library(data.table) + library(dplyr) + library(tidyr) + library(stringr) + library(ggplot2) + library(forcats) + library(scales) + library(glue) + library(knitr) + library(kableExtra) + library(ComplexHeatmap) + library(circlize) + library(ggalluvial) + library(patchwork) +}) + +options(stringsAsFactors = FALSE) + +dir.create(params$outdir, recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$data_dir), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$tables_dir), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$figures_dir), recursive = TRUE, showWarnings = FALSE) + +`%||%` <- function(a, b) if (!is.null(a) && length(a) > 0 && !all(is.na(a))) a else b + +theme_scratch_pub <- function(base_size = 12) { + theme_bw(base_size = base_size) + + theme( + plot.title = element_text(face = "bold", size = base_size + 2, hjust = 0), + plot.subtitle = element_text(size = base_size, hjust = 0), + axis.title = element_text(face = "bold"), + axis.text = element_text(color = "black"), + panel.grid.minor = element_blank(), + panel.grid.major = element_line(linewidth = 0.2, color = "grey90"), + strip.background = element_rect(fill = "grey95", color = "grey80"), + strip.text = element_text(face = "bold"), + legend.title = element_text(face = "bold"), + legend.key = element_blank(), + plot.caption = element_text(size = base_size - 2, color = "grey40") + ) +} + +save_plot_safe <- function(plot_obj, filename, + width = params$figure_width, + height = params$figure_height, + dpi = params$figure_dpi) { + if (!isTRUE(params$save_figures)) return(invisible(NULL)) + ggplot2::ggsave( + filename = file.path(params$outdir, params$figures_dir, filename), + plot = plot_obj, + width = width, + height = height, + dpi = dpi, + bg = "white", + limitsize = FALSE + ) +} + +save_table_safe <- function(df, filename) { + if (!isTRUE(params$save_tables)) return(invisible(NULL)) + data.table::fwrite(df, file.path(params$outdir, params$tables_dir, filename), sep = "\t") +} + +safe_read_table <- function(path) { + if (is.null(path) || is.na(path) || path == "" || !file.exists(path)) return(NULL) + ext <- tolower(tools::file_ext(path)) + if (ext %in% c("tsv", "tab", "txt")) { + data.table::fread(path, sep = "\t") + } else { + data.table::fread(path) + } +} + +require_file <- function(path, msg = NULL) { + if (!file.exists(path)) stop(msg %||% paste("Missing required file:", path)) + path +} + +sanitize_param_string <- function(x) { + if (is.null(x) || length(x) == 0) return(NULL) + trimws(gsub("\\u00A0", " ", as.character(x))) +} + +normalize_colnames <- function(df) { + colnames(df) <- trimws(gsub("\\u00A0", " ", colnames(df))) + df +} + +split_candidate_string <- function(x) { + if (is.null(x) || length(x) == 0 || is.na(x) || x == "") return(character()) + x <- gsub('"', "", as.character(x)) + x <- trimws(unlist(strsplit(x, ";", fixed = TRUE))) + x[nzchar(x)] +} + +parse_numeric_param_vector <- function(x) { + vals <- split_candidate_string(x) + as.numeric(vals) +} + +parse_character_param_vector <- function(x) { + split_candidate_string(x) +} + +first_existing_col <- function(df, preferred, candidates = character()) { + df <- normalize_colnames(df) + preferred <- sanitize_param_string(preferred) + + if (length(candidates) == 1 && is.character(candidates)) { + candidates <- split_candidate_string(candidates) + } + candidates <- trimws(gsub("\\u00A0", " ", as.character(candidates))) + + if (!is.null(preferred) && preferred != "" && preferred %in% colnames(df)) return(preferred) + + hits <- intersect(candidates, colnames(df)) + if (length(hits) > 0) return(hits[[1]]) + NULL +} + +safe_n_unique <- function(x) length(unique(na.omit(x))) + +calc_shannon <- function(x) { + x <- x[x > 0] + p <- x / sum(x) + -sum(p * log(p)) +} + +calc_simpson <- function(x) { + x <- x[x > 0] + p <- x / sum(x) + 1 - sum(p^2) +} + +calc_inv_simpson <- function(x) { + x <- x[x > 0] + p <- x / sum(x) + 1 / sum(p^2) +} + +calc_richness <- function(x) { + sum(x > 0) +} + +calc_overlap_score <- function(a, b, metric = "jaccard") { + a <- unique(na.omit(a)) + b <- unique(na.omit(b)) + inter <- length(intersect(a, b)) + union_n <- length(union(a, b)) + if (metric == "jaccard") { + ifelse(union_n > 0, inter / union_n, NA_real_) + } else { + denom <- min(length(a), length(b)) + ifelse(denom > 0, inter / denom, NA_real_) + } +} + +save_rds_safe <- function(obj, filename) { + saveRDS(obj, file.path(params$outdir, params$data_dir, filename)) +} + +save_rdata_safe <- function(..., filename) { + save(..., file = file.path(params$outdir, params$data_dir, filename)) +} + +``` + +# loading Inputs +```{r} +#| label: read-inputs +# Seurat object is OPTIONAL: absent (or a NO_FILE placeholder) in the VDJ-only route, +# where a per-clonotype export table is supplied instead. All analysis below runs on +# export_cells_in, so the Seurat is only used as a fallback source of that table. +seu <- if (!is.null(params$seurat_rds) && file.exists(params$seurat_rds) && + !grepl("NO_FILE", params$seurat_rds)) readRDS(params$seurat_rds) else NULL + +export_cells_in <- safe_read_table(params$export_cells_file) +metadata_tbl <- safe_read_table(params$metadata_file) +previous_summary_tbl <- safe_read_table(params$previous_summary_file) + +if (is.null(export_cells_in)) { + if (is.null(seu)) { + stop("REPERTOIRE needs either a Seurat object (--input_annotated_object) or an export_cells table.") + } + export_cells_in <- seu@meta.data %>% tibble::rownames_to_column("cell_id") +} +``` + +# Resolving Columns +```{r} +#| label: resolve-columns +df0 <- normalize_colnames(as.data.frame(export_cells_in)) + +label_col <- first_existing_col(df0, params$label_col, params$label_candidates) +sample_col <- first_existing_col(df0, params$sample_col, params$sample_candidates) +patient_col <- first_existing_col(df0, params$patient_col, params$patient_candidates) +condition_col <- first_existing_col(df0, params$condition_col, params$condition_candidates) +timepoint_col <- first_existing_col(df0, params$timepoint_col, params$timepoint_candidates) +batch_col <- first_existing_col(df0, params$batch_col, params$batch_candidates) +clone_id_col <- first_existing_col(df0, params$clone_id_col, params$clone_id_candidates) +clone_size_col <- first_existing_col(df0, params$clone_size_col, params$clone_size_candidates) +paired_tcr_col <- first_existing_col(df0, params$paired_tcr_col, params$paired_tcr_candidates) +has_tcr_col <- first_existing_col(df0, params$has_tcr_col, params$has_tcr_candidates) +consensus_cluster_col <- first_existing_col(df0, params$consensus_cluster_col, params$consensus_cluster_candidates) + +if (is.null(label_col)) stop("Could not resolve an annotation label column.") +if (is.null(sample_col)) stop("Could not resolve a sample column.") +if (is.null(clone_id_col)) stop("Could not resolve a clone ID column.") +if (is.null(clone_size_col)) stop("Could not resolve a clone size column.") +if (is.null(has_tcr_col)) stop("Could not resolve a has_tcr column.") +if (is.null(paired_tcr_col)) stop("Could not resolve a paired_tcr column.") + +print(list( + resolved_label_col = label_col, + resolved_sample_col = sample_col, + resolved_patient_col = patient_col, + resolved_condition_col = condition_col, + resolved_timepoint_col = timepoint_col, + resolved_batch_col = batch_col, + resolved_clone_id_col = clone_id_col, + resolved_clone_size_col = clone_size_col, + resolved_paired_tcr_col = paired_tcr_col, + resolved_has_tcr_col = has_tcr_col, + resolved_consensus_cluster_col = consensus_cluster_col +)) + +``` + +# standardize-export +```{r} +#| label: standardize-export +export_cells <- as.data.frame(export_cells_in) %>% + mutate( + sample = as.character(.data[[sample_col]]), + patient = if (!is.null(patient_col)) as.character(.data[[patient_col]]) else NA_character_, + condition = if (!is.null(condition_col)) as.character(.data[[condition_col]]) else NA_character_, + timepoint = if (!is.null(timepoint_col)) as.character(.data[[timepoint_col]]) else NA_character_, + batch = if (!is.null(batch_col)) as.character(.data[[batch_col]]) else NA_character_, + annot = as.character(.data[[label_col]]), + clone_id = as.character(.data[[clone_id_col]]), + clone_size = suppressWarnings(as.numeric(.data[[clone_size_col]])), + has_tcr = as.logical(.data[[has_tcr_col]]), + paired_tcr = as.logical(.data[[paired_tcr_col]]), + consensus_cluster = if (!is.null(consensus_cluster_col)) as.character(.data[[consensus_cluster_col]]) else NA_character_ + ) %>% + mutate( + clone_id = ifelse(is.na(clone_id) | clone_id == "", NA_character_, clone_id), + clone_size = ifelse(is.na(clone_size), 0, clone_size) + ) +``` + +# Clone frequencies and burden bins +```{r} +#| label: clone-frequency-and-bins +#| results: asis +clone_bin_breaks <- parse_numeric_param_vector(params$clone_bin_breaks) +clone_bin_labels <- parse_character_param_vector(params$clone_bin_labels) + +if (length(clone_bin_breaks) < 2) { + stop("clone_bin_breaks must contain at least two numeric break values.") +} +if (length(clone_bin_labels) != (length(clone_bin_breaks) - 1)) { + stop("clone_bin_labels must have exactly length(clone_bin_breaks) - 1 entries.") +} + +clone_counts_by_sample <- export_cells %>% + filter(!is.na(clone_id), has_tcr) %>% + count(sample, clone_id, name = "n_cells") + +sample_totals <- export_cells %>% + filter(has_tcr) %>% + count(sample, name = "n_tcr_cells") + +clone_counts_by_sample <- clone_counts_by_sample %>% + left_join(sample_totals, by = "sample") %>% + mutate( + clone_freq = ifelse(n_tcr_cells > 0, n_cells / n_tcr_cells, NA_real_), + clone_bin = cut( + clone_freq, + breaks = clone_bin_breaks, + labels = clone_bin_labels, + include.lowest = TRUE, + right = TRUE + ) + ) + +save_table_safe(clone_counts_by_sample, "clone_counts_by_sample.tsv") + +print( + kableExtra::kbl( + clone_counts_by_sample %>% slice_head(n = 20), + caption = "Example clone frequencies and clone-size bins by sample." + ) %>% + kableExtra::kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed", "responsive") + ) +) + +``` + +# Diversity matrics +```{r} +#| label: diversity-metrics +#| results: asis +diversity_tbl <- clone_counts_by_sample %>% + group_by(sample) %>% + summarise( + n_clones = n(), + shannon = calc_shannon(n_cells), + simpson = calc_simpson(n_cells), + inv_simpson = calc_inv_simpson(n_cells), + richness = calc_richness(n_cells), + .groups = "drop" + ) + +if (any(!is.na(export_cells$patient))) { + diversity_by_patient <- clone_counts_by_sample %>% + left_join(export_cells %>% distinct(sample, patient), by = "sample") %>% + filter(!is.na(patient)) %>% + group_by(patient, clone_id) %>% + summarise(n_cells = sum(n_cells), .groups = "drop") %>% + group_by(patient) %>% + summarise( + n_clones = n(), + shannon = calc_shannon(n_cells), + simpson = calc_simpson(n_cells), + inv_simpson = calc_inv_simpson(n_cells), + richness = calc_richness(n_cells), + .groups = "drop" + ) + save_table_safe(diversity_by_patient, "diversity_by_patient.tsv") +} + +if (any(!is.na(export_cells$condition))) { + diversity_by_condition <- clone_counts_by_sample %>% + left_join(export_cells %>% distinct(sample, condition), by = "sample") %>% + filter(!is.na(condition)) %>% + group_by(condition, clone_id) %>% + summarise(n_cells = sum(n_cells), .groups = "drop") %>% + group_by(condition) %>% + summarise( + n_clones = n(), + shannon = calc_shannon(n_cells), + simpson = calc_simpson(n_cells), + inv_simpson = calc_inv_simpson(n_cells), + richness = calc_richness(n_cells), + .groups = "drop" + ) + save_table_safe(diversity_by_condition, "diversity_by_condition.tsv") +} + +save_table_safe(diversity_tbl, "diversity_by_sample.tsv") + +print( + kableExtra::kbl( + diversity_tbl, + caption = "Diversity metrics by sample." + ) %>% + kableExtra::kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed", "responsive") + ) +) + +``` + +# Expansion / contraction and clone burden +```{r} +#| label: clone-burden +#| results: asis +clone_burden_tbl <- clone_counts_by_sample %>% + count(sample, clone_bin, name = "n_clones") %>% + left_join( + clone_counts_by_sample %>% count(sample, name = "n_total_clones"), + by = "sample" + ) %>% + mutate(frac_clones = n_clones / n_total_clones) + +save_table_safe(clone_burden_tbl, "clone_burden_by_sample.tsv") + +clone_burden_cells_tbl <- clone_counts_by_sample %>% + group_by(sample, clone_bin) %>% + summarise( + n_cells = sum(n_cells), + .groups = "drop" + ) %>% + left_join( + clone_counts_by_sample %>% + group_by(sample) %>% + summarise(total_cells = sum(n_cells), .groups = "drop"), + by = "sample" + ) %>% + mutate(frac_cells = n_cells / total_cells) + +save_table_safe(clone_burden_cells_tbl, "clone_burden_cells_by_sample.tsv") + +print( + kableExtra::kbl( + clone_burden_cells_tbl, + caption = "Clone burden by sample and clone-size bin." + ) %>% + kableExtra::kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed", "responsive") + ) +) + +``` + +# Rank Abundance +```{r} +#| label: rank-abundance +#| results: asis +clone_rank_tbl <- clone_counts_by_sample %>% + filter(n_cells >= params$min_cells_per_clone_plot) %>% + group_by(sample) %>% + arrange(desc(n_cells), .by_group = TRUE) %>% + mutate(rank = row_number()) %>% + ungroup() + +save_table_safe(clone_rank_tbl, "clone_rank_abundance.tsv") + +print( + kableExtra::kbl( + clone_rank_tbl %>% slice_head(n = 30), + caption = "Top ranked clonotypes by sample." + ) %>% + kableExtra::kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed", "responsive") + ) +) + +``` + +# Shareability +```{r} +#| label: shareability +#| results: asis +shareability_tbl <- export_cells %>% + filter(!is.na(clone_id), has_tcr) %>% + distinct(sample, patient, condition, clone_id) %>% + group_by(clone_id) %>% + summarise( + n_samples = safe_n_unique(sample), + n_patients = safe_n_unique(patient), + n_conditions = safe_n_unique(condition), + .groups = "drop" + ) %>% + mutate( + shared_across_samples = n_samples >= params$shareability_min_n_groups, + shared_across_patients = n_patients >= params$shareability_min_n_groups, + shared_across_conditions = n_conditions >= params$shareability_min_n_groups + ) %>% + arrange(desc(n_samples), desc(n_patients), desc(n_conditions)) + +save_table_safe(shareability_tbl, "clone_shareability.tsv") + +top_shared_clones_tbl <- shareability_tbl %>% + slice_head(n = params$top_n_shared_clones) +save_table_safe(top_shared_clones_tbl, "top_shared_clones.tsv") + +print( + kableExtra::kbl( + top_shared_clones_tbl, + caption = "Top shared clonotypes." + ) %>% + kableExtra::kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed", "responsive") + ) +) + +``` + +# Sample Overlap +```{r} +#| label: sample-overlap +#| results: asis +samples_vec <- sort(unique(na.omit(export_cells$sample))) +sample_clone_sets <- lapply(samples_vec, function(s) { + export_cells %>% + filter(sample == s, !is.na(clone_id), has_tcr) %>% + pull(clone_id) %>% + unique() +}) +names(sample_clone_sets) <- samples_vec + +overlap_mat <- matrix(NA_real_, nrow = length(samples_vec), ncol = length(samples_vec)) +rownames(overlap_mat) <- samples_vec +colnames(overlap_mat) <- samples_vec + +for (i in seq_along(samples_vec)) { + for (j in seq_along(samples_vec)) { + overlap_mat[i, j] <- calc_overlap_score( + sample_clone_sets[[i]], + sample_clone_sets[[j]], + metric = params$overlap_metric + ) + } +} + +overlap_df <- as.data.frame(overlap_mat) %>% + tibble::rownames_to_column("sample") + +save_table_safe(overlap_df, "sample_overlap_matrix.tsv") + +print( + kableExtra::kbl( + overlap_df, + caption = paste("Sample overlap matrix using", params$overlap_metric, "similarity.") + ) %>% + kableExtra::kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed", "responsive") + ) +) + +``` + +# Phenotypic flux +```{r} +#| label: phenotypic-flux +#| results: asis +flux_clone_candidates <- export_cells %>% + filter(!is.na(clone_id), clone_size >= params$min_cells_per_clone_plot, has_tcr) %>% + group_by(clone_id) %>% + summarise( + n_states = safe_n_unique(annot), + total_cells = n(), + .groups = "drop" + ) %>% + filter(n_states > 1) %>% + arrange(desc(total_cells), desc(n_states)) %>% + slice_head(n = params$top_n_flux_clones) + +flux_tbl <- export_cells %>% + filter(clone_id %in% flux_clone_candidates$clone_id) %>% + count(clone_id, sample, annot, name = "n_cells") + +save_table_safe(flux_tbl, "phenotypic_flux.tsv") + +print( + kableExtra::kbl( + flux_tbl %>% slice_head(n = 30), + caption = "Phenotypic flux table for top expanded multi-state clonotypes." + ) %>% + kableExtra::kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed", "responsive") + ) +) + +``` + +# Longitudinal clone tracking +```{r} +#| label: longitudinal-tracking +#| results: asis +longitudinal_tbl <- NULL +if (any(!is.na(export_cells$timepoint))) { + longitudinal_tbl <- export_cells %>% + filter(!is.na(clone_id), !is.na(timepoint), has_tcr) %>% + count(clone_id, sample, patient, timepoint, name = "n_cells") %>% + group_by(clone_id) %>% + summarise( + n_timepoints = safe_n_unique(timepoint), + total_cells = sum(n_cells), + .groups = "drop" + ) %>% + filter(n_timepoints > 1) %>% + arrange(desc(total_cells), desc(n_timepoints)) + + save_table_safe(longitudinal_tbl, "longitudinal_clone_tracking_summary.tsv") + + print( + kableExtra::kbl( + longitudinal_tbl %>% slice_head(n = 30), + caption = "Longitudinally recurrent clonotypes." + ) %>% + kableExtra::kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed", "responsive") + ) + ) +} else { + cat("No longitudinal clone tracking table was produced because no timepoint metadata were available.\n") +} + +``` + +# consensus-cluster-burden +```{r} +#| label: consensus-cluster-burden +#| results: asis +consensus_burden_tbl <- NULL +consensus_cluster_summary <- NULL + +if (any(!is.na(export_cells$consensus_cluster))) { + consensus_burden_tbl <- export_cells %>% + filter(!is.na(consensus_cluster)) %>% + count(sample, consensus_cluster, name = "n_cells") %>% + group_by(sample) %>% + mutate(frac_cells = n_cells / sum(n_cells)) %>% + ungroup() + + save_table_safe(consensus_burden_tbl, "consensus_cluster_burden.tsv") + + consensus_cluster_summary <- export_cells %>% + filter(!is.na(consensus_cluster)) %>% + count(consensus_cluster, name = "n_cells") %>% + mutate(frac_cells = n_cells / sum(n_cells)) %>% + arrange(desc(n_cells)) + + save_table_safe(consensus_cluster_summary, "consensus_cluster_summary.tsv") + + print( + kableExtra::kbl( + consensus_cluster_summary, + caption = "Consensus clonotype cluster burden summary." + ) %>% + kableExtra::kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed", "responsive") + ) + ) +} else { + cat("No consensus cluster burden table was produced because no consensus cluster column was available in the current export table.\n") +} + +``` + + + +# Report overview +```{r} +#| label: overview +#| results: asis +summary_rollup <- tibble( + metric = c( + "Report label", + "Cells in export table", + "TCR-positive cells", + "Paired TCR cells", + "Unique samples", + "Unique patients", + "Unique conditions", + "Unique timepoints", + "Unique clonotypes", + "Shared clonotypes across samples" + ), + value = c( + params$report_label, + nrow(export_cells), + sum(export_cells$has_tcr, na.rm = TRUE), + sum(export_cells$paired_tcr, na.rm = TRUE), + safe_n_unique(export_cells$sample), + safe_n_unique(export_cells$patient), + safe_n_unique(export_cells$condition), + safe_n_unique(export_cells$timepoint), + safe_n_unique(export_cells$clone_id), + sum(shareability_tbl$shared_across_samples, na.rm = TRUE) + ) +) + +save_table_safe(summary_rollup, "repertoire_summary_rollup.tsv") + +print( + kableExtra::kbl( + summary_rollup, + caption = "High-level overview of repertoire analysis." + ) %>% + kableExtra::kable_styling( + full_width = FALSE, + bootstrap_options = c("striped", "hover", "condensed") + ) +) + + +``` + +# Diversity plots +```{r} +#| label: diversity-plots +if (isTRUE(params$show_diversity_boxplots) && nrow(diversity_tbl) > 0) { + div_long <- diversity_tbl %>% + pivot_longer( + cols = c(shannon, simpson, inv_simpson, richness), + names_to = "metric", + values_to = "value" + ) + + p_div <- ggplot(div_long, aes(x = metric, y = value, fill = metric)) + + geom_boxplot(alpha = 0.85, outlier.size = 0.4) + + geom_jitter(width = 0.12, size = 1.2, alpha = 0.7) + + guides(fill = "none") + + labs( + title = "Repertoire diversity across samples", + x = NULL, + y = "Metric value" + ) + + theme_scratch_pub(params$base_size) + + print(p_div) + save_plot_safe(p_div, glue("diversity_boxplots.{params$figure_format}")) +} +``` + +# Clone burden plots +```{r} +#| label: clone-burden-plots +if (isTRUE(params$show_clone_burden_barplot) && nrow(clone_burden_cells_tbl) > 0) { + + sample_order <- clone_burden_cells_tbl %>% + mutate(clone_bin = factor(clone_bin, levels = clone_bin_labels)) %>% + group_by(sample) %>% + summarise( + burden_score = sum(frac_cells * as.numeric(clone_bin), na.rm = TRUE), + .groups = "drop" + ) %>% + arrange(desc(burden_score)) %>% + pull(sample) + + p_burden <- clone_burden_cells_tbl %>% + mutate( + sample = factor(sample, levels = sample_order), + clone_bin = factor(clone_bin, levels = clone_bin_labels) + ) %>% + ggplot(aes(x = sample, y = n_cells, fill = clone_bin)) + + geom_col(position = "fill") + + coord_flip() + + scale_y_continuous(labels = percent_format(accuracy = 1)) + + labs( + title = "Clonal burden across samples", + subtitle = "Samples ordered by overall expansion burden.", + x = NULL, + y = "Fraction of cells", + fill = "Clone bin" + ) + + theme_scratch_pub(params$base_size) + + print(p_burden) + save_plot_safe(p_burden, glue("clone_burden_by_sample.{params$figure_format}")) +} + +``` + +# Rank-abundance +```{r} +#| label: rank-abundance-plot +if (isTRUE(params$show_rank_abundance) && nrow(clone_rank_tbl) > 0) { + p_rank <- ggplot(clone_rank_tbl, aes(x = rank, y = n_cells, color = sample)) + + geom_line(linewidth = 0.9, alpha = 0.8) + + geom_point(size = 1.2, alpha = 0.9) + + scale_x_log10() + + scale_y_log10() + + labs( + title = "Clone rank-abundance curve", + subtitle = glue("Only clones with at least {params$min_cells_per_clone_plot} cells are shown."), + x = "Clone rank (log10)", + y = "Cells per clone (log10)", + color = "Sample" + ) + + theme_scratch_pub(params$base_size) + + print (p_rank) + save_plot_safe(p_rank, glue("clone_rank_abundance.{params$figure_format}")) +} +``` + +# Shareability +```{r} +#| label: shareability-plots +if (isTRUE(params$show_shareability_barplot) && nrow(shareability_tbl) > 0) { + share_bar_tbl <- tibble( + category = c("Shared across samples", "Shared across patients", "Shared across conditions"), + n_clones = c( + sum(shareability_tbl$shared_across_samples, na.rm = TRUE), + sum(shareability_tbl$shared_across_patients, na.rm = TRUE), + sum(shareability_tbl$shared_across_conditions, na.rm = TRUE) + ) + ) + + p_share <- ggplot(share_bar_tbl, aes(x = category, y = n_clones)) + + geom_col(fill = "steelblue") + + coord_flip() + + scale_y_continuous(labels = comma) + + labs( + title = "Clone shareability summary", + x = NULL, + y = "Number of clones" + ) + + theme_scratch_pub(params$base_size) + + print (p_share) + save_plot_safe(p_share, glue("clone_shareability_barplot.{params$figure_format}")) +} +``` + +# Shareability heatmap +```{r} +#| label: shareability-heatmap +if (isTRUE(params$show_shareability_heatmap) && nrow(top_shared_clones_tbl) > 0) { + share_heat_tbl <- export_cells %>% + filter(clone_id %in% top_shared_clones_tbl$clone_id, !is.na(sample), has_tcr) %>% + count(clone_id, sample, name = "n_cells") %>% + pivot_wider(names_from = sample, values_from = n_cells, values_fill = 0) %>% + as.data.frame() + + rownames(share_heat_tbl) <- share_heat_tbl$clone_id + share_heat_tbl$clone_id <- NULL + share_mat <- as.matrix(share_heat_tbl) + + max_mat <- max(share_mat, na.rm = TRUE) + if (!is.finite(max_mat) || max_mat <= 0) max_mat <- 1 + + ht <- ComplexHeatmap::Heatmap( + share_mat, + name = "Cells", + col = circlize::colorRamp2( + c(0, max_mat / 2, max_mat), + c("white", "gold", "firebrick") + ), + cluster_rows = TRUE, + cluster_columns = TRUE, + row_names_side = "left", + row_names_gp = grid::gpar(fontsize = 5), + column_names_gp = grid::gpar(fontsize = 8), + column_names_rot = 45, + row_dend_width = grid::unit(2.5, "cm"), + column_dend_height = grid::unit(2.0, "cm"), + rect_gp = grid::gpar(col = "grey85"), + column_title = "Top shared clonotypes across samples", + heatmap_legend_param = list(title = "Cells") + ) + ComplexHeatmap::draw(ht) + + pdf( + file.path(params$outdir, params$figures_dir, "shareability_heatmap.pdf"), + width = 10, + height = 12 + ) + ComplexHeatmap::draw(ht) + dev.off() +} + + +``` + +# sample-overlap-heatmap +```{r} +#| label: sample-overlap-heatmap +if (isTRUE(params$show_sample_overlap_heatmap) && length(samples_vec) > 1) { + ht <- Heatmap( + overlap_mat, + name = params$overlap_metric, + col = colorRamp2(c(0, 0.5, 1), c("white", "gold", "firebrick")), + cluster_rows = TRUE, + cluster_columns = TRUE, + row_names_side = "left", + row_names_gp = grid::gpar(fontsize = 9), + column_names_gp = grid::gpar(fontsize = 9), + column_names_rot = 45, + rect_gp = grid::gpar(col = "grey85"), + column_title = paste("Sample overlap by", params$overlap_metric), + heatmap_legend_param = list(title = params$overlap_metric) + ) + draw(ht) +} +``` + +# phenotypic-flux-plot +# phenotypic-flux-plot +```{r} +# #| label: phenotypic-flux-plot +# if (isTRUE(params$show_phenotypic_flux) && nrow(flux_tbl) > 0) { +# flux_plot_tbl <- flux_tbl %>% +# group_by(clone_id) %>% +# mutate(total_clone_cells = sum(n_cells)) %>% +# ungroup() %>% +# arrange(desc(total_clone_cells)) %>% +# mutate(clone_id = fct_inorder(clone_id)) +# +# p_flux <- ggplot( +# flux_plot_tbl, +# aes(y = n_cells, axis1 = sample, axis2 = annot) +# ) + +# geom_alluvium(aes(fill = clone_id), width = 1/12, alpha = 0.85) + +# geom_stratum(width = 1/12, fill = "grey85", color = "black") + +# geom_text( +# stat = "stratum", +# aes( +# label = after_stat(stratum), +# hjust = ifelse(after_stat(x) == 1, 1, 0) +# ), +# nudge_x = ifelse(unique(flux_plot_tbl$sample)[1] == unique(flux_plot_tbl$sample)[1], 0, 0), +# size = 3 +# ) + +# scale_x_discrete(limits = c("Sample", "Phenotype"), expand = c(.05, .05)) + +# guides( +# fill = guide_legend( +# ncol = 1, +# byrow = FALSE +# ) +# ) + +# labs( +# title = "Phenotypic flux of expanded clonotypes", +# x = NULL, +# y = "Cells", +# fill = "clone_id" +# ) + +# theme_scratch_pub(params$base_size) + +# theme( +# legend.position = "right", +# legend.direction = "vertical", +# legend.title = element_text(face = "bold"), +# legend.text = element_text(size = 8), +# legend.key.height = unit(0.45, "cm"), +# legend.key.width = unit(0.45, "cm") +# ) +# +# print(p_flux) +# +# save_plot_safe( +# p_flux, +# glue("phenotypic_flux_alluvial.{params$figure_format}"), +# width = 14, +# height = 8 +# ) +# } + + + +#| label: phenotypic-flux-plot +if (isTRUE(params$show_phenotypic_flux) && nrow(flux_tbl) > 0) { + flux_plot_tbl <- flux_tbl %>% + group_by(clone_id) %>% + mutate(total_clone_cells = sum(n_cells)) %>% + ungroup() %>% + arrange(desc(total_clone_cells)) %>% + mutate(clone_id = fct_inorder(clone_id)) + + p_flux <- ggplot( + flux_plot_tbl, + aes(y = n_cells, axis1 = sample, axis2 = annot) + ) + + geom_alluvium(aes(fill = clone_id), width = 1/12, alpha = 0.85) + + geom_stratum(width = 1/12, fill = "grey85", color = "black") + + geom_text(stat = "stratum", aes(label = after_stat(stratum)), size = 3) + + scale_x_discrete(limits = c("Sample", "Phenotype"), expand = c(.05, .05)) + + guides( + fill = guide_legend( + ncol = 1, + byrow = FALSE + ) + ) + + labs( + title = "Phenotypic flux of expanded clonotypes", + x = NULL, + y = "Cells", + fill = "clone_id" + ) + + theme_scratch_pub(params$base_size) + + theme( + legend.position = "right", + legend.direction = "vertical", + legend.title = element_text(face = "bold"), + legend.text = element_text(size = 6), + legend.key.height = unit(0.45, "cm"), + legend.key.width = unit(0.45, "cm") + ) + + print(p_flux) + + save_plot_safe( + p_flux, + glue("phenotypic_flux_alluvial.{params$figure_format}"), + width = 14, + height = 8 + ) +} + + + +``` + +# Longitudional Clone Tracking +```{r} +#| label: longitudinal-tracking-plot +#| results: asis +if (isTRUE(params$show_longitudinal_clone_tracking) && !is.null(longitudinal_tbl) && nrow(longitudinal_tbl) > 0) { + top_longitudinal <- export_cells %>% + filter(!is.na(clone_id), !is.na(timepoint), has_tcr) %>% + count(clone_id, timepoint, name = "n_cells") %>% + group_by(clone_id) %>% + summarise( + total_cells = sum(n_cells), + n_timepoints = safe_n_unique(timepoint), + .groups = "drop" + ) %>% + filter(n_timepoints > 1) %>% + arrange(desc(total_cells)) %>% + slice_head(n = params$top_n_flux_clones) %>% + pull(clone_id) + + long_plot_tbl <- export_cells %>% + filter(clone_id %in% top_longitudinal, !is.na(timepoint), has_tcr) %>% + count(clone_id, timepoint, name = "n_cells") + + p_long <- ggplot(long_plot_tbl, aes(x = timepoint, y = n_cells, color = clone_id, group = clone_id)) + + geom_line(linewidth = 0.9) + + geom_point(size = 2) + + labs( + title = "Longitudinal tracking of recurrent clonotypes", + x = "Timepoint", + y = "Cells", + color = "Clone" + ) + + theme_scratch_pub(params$base_size) + + print(p_long) + save_plot_safe(p_long, glue("longitudinal_clone_tracking.{params$figure_format}"), width = 11, height = 7) +} else { + cat("No longitudinal clone tracking plot was produced because no usable timepoint metadata were available.\n") +} + +``` + +# consensus-burden-plot +```{r} +#| label: consensus-burden-plot +#| results: asis +if (isTRUE(params$show_consensus_cluster_burden) && !is.null(consensus_burden_tbl) && nrow(consensus_burden_tbl) > 0) { + top_n_cons <- params$top_n_clusters + if (is.null(top_n_cons) || length(top_n_cons) == 0 || is.na(top_n_cons)) { + top_n_cons <- 20 + } + top_n_cons <- as.integer(top_n_cons) + + top_cons <- consensus_burden_tbl %>% + group_by(consensus_cluster) %>% + summarise(total_cells = sum(n_cells), .groups = "drop") %>% + arrange(desc(total_cells)) %>% + slice_head(n = top_n_cons) %>% + pull(consensus_cluster) + + p_cons <- consensus_burden_tbl %>% + filter(consensus_cluster %in% top_cons) %>% + ggplot(aes(x = sample, y = frac_cells, fill = consensus_cluster)) + + geom_col(position = "fill") + + coord_flip() + + scale_y_continuous(labels = percent_format(accuracy = 1)) + + labs( + title = "Consensus clonotype burden across samples", + x = NULL, + y = "Fraction of cells", + fill = "Consensus cluster" + ) + + theme_scratch_pub(params$base_size) + + print(p_cons) + save_plot_safe(p_cons, glue("consensus_cluster_burden.{params$figure_format}"), width = 11, height = 8) +} else { + cat("No consensus burden plot was produced because consensus cluster assignments were not available in the current repertoire input.\n") +} + + + +``` + +# save-repertoire-objects +```{r} +#| label: save-repertoire-objects +save_rds_safe(export_cells, "repertoire_export_cells.rds") + +save_rdata_safe( + export_cells, + clone_counts_by_sample, + diversity_tbl, + clone_burden_tbl, + clone_burden_cells_tbl, + clone_rank_tbl, + shareability_tbl, + top_shared_clones_tbl, + overlap_mat, + flux_tbl, + longitudinal_tbl, + consensus_burden_tbl, + consensus_cluster_summary, + summary_rollup, + filename = "repertoire_analysis_objects.RData" +) +``` + +# Summary tables display +```{r} +#| label: summary-tables-display +#| results: asis +if (isTRUE(params$show_summary_tables)) { + + print( + kableExtra::kbl( + diversity_tbl, + caption = "Diversity metrics by sample." + ) %>% + kableExtra::kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed", "responsive") + ) + ) + + print( + kableExtra::kbl( + top_shared_clones_tbl, + caption = "Top shared clonotypes." + ) %>% + kableExtra::kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed", "responsive") + ) + ) + + if (!is.null(consensus_cluster_summary) && nrow(consensus_cluster_summary) > 0) { + top_n_cons <- params$top_n_clusters + if (is.null(top_n_cons) || length(top_n_cons) == 0 || is.na(top_n_cons)) { + top_n_cons <- 20 + } + top_n_cons <- as.integer(top_n_cons) + + print( + kableExtra::kbl( + consensus_cluster_summary %>% slice_head(n = top_n_cons), + caption = "Top consensus clonotype clusters by burden." + ) %>% + kableExtra::kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed", "responsive") + ) + ) + } else { + cat("Consensus-cluster summary table was skipped because no consensus clusters were available in the current repertoire input.\n") + } +} + +``` + +# Warnings +```{r} +#| label: warnings +#| results: asis +warn_tbl <- tibble( + warning = c( + "Few unique clonotypes", + "No patient metadata", + "No condition metadata", + "No timepoint metadata", + "No consensus clusters available", + "Limited shareability across samples" + ), + triggered = c( + safe_n_unique(export_cells$clone_id) < 20, + all(is.na(export_cells$patient)), + all(is.na(export_cells$condition)), + all(is.na(export_cells$timepoint)), + all(is.na(export_cells$consensus_cluster)), + sum(shareability_tbl$shared_across_samples, na.rm = TRUE) < 5 + ), + interpretation = c( + "Fewer than 20 unique clonotypes were detected, which may limit repertoire-level interpretation.", + "Patient-level summaries were skipped because patient metadata were unavailable.", + "Condition-level summaries were skipped because condition metadata were unavailable.", + "Longitudinal summaries were skipped because timepoint metadata were unavailable.", + "Consensus-cluster-aware summaries were limited because no consensus clusters were present.", + "Very few clones were shared across samples under the current threshold." + ) +) %>% + filter(triggered) + +if (nrow(warn_tbl) == 0) { + cat("No major automatic warnings were triggered under the current repertoire reporting settings.") +} else { + print( + kableExtra::kbl( + warn_tbl %>% select(-triggered), + caption = "Automatically generated repertoire analysis warnings and notes." + ) %>% + kableExtra::kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed") + ) + ) +} + +``` + +# session info +```{r} +#| label: session-info +writeLines(capture.output(sessionInfo()), file.path(params$outdir, "sessionInfo.repertoire.txt")) +sessionInfo() +``` + + + + + + + + + + diff --git a/modules/scratch/REPERTOIRE/main.nf b/modules/scratch/REPERTOIRE/main.nf new file mode 100644 index 0000000..c047cad --- /dev/null +++ b/modules/scratch/REPERTOIRE/main.nf @@ -0,0 +1,43 @@ +process REPERTOIRE { + tag "${project_name}" + label 'process_medium' + container "${params.container}" + + + publishDir "${params.outdir}/Repertoire", mode: 'copy', overwrite: true + + input: + path seurat_rds + path export_cells + path qmd + val project_name + + output: + path "Repertoire_Report.html", emit: report_html + path "Repertoire_Report/tables/*", emit: tables + path "Repertoire_Report/figures/*", emit: figures + + script: + """ + mkdir -p Repertoire_Report + + quarto render ${qmd} \ + -P seurat_rds=${seurat_rds} \ + -P export_cells_file=${export_cells} \ + -P outdir=Repertoire_Report \ + -P label_col="${params.label_col}" \ + -P sample_col="${params.sample_col}" \ + -P patient_col="${params.patient_col}" \ + -P condition_col="${params.condition_col}" \ + -P timepoint_col="${params.timepoint_col}" \ + -P batch_col="${params.batch_col}" \ + -P min_cells_per_clone_plot=${params.repertoire_min_cells_per_clone_plot} \ + -P top_n_shared_clones=${params.repertoire_top_n_shared_clones} \ + -P top_n_flux_clones=${params.repertoire_top_n_flux_clones} \ + -P shareability_min_n_groups=${params.repertoire_shareability_min_n_groups} \ + -P min_cells_per_group=${params.repertoire_min_cells_per_group} \ + -P overlap_metric="${params.repertoire_overlap_metric}" \ + -P use_relative_clone_frequencies=${params.repertoire_use_relative_clone_freqs} \ + -P report_label="${project_name} Repertoire" + """ +} \ No newline at end of file diff --git a/modules/scratch/TCELL_INTEGRATION/TCell_Integration_Report.qmd b/modules/scratch/TCELL_INTEGRATION/TCell_Integration_Report.qmd new file mode 100644 index 0000000..1d87b30 --- /dev/null +++ b/modules/scratch/TCELL_INTEGRATION/TCell_Integration_Report.qmd @@ -0,0 +1,2549 @@ +--- +title: "SCRATCH-TCR: T-Cell Integration Report" +author: "Syed Shujaat Ali Zaidi" +format: + html: + toc: true + toc-depth: 3 + number-sections: true + code-fold: true + code-summary: "Show code" + embed-resources: true + theme: cosmo + df-print: paged +execute: + echo: false + warning: false + message: false +params: + # ====================================================== + # Inputs + # ====================================================== + seurat_rds: "data/project_annotated_object.RDS" + contigs_file: "data/contigs_after_qc.tsv" + sample_sheet: "" + metadata_file: "" + outdir: "TCell_Integration_Report" + + # Optional upstream artifacts + clonotypes_file: "" + vdj_qc_summary_file: "" + barcode_harmonization_required: true + + # ====================================================== + # General output controls + # ====================================================== + data_dir: "data" + tables_dir: "tables" + figures_dir: "figures" + save_tables: true + save_figures: true + figure_format: "png" + figure_width: 10 + figure_height: 7 + figure_dpi: 300 + base_size: 12 + + # ====================================================== + # Metadata mapping in Seurat + # ====================================================== + label_col: "" + sample_col: "orig.ident" + patient_col: "" + condition_col: "" + timepoint_col: "" + batch_col: "" + + sample_candidates: !expr c("META_SAMPLE","orig.ident","sample","sample_id","Sample","SampleID") + patient_candidates: !expr c("META_PATIENT","patient_id","patient","Patient","subject","donor","case_id") + condition_candidates: !expr c("META_TIMECOND","condition","Condition","group","Group","status","timepoint","Timepoint") + timepoint_candidates: !expr c("META_TIMECOND","timepoint","Timepoint","visit","Visit","day","Day") + batch_candidates: !expr c("META_BATCH","batch","Batch","library","Library","run","Run","lane","Lane") + + label_candidates: !expr c( + "predicted.celltype.l2","predicted.celltype.l1","azimuth_labels", + "celltypist","celltypist_label","majority_voting", + "scType","sctype","sctype_label","celltype","CellType","annot","Annotation") + + # ====================================================== + # Contig mapping + # ====================================================== + contig_sample_col: "sample" + contig_barcode_col: "barcode" + contig_chain_col: "chain" + contig_cdr3_col: "cdr3" + contig_v_col: "v_gene" + contig_j_col: "j_gene" + contig_clone_col: "raw_clonotype_id" + + contig_sample_candidates: !expr c("sample","sample_id","orig.ident") + contig_barcode_candidates: !expr c("barcode","cell_id","cell","CB") + contig_chain_candidates: !expr c("chain","locus") + contig_cdr3_candidates: !expr c("cdr3","cdr3_aa","CDR3aa","junction_aa") + contig_v_candidates: !expr c("v_gene","v_call","bestVGene") + contig_j_candidates: !expr c("j_gene","j_call","bestJGene") + contig_clone_candidates: !expr c("raw_clonotype_id","clonotype_id","clone_id") + + # ====================================================== + # Integration / scRepertoire controls + # ====================================================== + cells_mode: "T-AB" # T-AB / T-GD / both + filter_to_t_ab: true + combine_id: "" + clone_call_preference: "aa" # aa / nt / strict / gene + keep_na_clonotypes: false + + # ====================================================== + # Barcode harmonization controls + # ====================================================== + harmonize_apply_to: "tcr" # tcr / seurat + harmonization_overlap_threshold: 0.80 + minimum_final_overlap_fraction: 0.50 + clean_duplicated_sample_prefix: true + + # ====================================================== + # T-cell subset controls + # ====================================================== + subset_tcells: true + tcell_regex: "(?i)(^|[^A-Za-z])(t[ -]?cell|cd4|cd8|treg|trm|tem|naive t|memory t|exhausted t|cytotoxic t|gamma.?delta|gd t)([^A-Za-z]|$)" + use_ident_if_label_missing: true + + # ====================================================== + # UMAP / plotting controls + # ====================================================== + reduction_use: "umap" + make_umap_if_missing: true + umap_dims_max: 30 + umap_nfeatures: 3000 + raster_large_umap: true + label_clusters: true + + # ====================================================== + # Clone summarization controls + # ====================================================== + min_clone_size_plot: 2 + top_n_clones_umap: 12 + top_n_clone_table: 50 + clone_bins_quantiles: !expr c(0, 0.2, 0.4, 0.6, 0.8, 1.0) + force_quantile_clone_bins: true + + # ====================================================== + # Report toggles + # ====================================================== + show_barcode_harmonization: true + show_pairing_summary: true + show_clone_rank_plot: true + show_clone_state_heatmap: true + show_homeostasis: true + show_diversity: true + show_sample_panels: true + show_condition_panels: true + show_patient_panels: true + show_timepoint_panels: true + show_top_clone_umaps: true + + report_label: "T-Cell Integration" +--- + +# ======================================================== +## Integrates post-QC VDJ contigs with an annotated Seurat object using scRepertoire and a robust fast-merge strategy. +## Performs barcode cleaning, harmonization checks, and merge diagnostics to maximize reliable TCR↔GEX matching. +## Adds per-cell TCR metadata to Seurat, including clonotype IDs, paired chains, and clone sizes. +## Builds a T-cell-focused integrated object and generates high-quality embedding and clonotype visualizations. +## Exports reusable RDS objects, diagnostic tables, and summary figures for downstream TCRi, CoNGA, and clustering modules. +# ======================================================== + +## setup +```{r} +#| label: setup +suppressPackageStartupMessages({ + library(Seurat) + library(SeuratObject) + library(dplyr) + library(data.table) + library(stringr) + library(ggplot2) + library(patchwork) + library(tidyr) + library(purrr) + library(Matrix) + library(scales) + library(readr) + library(ComplexHeatmap) + library(circlize) + library(forcats) + library(glue) + library(knitr) + library(kableExtra) + library(scRepertoire) +}) + +options(stringsAsFactors = FALSE) + +dir.create(params$outdir, recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$data_dir), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$tables_dir), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$figures_dir), recursive = TRUE, showWarnings = FALSE) + +`%||%` <- function(a, b) if (!is.null(a) && length(a) > 0 && !all(is.na(a))) a else b + +# theme_scratch_pub <- function(base_size = 12) { +# theme_bw(base_size = base_size) + +# theme( +# plot.title = element_text(face = "bold", size = base_size + 2, hjust = 0), +# plot.subtitle = element_text(size = base_size, hjust = 0), +# axis.title = element_text(face = "bold"), +# axis.text = element_text(color = "black"), +# panel.grid.minor = element_blank(), +# panel.grid.major = element_line(linewidth = 0.2, color = "grey90"), +# strip.background = element_rect(fill = "grey95", color = "grey80"), +# strip.text = element_text(face = "bold"), +# legend.title = element_text(face = "bold"), +# legend.key = element_blank(), +# plot.caption = element_text(size = base_size - 2, color = "grey40") +# ) +# } +# theme_scratch_pub <- function(base_size = 12) { +# theme_bw(base_size = base_size) + +# theme( +# plot.title = element_text(face = "bold", size = base_size + 2, hjust = 0), +# plot.subtitle = element_text(size = base_size, hjust = 0), +# axis.title = element_text(face = "bold", size = base_size - 1), +# # Shrink Y-axis labels slightly +# axis.text.y = element_text(color = "black", size = base_size - 3), +# # Rotate X-axis labels 45 degrees and shrink +# axis.text.x = element_text(color = "black", size = base_size - 3, angle = 45, hjust = 1), +# panel.grid.minor = element_blank(), +# panel.grid.major = element_line(linewidth = 0.1, color = "grey90"), +# strip.background = element_rect(fill = "grey95", color = "grey80"), +# strip.text = element_text(face = "bold", size = base_size - 2), +# legend.title = element_text(face = "bold", size = base_size - 2), +# legend.text = element_text(size = base_size - 3), +# plot.caption = element_text(size = base_size - 4, color = "grey40") +# ) +# } + +theme_scratch_pub <- function(base_size = 12) { + theme_bw(base_size = base_size) + + theme( + plot.title = element_text(face = "bold", size = base_size + 2, hjust = 0), + plot.subtitle = element_text(size = base_size, hjust = 0), + axis.title = element_text(face = "bold", size = base_size - 1), + axis.text.y = element_text(color = "black", size = base_size - 3), + # Fix for Q2 & Q3: Smaller text and 45-degree rotation for X-axis + axis.text.x = element_text(color = "black", size = base_size - 3, angle = 45, hjust = 1), + panel.grid.minor = element_blank(), + panel.grid.major = element_line(linewidth = 0.1, color = "grey90"), + strip.background = element_rect(fill = "grey95", color = "grey80"), + strip.text = element_text(face = "bold", size = base_size - 2), + legend.title = element_text(face = "bold", size = base_size - 2), + legend.text = element_text(size = base_size - 3), + legend.key = element_blank(), + plot.caption = element_text(size = base_size - 4, color = "grey40") + ) +} + + + +save_plot_safe <- function(plot_obj, filename, + width = params$figure_width, + height = params$figure_height, + dpi = params$figure_dpi) { + if (!isTRUE(params$save_figures)) return(invisible(NULL)) + ggsave( + filename = file.path(params$outdir, params$figures_dir, filename), + plot = plot_obj, + width = width, + height = height, + dpi = dpi, + bg = "white", + limitsize = FALSE + ) +} + +save_table_safe <- function(df, filename) { + if (!isTRUE(params$save_tables)) return(invisible(NULL)) + fwrite(df, file.path(params$outdir, params$tables_dir, filename), sep = "\t") +} + +save_rds_safe <- function(obj, filename) { + saveRDS(obj, file.path(params$outdir, params$data_dir, filename)) +} + +safe_read_table <- function(path) { + if (is.null(path) || is.na(path) || path == "" || !file.exists(path)) return(NULL) + ext <- tolower(tools::file_ext(path)) + if (ext %in% c("tsv", "tab", "txt")) { + fread(path, sep = "\t") + } else { + fread(path) + } +} + +require_file <- function(path, msg = NULL) { + if (!file.exists(path)) stop(msg %||% paste("Missing required file:", path)) + path +} + +first_existing_col <- function(df, preferred, candidates = character()) { + if (!is.null(preferred) && preferred != "" && preferred %in% colnames(df)) return(preferred) + hits <- intersect(candidates, colnames(df)) + if (length(hits) > 0) return(hits[[1]]) + NULL +} + +has_col <- function(df, x) x %in% colnames(df) + +detect_label_col <- function(seu, preferred = "", candidates = character(), use_ident = TRUE) { + md <- seu@meta.data + if (!is.null(preferred) && preferred != "" && preferred %in% colnames(md)) return(preferred) + hit <- intersect(candidates, colnames(md)) + if (length(hit)) return(hit[1]) + if (use_ident) { + seu$seurat_clusters <- as.character(Idents(seu)) + return("seurat_clusters") + } + stop("Could not find a valid annotation column in Seurat metadata.") +} + +choose_col <- function(df, primary = NULL, fallback = NULL) { + nm <- colnames(df) + if (!is.null(primary) && primary != "" && primary %in% nm) return(primary) + if (!is.null(fallback) && fallback != "" && fallback %in% nm) return(fallback) + NULL +} + +clean_barcode <- function(x) sub("^([A-Za-z0-9-]+)_\\1_", "\\1_", x) + +combineTCR_safe <- function(contig_list, samples, ID = NULL, cells = "T-AB") { + fun <- get("combineTCR", asNamespace("scRepertoire")) + fml <- names(formals(fun)) + args <- list() + if ("contig_list" %in% fml) args$contig_list <- contig_list else if ("contig.list" %in% fml) args$contig.list <- contig_list else args[[1]] <- contig_list + if ("samples" %in% fml) args$samples <- samples else args[[length(args)+1]] <- samples + if ("ID" %in% fml) args$ID <- ID else args[[length(args)+1]] <- ID + if ("cells" %in% fml) args$cells <- cells + do.call(fun, args) +} + +dropNA_clonotypes <- function(combined_obj) { + detect_clone_col <- function(df) { + candidates <- c("CTaa", "CTnt", "CTstrict", "CTgene", "clonotype", "cloneType", "cloneCall") + hit <- intersect(candidates, colnames(df)) + if (length(hit) == 0) return(NA_character_) + if ("CTaa" %in% hit) return("CTaa") + hit[1] + } + filter_one <- function(df) { + if (!is.data.frame(df) || nrow(df) == 0) return(df) + cc <- detect_clone_col(df) + if (is.na(cc)) return(df) + keep <- !is.na(df[[cc]]) & df[[cc]] != "" & df[[cc]] != "NA" + df[keep, , drop = FALSE] + } + if (is.list(combined_obj) && !is.data.frame(combined_obj)) { + out <- lapply(combined_obj, filter_one) + out <- out[vapply(out, nrow, integer(1)) > 0] + return(out) + } + filter_one(combined_obj) +} + +safe_make_umap <- function(seu, reduction_name = "umap", dims = 1:30, nfeatures = 3000, seed = 1234) { + if ("RNA" %in% names(seu@assays)) DefaultAssay(seu) <- "RNA" + npcs <- min(max(dims), max(2, ncol(seu) - 1), 50) + nf <- min(nfeatures, nrow(seu)) + + if (!"pca" %in% Reductions(seu)) { + seu <- FindVariableFeatures(seu, nfeatures = nf, verbose = FALSE) + seu <- ScaleData(seu, verbose = FALSE) + seu <- RunPCA(seu, npcs = npcs, verbose = FALSE) + } + + use_dims <- dims[dims <= npcs] + if (length(use_dims) < 2) use_dims <- 1:min(10, npcs) + + seu <- FindNeighbors(seu, dims = use_dims, verbose = FALSE) + seu <- RunUMAP(seu, dims = use_dims, reduction.name = reduction_name, verbose = FALSE) + seu +} + +score_harmonizer_candidates <- function(combined, seu, seu_sample_col = "orig.ident") { + md <- seu@meta.data + if (!seu_sample_col %in% colnames(md)) seu_sample_col <- "orig.ident" + if (!seu_sample_col %in% colnames(md)) stop("Seurat lacks sample column: ", seu_sample_col) + + seu_barcodes <- colnames(seu) + all_tcr <- data.table::rbindlist(combined, idcol = "sample", fill = TRUE) + tcr_samples <- as.character(all_tcr$sample) + tcr_barcodes <- as.character(all_tcr$barcode) + + add_minus1 <- function(x) ifelse(grepl("-\\d+$", x), x, paste0(x, "-1")) + remove_minus1 <- function(x) sub("-\\d+$", "", x) + prepend_us <- function(x, s) paste(s, x, sep = "_") + append_us <- function(x, s) paste(x, s, sep = "_") + prepend_dash <- function(x, s) paste(s, x, sep = "-") + append_dash <- function(x, s) paste(x, s, sep = "-") + # scRepertoire::combineTCR() unconditionally prepends "{sample}_" to its own + # barcode column regardless of the `ID` argument, so a real dataset's TCR + # barcodes routinely arrive already-prefixed - none of the "add a prefix" + # candidates above can undo that; this one strips it back off. + strip_sample <- function(x, s) sub(paste0("^", s, "_"), "", x) + + candidates <- list( + list(name = "identity", f = function(x, s) x), + list(name = "add_-1", f = function(x, s) add_minus1(x)), + list(name = "remove_-1", f = function(x, s) remove_minus1(x)), + list(name = "prepend_sample_", f = function(x, s) prepend_us(x, s)), + list(name = "append_sample_", f = function(x, s) append_us(x, s)), + list(name = "prepend-sample", f = function(x, s) prepend_dash(x, s)), + list(name = "append-sample", f = function(x, s) append_dash(x, s)), + list(name = "strip_sample_", f = function(x, s) strip_sample(x, s)) + ) + + score <- lapply(candidates, function(cn) { + xb <- cn$f(tcr_barcodes, tcr_samples) + inter <- length(intersect(xb, seu_barcodes)) + data.frame( + transform = cn$name, + overlap = inter, + total = length(xb), + rate = ifelse(length(xb) > 0, inter / length(xb), NA_real_), + stringsAsFactors = FALSE + ) + }) + score <- do.call(rbind, score) + score <- score[order(-score$overlap, -score$rate), , drop = FALSE] + list(table = score, selected = score$transform[1]) +} + +harmonize_barcodes <- function(combined, seu, seu_sample_col = "orig.ident", + apply_to = c("tcr", "seurat"), verbose = TRUE) { + apply_to <- match.arg(apply_to) + md <- seu@meta.data + seu_barcodes <- colnames(seu) + + if (!seu_sample_col %in% colnames(md)) { + warning("'", seu_sample_col, "' not in Seurat metadata; falling back to 'orig.ident'.") + seu_sample_col <- "orig.ident" + } + if (!seu_sample_col %in% colnames(md)) stop("Seurat lacks a per-cell sample column.") + + seu_samples_per_cell <- as.character(md[[seu_sample_col]]) + all_tcr <- data.table::rbindlist(combined, idcol = "sample", fill = TRUE) + tcr_samples <- as.character(all_tcr$sample) + tcr_barcodes <- as.character(all_tcr$barcode) + + add_minus1 <- function(x) ifelse(grepl("-\\d+$", x), x, paste0(x, "-1")) + remove_minus1 <- function(x) sub("-\\d+$", "", x) + prepend_us <- function(x, s) paste(s, x, sep = "_") + append_us <- function(x, s) paste(x, s, sep = "_") + prepend_dash <- function(x, s) paste(s, x, sep = "-") + append_dash <- function(x, s) paste(x, s, sep = "-") + # See the matching comment in score_harmonizer_candidates() - combineTCR() + # already prepends "{sample}_" to its barcode column, so this undoes it. + strip_sample <- function(x, s) sub(paste0("^", s, "_"), "", x) + + cand <- list( + list(name = "identity", side = "tcr", f = function(x, s) x), + list(name = "add_-1", side = "tcr", f = function(x, s) add_minus1(x)), + list(name = "remove_-1", side = "tcr", f = function(x, s) remove_minus1(x)), + list(name = "prepend_sample_", side = "tcr", f = function(x, s) prepend_us(x, s)), + list(name = "append_sample_", side = "tcr", f = function(x, s) append_us(x, s)), + list(name = "prepend-sample", side = "tcr", f = function(x, s) prepend_dash(x, s)), + list(name = "append-sample", side = "tcr", f = function(x, s) append_dash(x, s)), + list(name = "strip_sample_", side = "tcr", f = function(x, s) strip_sample(x, s)), + list(name = "seurat_add_-1", side = "seurat", f = function(x, s) add_minus1(x)), + list(name = "seurat_remove_-1", side = "seurat", f = function(x, s) remove_minus1(x)), + list(name = "seurat_prepend_", side = "seurat", f = function(x, s) prepend_us(x, s)), + list(name = "seurat_append_", side = "seurat", f = function(x, s) append_us(x, s)), + list(name = "seurat_prepend-", side = "seurat", f = function(x, s) prepend_dash(x, s)), + list(name = "seurat_append-", side = "seurat", f = function(x, s) append_dash(x, s)) + ) + + score_tbl <- lapply(cand, function(cn) { + if (cn$side == "tcr") { + xb <- cn$f(tcr_barcodes, tcr_samples) + inter <- length(intersect(xb, seu_barcodes)) + list(name = cn$name, side = "tcr", overlap = inter, total = length(xb)) + } else { + if (apply_to != "seurat") return(NULL) + xb <- cn$f(seu_barcodes, seu_samples_per_cell) + inter <- length(intersect(tcr_barcodes, xb)) + list(name = cn$name, side = "seurat", overlap = inter, total = length(xb)) + } + }) + score_tbl <- score_tbl[!sapply(score_tbl, is.null)] + score_tbl <- do.call(rbind, lapply(score_tbl, as.data.frame)) + score_tbl$rate <- score_tbl$overlap / pmax(score_tbl$total, 1) + + pool <- subset(score_tbl, side == apply_to) + best <- pool[which.max(pool$overlap), , drop = FALSE] + + if (verbose) { + print(pool[order(-pool$overlap, -pool$rate), ], row.names = FALSE) + } + + if (apply_to == "tcr") { + combined2 <- lapply(names(combined), function(sid) { + df <- combined[[sid]] + df$barcode <- switch(best$name, + "identity" = df$barcode, + "add_-1" = add_minus1(df$barcode), + "remove_-1" = remove_minus1(df$barcode), + "prepend_sample_" = prepend_us(df$barcode, sid), + "append_sample_" = append_us(df$barcode, sid), + "prepend-sample" = prepend_dash(df$barcode, sid), + "append-sample" = append_dash(df$barcode, sid), + "strip_sample_" = strip_sample(df$barcode, sid), + df$barcode + ) + df + }) + names(combined2) <- names(combined) + return(list(combined = combined2, transformed = best$name, table = pool)) + } else { + seu2 <- seu + new_names <- switch(best$name, + "seurat_add_-1" = add_minus1(seu_barcodes), + "seurat_remove_-1" = remove_minus1(seu_barcodes), + "seurat_prepend_" = prepend_us(seu_barcodes, seu_samples_per_cell), + "seurat_append_" = append_us(seu_barcodes, seu_samples_per_cell), + "seurat_prepend-" = prepend_dash(seu_barcodes, seu_samples_per_cell), + "seurat_append-" = append_dash(seu_barcodes, seu_samples_per_cell), + seu_barcodes + ) + colnames(seu2) <- new_names + return(list(seurat = seu2, transformed = best$name, table = pool)) + } +} + +subset_tcells_safe <- function(seu, label_col, regex, fallback_ident = TRUE) { + seu <- repair_seurat_for_subset(seu) + + md <- seu@meta.data + + if (!label_col %in% colnames(md)) { + if (fallback_ident) { + seu$seurat_clusters <- as.character(Idents(seu)) + label_col <- "seurat_clusters" + } else { + stop("Label column not found: ", label_col) + } + } + + labs <- as.character(md[[label_col]]) + keep <- grepl(regex, labs, perl = TRUE) + + if (sum(keep, na.rm = TRUE) == 0) { + warning("No T-cell subset matched the provided regex. Returning repaired original object.") + return(seu) + } + + keep_cells <- rownames(md)[keep] + subset(seu, cells = keep_cells) +} + + +safe_percent <- function(x, denom) ifelse(denom > 0, x / denom, NA_real_) + +repair_seurat_for_subset <- function(seu) { + suppressPackageStartupMessages({ + library(Seurat) + library(SeuratObject) + library(Matrix) + }) + + `%||%` <- function(a, b) if (!is.null(a)) a else b + + get_any_matrix <- function(obj, assay_name) { + out <- NULL + + # Try Seurat v4/v5 standard accessors first + out <- tryCatch(Seurat::GetAssayData(obj, assay = assay_name, slot = "counts"), error = function(e) NULL) + if (!is.null(out) && nrow(out) > 0 && ncol(out) > 0) return(out) + + out <- tryCatch(Seurat::GetAssayData(obj, assay = assay_name, slot = "data"), error = function(e) NULL) + if (!is.null(out) && nrow(out) > 0 && ncol(out) > 0) return(out) + + # Try Seurat v5 layers explicitly + lyr_names <- tryCatch(SeuratObject::Layers(obj[[assay_name]]), error = function(e) NULL) + if (!is.null(lyr_names) && length(lyr_names) > 0) { + for (lyr in c("counts", "data", lyr_names)) { + out <- tryCatch(SeuratObject::LayerData(obj[[assay_name]], layer = lyr), error = function(e) NULL) + if (!is.null(out) && nrow(out) > 0 && ncol(out) > 0) return(out) + } + } + + NULL + } + + assay_names <- names(seu@assays) + if (length(assay_names) == 0) { + stop("No assays found in Seurat object.") + } + + cat("repair_seurat_for_subset(): assays found ->", paste(assay_names, collapse = ", "), "\n") + cat("repair_seurat_for_subset(): default assay ->", DefaultAssay(seu), "\n") + + mat <- NULL + chosen_assay <- NULL + + assay_priority <- unique(c(DefaultAssay(seu), "RNA", assay_names)) + + for (aa in assay_priority) { + if (!aa %in% assay_names) next + candidate <- get_any_matrix(seu, aa) + if (!is.null(candidate)) { + mat <- candidate + chosen_assay <- aa + break + } + } + + if (is.null(mat)) { + stop("Could not recover a usable assay matrix to rebuild Seurat object.") + } + + cat("repair_seurat_for_subset(): using assay ->", chosen_assay, "\n") + cat("repair_seurat_for_subset(): matrix dim ->", nrow(mat), "x", ncol(mat), "\n") + + # Ensure sparse matrix if possible + if (!inherits(mat, "dgCMatrix")) { + mat <- tryCatch(as(mat, "dgCMatrix"), error = function(e) Matrix::Matrix(as.matrix(mat), sparse = TRUE)) + } + + # Rebuild a minimal clean Seurat object + seu_new <- Seurat::CreateSeuratObject( + counts = mat, + assay = chosen_assay, + meta.data = seu@meta.data[colnames(mat), , drop = FALSE] + ) + + DefaultAssay(seu_new) <- chosen_assay + + # Restore reductions only if cell order is compatible + red_names <- tryCatch(SeuratObject::Reductions(seu), error = function(e) character()) + if (length(red_names) > 0) { + for (r in red_names) { + emb <- tryCatch(Seurat::Embeddings(seu, reduction = r), error = function(e) NULL) + if (is.null(emb)) next + common_cells <- intersect(colnames(seu_new), rownames(emb)) + if (length(common_cells) < 2) next + + emb2 <- emb[common_cells, , drop = FALSE] + seu_new <- subset(seu_new, cells = common_cells) + + dr <- SeuratObject::CreateDimReducObject( + embeddings = emb2, + key = paste0(toupper(substr(r, 1, 1)), "_"), + assay = DefaultAssay(seu_new) + ) + seu_new[[r]] <- dr + } + } + + # Restore identities if possible + old_ids <- tryCatch(Idents(seu), error = function(e) NULL) + if (!is.null(old_ids)) { + old_ids <- as.character(old_ids) + names(old_ids) <- names(Idents(seu)) + common_id_cells <- intersect(colnames(seu_new), names(old_ids)) + if (length(common_id_cells) > 0) { + Idents(seu_new) <- factor(old_ids[colnames(seu_new)]) + } + } + + validObject(seu_new) + seu_new +} + +``` + +##loading inputs +```{r} +#| label: read-inputs +require_file(params$seurat_rds, sprintf("Seurat RDS not found: %s", params$seurat_rds)) +require_file(params$contigs_file, sprintf("Filtered contigs file not found: %s", params$contigs_file)) + +seu <- readRDS(params$seurat_rds) +contigs_all_post <- safe_read_table(params$contigs_file) %>% as.data.frame() +clonotypes_tbl <- safe_read_table(params$clonotypes_file) +metadata_tbl <- safe_read_table(params$metadata_file) +vdj_qc_summary_tbl <- safe_read_table(params$vdj_qc_summary_file) +``` + +## standardize-contigs +```{r} +#| label: standardize-contigs +sample_col_ctg <- first_existing_col(contigs_all_post, params$contig_sample_col, params$contig_sample_candidates) +barcode_col_ctg <- first_existing_col(contigs_all_post, params$contig_barcode_col, params$contig_barcode_candidates) +chain_col_ctg <- first_existing_col(contigs_all_post, params$contig_chain_col, params$contig_chain_candidates) +cdr3_col_ctg <- first_existing_col(contigs_all_post, params$contig_cdr3_col, params$contig_cdr3_candidates) +v_col_ctg <- first_existing_col(contigs_all_post, params$contig_v_col, params$contig_v_candidates) +j_col_ctg <- first_existing_col(contigs_all_post, params$contig_j_col, params$contig_j_candidates) +clone_col_ctg <- first_existing_col(contigs_all_post, params$contig_clone_col, params$contig_clone_candidates) + +need_cols <- c(sample_col_ctg, barcode_col_ctg, chain_col_ctg, cdr3_col_ctg) +if (any(is.null(need_cols))) stop("Could not resolve required contig columns.") + +contigs_all_post <- contigs_all_post %>% + mutate( + sample = as.character(.data[[sample_col_ctg]]), + barcode = as.character(.data[[barcode_col_ctg]]), + chain = as.character(.data[[chain_col_ctg]]), + cdr3 = as.character(.data[[cdr3_col_ctg]]), + v_gene = if (!is.null(v_col_ctg)) as.character(.data[[v_col_ctg]]) else NA_character_, + j_gene = if (!is.null(j_col_ctg)) as.character(.data[[j_col_ctg]]) else NA_character_, + raw_clonotype_id = if (!is.null(clone_col_ctg)) as.character(.data[[clone_col_ctg]]) else NA_character_ + ) + +# fread() (used by safe_read_table() above) auto-detects "True"/"False" string +# columns as logical - scRepertoire::combineTCR() expects productive as a +# string and silently misclassifies every contig if it isn't one. +if ("productive" %in% colnames(contigs_all_post)) { + contigs_all_post$productive <- as.character(contigs_all_post$productive) +} + +# scRepertoire::combineTCR() parses contig rows positionally, not by column +# name. Cell Ranger's natural filtered_contig_annotations.csv column order +# (barcode, is_cell, contig_id, high_confidence, length, chain, ...) makes it +# silently produce zero cells for every sample - no error, just an empty +# result that only surfaces much later as a confusing "column doesn't exist" +# failure downstream. Reorder so the fields it actually needs come first. +canonical_order <- c("barcode", "sample", "chain", "cdr3", "cdr3_nt", + "v_gene", "j_gene", "d_gene", "c_gene", + "productive", "raw_clonotype_id") +front <- intersect(canonical_order, colnames(contigs_all_post)) +rest <- setdiff(colnames(contigs_all_post), front) +contigs_all_post <- contigs_all_post[, c(front, rest)] + +head(contigs_all_post) + +``` + +## SCrepertoire prepare +```{r} +#| label: prepare-screpertoire +if (isTRUE(params$filter_to_t_ab) && any(contigs_all_post$chain %in% c("TRG", "TRD"))) { + contigs_all_post <- dplyr::filter(contigs_all_post, chain %in% c("TRA", "TRB")) +} + +if (params$cells_mode == "T-GD") { + contigs_all_post <- dplyr::filter(contigs_all_post, chain %in% c("TRG", "TRD")) +} +if (params$cells_mode == "both") { + contigs_all_post <- dplyr::filter(contigs_all_post, chain %in% c("TRA", "TRB", "TRG", "TRD")) +} + +contig_list <- split(contigs_all_post, contigs_all_post$sample) +contig_list <- contig_list[vapply(contig_list, nrow, FUN.VALUE = integer(1)) > 0] +stopifnot(length(contig_list) > 0) + +required_cols_screp <- c("barcode", "chain", "cdr3", "v_gene", "j_gene") +bad <- names(contig_list)[!vapply(contig_list, function(df) all(required_cols_screp %in% names(df)), TRUE)] +if (length(bad)) stop("Missing required columns for scRepertoire in: ", paste(bad, collapse = ", ")) + +sample_vec <- names(contig_list) + +combined <- combineTCR_safe( + contig_list, + samples = sample_vec, + ID = if (params$combine_id == "") NULL else params$combine_id, + cells = params$cells_mode +) + +if (!isTRUE(params$keep_na_clonotypes)) { + combined <- dropNA_clonotypes(combined) +} + +save_rds_safe(combined, "scRepertoire_combined.rds") + +comb_cells <- vapply(combined, function(x) length(unique(as.character(x$barcode))), integer(1)) +comb_cells_tbl <- data.frame(sample = names(comb_cells), cells = comb_cells, stringsAsFactors = FALSE) +save_table_safe(comb_cells_tbl, "scRepertoire_cells_per_sample.tsv") +``` + +## barcode-preclean +```{r} +#| label: barcode-preclean +if (isTRUE(params$clean_duplicated_sample_prefix)) { + for (s in names(combined)) combined[[s]]$barcode <- clean_barcode(as.character(combined[[s]]$barcode)) + contigs_all_post$barcode <- clean_barcode(as.character(contigs_all_post$barcode)) +} + +seu_md <- seu@meta.data + +sample_col_seu <- first_existing_col(seu_md, params$sample_col, params$sample_candidates) +patient_col_seu <- first_existing_col(seu_md, params$patient_col, params$patient_candidates) +condition_col_seu <- first_existing_col(seu_md, params$condition_col, params$condition_candidates) +timepoint_col_seu <- first_existing_col(seu_md, params$timepoint_col, params$timepoint_candidates) +batch_col_seu <- first_existing_col(seu_md, params$batch_col, params$batch_candidates) +label_col_seu <- detect_label_col( + seu, + preferred = params$label_col, + candidates = params$label_candidates, + use_ident = params$use_ident_if_label_missing +) + +if (!"META_SAMPLE" %in% colnames(seu@meta.data) && !is.null(sample_col_seu)) seu$META_SAMPLE <- as.character(seu@meta.data[[sample_col_seu]]) +if (!"META_PATIENT" %in% colnames(seu@meta.data) && !is.null(patient_col_seu)) seu$META_PATIENT <- as.character(seu@meta.data[[patient_col_seu]]) +if (!"META_TIMECOND" %in% colnames(seu@meta.data) && !is.null(condition_col_seu)) seu$META_TIMECOND <- as.character(seu@meta.data[[condition_col_seu]]) +if (!"META_BATCH" %in% colnames(seu@meta.data) && !is.null(batch_col_seu)) seu$META_BATCH <- as.character(seu@meta.data[[batch_col_seu]]) +``` + +## Sample Matching Diagnostic + +```{r} +#| label: sample-matching-diagnostic +#| warning: false + +# VDJ samples (from filtered contigs) +vdj_samples <- sort(unique(as.character(contigs_all_post$sample))) + +# GEX samples (from Seurat metadata) +gex_samples <- if (!is.null(sample_col_seu)) { + sort(unique(as.character(seu@meta.data[[sample_col_seu]]))) +} else { + sort(unique(as.character(seu@meta.data[["orig.ident"]]))) +} + +matched_samples <- intersect(vdj_samples, gex_samples) +vdj_only_samples <- setdiff(vdj_samples, gex_samples) +gex_only_samples <- setdiff(gex_samples, vdj_samples) + +# Format helper: collapse to comma-separated string, or "none" +fmt <- function(x) if (length(x) == 0) "none" else paste(x, collapse = ", ") + +cat("=== Sample Matching Diagnostic ===\n\n") +cat(sprintf("VDJ samples (%d): %s\n", length(vdj_samples), fmt(vdj_samples))) +cat(sprintf("GEX samples (%d): %s\n", length(gex_samples), fmt(gex_samples))) +cat("\n") +cat(sprintf("Matched (%d): %s\n", length(matched_samples), fmt(matched_samples))) +cat(sprintf("VDJ-only (%d): %s\n", length(vdj_only_samples), fmt(vdj_only_samples))) +cat(sprintf("GEX-only (%d): %s\n", length(gex_only_samples), fmt(gex_only_samples))) +cat("\n") + +if (length(matched_samples) == 0) { + stop("No VDJ samples match any GEX sample. Check that sample names are consistent between contigs_after_qc.tsv and the Seurat object.") +} + +cat(sprintf(">>> Proceeding with %d matched sample(s): %s\n", length(matched_samples), fmt(matched_samples))) + +if (length(vdj_only_samples) > 0) { + cat(sprintf("\nWARNING: %d VDJ sample(s) have no matching GEX data and will be excluded: %s\n", + length(vdj_only_samples), fmt(vdj_only_samples))) +} +if (length(gex_only_samples) > 0) { + cat(sprintf("\nNOTE: %d GEX sample(s) have no VDJ data. TCR metrics will be NA for these cells: %s\n", + length(gex_only_samples), fmt(gex_only_samples))) +} + +# Build a summary table for display +sample_match_tbl <- data.frame( + sample = c(matched_samples, vdj_only_samples, gex_only_samples), + has_vdj = c(rep(TRUE, length(matched_samples)), + rep(TRUE, length(vdj_only_samples)), + rep(FALSE, length(gex_only_samples))), + has_gex = c(rep(TRUE, length(matched_samples)), + rep(FALSE, length(vdj_only_samples)), + rep(TRUE, length(gex_only_samples))), + status = c(rep("Matched", length(matched_samples)), + rep("VDJ only", length(vdj_only_samples)), + rep("GEX only", length(gex_only_samples))), + stringsAsFactors = FALSE +) +sample_match_tbl <- sample_match_tbl[order(sample_match_tbl$status, sample_match_tbl$sample), ] +knitr::kable(sample_match_tbl, row.names = FALSE, + caption = "Sample presence in VDJ and GEX data") +``` + +## overlap-preharm +```{r} +#| label: overlap-preharm +tcr_barcodes_pre <- unique(unlist(lapply(combined, `[[`, "barcode"))) +overlap_pre <- length(intersect(tcr_barcodes_pre, colnames(seu))) +total_pre <- length(tcr_barcodes_pre) +overlap_rate_pre <- ifelse(total_pre > 0, overlap_pre / total_pre, NA_real_) + +harmonizer_score <- score_harmonizer_candidates( + combined, + seu, + seu_sample_col = sample_col_seu %||% "orig.ident" +) + +save_table_safe(harmonizer_score$table, "barcode_harmonizer_candidates.tsv") +head(harmonizer_score$table) + +selected_transform <- harmonizer_score$selected +writeLines(as.character(selected_transform), file.path(params$outdir, params$tables_dir, "barcode_harmonizer_selected.txt")) +head(as.character(selected_transform)) +``` + +## harmonization-apply +```{r} +#| label: harmonization-apply +if (isTRUE(params$barcode_harmonization_required) && + length(tcr_barcodes_pre) > 0 && + overlap_rate_pre < params$harmonization_overlap_threshold) { + + hz <- harmonize_barcodes( + combined, + seu, + seu_sample_col = sample_col_seu %||% "orig.ident", + apply_to = params$harmonize_apply_to, + verbose = FALSE + ) + + if (params$harmonize_apply_to == "tcr") { + combined <- hz$combined + selected_transform <- hz$transformed + } else { + seu <- hz$seurat + selected_transform <- hz$transformed + } + + save_table_safe(hz$table, "barcode_harmonizer_candidates_applied.tsv") + writeLines(as.character(selected_transform), file.path(params$outdir, params$tables_dir, "barcode_harmonizer_applied.txt")) +} +``` + +## overlap-postharm +```{r} +#| label: overlap-postharm +tcr_barcodes <- unique(unlist(lapply(combined, `[[`, "barcode"))) +seu_cells <- colnames(seu) +final_overlap <- length(intersect(tcr_barcodes, seu_cells)) +total_tcr <- length(tcr_barcodes) +final_overlap_rate <- ifelse(total_tcr > 0, final_overlap / total_tcr, NA_real_) + +if (length(tcr_barcodes) > 0) { + stopifnot(final_overlap_rate >= params$minimum_final_overlap_fraction) +} + +barcode_overlap_summary <- data.frame( + stage = c("pre_harmonization", "post_harmonization"), + tcr_unique_barcodes = c(total_pre, total_tcr), + overlap_with_seurat = c(overlap_pre, final_overlap), + overlap_rate = c(overlap_rate_pre, final_overlap_rate), + stringsAsFactors = FALSE +) +save_table_safe(barcode_overlap_summary, "barcode_overlap_summary.tsv") +``` + +## overlap-by-sample +```{r} +#| label: overlap-by-sample +barcode_overlap_by_sample <- rbindlist(lapply(names(combined), function(sid) { + bcs <- unique(as.character(combined[[sid]]$barcode)) + ov <- sum(bcs %in% seu_cells) + data.table( + sample = sid, + tcr_barcodes = length(bcs), + overlap = ov, + unmapped = length(bcs) - ov, + overlap_rate = ifelse(length(bcs) > 0, ov / length(bcs), NA_real_) + ) +})) +save_table_safe(barcode_overlap_by_sample, "barcode_overlap_by_sample.tsv") +``` + +## meta-key-audit +```{r} +#| label: meta-key-audit +md <- seu@meta.data + +meta_key_audit <- data.frame( + key = c("label", "sample", "patient", "condition", "timepoint", "batch"), + column = c( + label_col_seu %||% NA_character_, + sample_col_seu %||% NA_character_, + patient_col_seu %||% NA_character_, + condition_col_seu %||% NA_character_, + timepoint_col_seu %||% NA_character_, + batch_col_seu %||% NA_character_ + ), + n_unique = c( + if (!is.null(label_col_seu)) length(unique(md[[label_col_seu]])) else NA_integer_, + if (!is.null(sample_col_seu)) length(unique(md[[sample_col_seu]])) else NA_integer_, + if (!is.null(patient_col_seu)) length(unique(md[[patient_col_seu]])) else NA_integer_, + if (!is.null(condition_col_seu)) length(unique(md[[condition_col_seu]])) else NA_integer_, + if (!is.null(timepoint_col_seu)) length(unique(md[[timepoint_col_seu]])) else NA_integer_, + if (!is.null(batch_col_seu)) length(unique(md[[batch_col_seu]])) else NA_integer_ + ), + stringsAsFactors = FALSE +) +save_table_safe(meta_key_audit, "meta_key_choices.tsv") +``` + +## build-per-cell-tcr +```{r} +#| label: build-per-cell-tcr +dt <- as.data.table(contigs_all_post) + +for (cc in c("sample", "barcode", "chain", "cdr3", "v_gene", "j_gene", "raw_clonotype_id")) { + if (cc %in% names(dt)) set(dt, j = cc, value = as.character(dt[[cc]])) +} + +alpha_chains <- c("TRA") +beta_chains <- c("TRB") + +alpha_by_cell <- dt[chain %in% alpha_chains & !is.na(cdr3), + .(cdr3a = cdr3[1L], trav = v_gene[1L], traj = j_gene[1L]), + by = .(sample, barcode) +] +beta_by_cell <- dt[chain %in% beta_chains & !is.na(cdr3), + .(cdr3b = cdr3[1L], trbv = v_gene[1L], trbj = j_gene[1L]), + by = .(sample, barcode) +] + +per_cell <- merge(alpha_by_cell, beta_by_cell, by = c("sample", "barcode"), all = TRUE) + +per_cell[, CTaa := fifelse( + !is.na(cdr3a) & !is.na(cdr3b), paste0("A:", cdr3a, "|B:", cdr3b), + fifelse(!is.na(cdr3b), paste0("B:", cdr3b), + fifelse(!is.na(cdr3a), paste0("A:", cdr3a), NA_character_) + ) +)] + +clone_source <- dt[, .(clone_id_raw = raw_clonotype_id[1L]), by = .(sample, barcode)] +per_cell <- merge(per_cell, clone_source, by = c("sample", "barcode"), all.x = TRUE) +``` + +## evaluate-merge-strategies +```{r} +#| label: evaluate-merge-strategies +seu_md_dt <- as.data.table(seu@meta.data, keep.rownames = "cell_id") +seu_md_dt[, cell_id := as.character(cell_id)] + +if (!"META_SAMPLE" %in% colnames(seu_md_dt)) { + if (!is.null(sample_col_seu)) { + seu_md_dt[, META_SAMPLE := as.character(get(sample_col_seu))] + } else { + seu_md_dt[, META_SAMPLE := NA_character_] + } +} + +seu_k1 <- seu_md_dt[, .(cell_id, META_SAMPLE, key = cell_id)] +seu_k1[, strategy := "cell_id==barcode"] +tcr_k1 <- per_cell[, .(key = barcode, sample, barcode)] + +seu_k2 <- copy(seu_md_dt) +seu_k2[, key := sub("^[^_]+_", "", cell_id)] +seu_k2 <- seu_k2[, .(cell_id, META_SAMPLE, key)] +seu_k2[, strategy := "strip_prefix_before_underscore"] +tcr_k2 <- tcr_k1 + +seu_k3 <- seu_md_dt[, .(cell_id, META_SAMPLE, key = cell_id)] +seu_k3[, strategy := "cell_id==SAMPLE_BARCODE"] +tcr_k3 <- copy(per_cell)[, key := paste(sample, barcode, sep = "_")] +tcr_k3 <- tcr_k3[, .(key, sample, barcode)] + +seu_k4 <- seu_md_dt[, .(cell_id, META_SAMPLE, key = paste(META_SAMPLE, cell_id, sep = "_"))] +seu_k4[, strategy := "META_SAMPLE+cell_id==sample+barcode"] +tcr_k4 <- tcr_k3 + +eval_strategy <- function(seu_tbl, tcr_tbl) { + s <- unique(seu_tbl$key) + t <- unique(tcr_tbl$key) + length(intersect(s, t)) +} + +counts <- data.table( + strategy = c(seu_k1$strategy[1], seu_k2$strategy[1], seu_k3$strategy[1], seu_k4$strategy[1]), + matches = c( + eval_strategy(seu_k1, tcr_k1), + eval_strategy(seu_k2, tcr_k2), + eval_strategy(seu_k3, tcr_k3), + eval_strategy(seu_k4, tcr_k4) + ) +) +setorder(counts, -matches) +best <- counts$strategy[1] + +idx3 <- which(counts$strategy == "cell_id==SAMPLE_BARCODE") +if (length(idx3) && counts$matches[idx3] >= 0.98 * counts$matches[1] && + best != "cell_id==SAMPLE_BARCODE") { + best <- "cell_id==SAMPLE_BARCODE" +} + +pick <- function(s) switch( + s, + "cell_id==barcode" = list(seu = seu_k1, tcr = tcr_k1), + "strip_prefix_before_underscore" = list(seu = seu_k2, tcr = tcr_k2), + "cell_id==SAMPLE_BARCODE" = list(seu = seu_k3, tcr = tcr_k3), + "META_SAMPLE+cell_id==sample+barcode" = list(seu = seu_k4, tcr = tcr_k4) +) + +pt <- pick(best) +seu_tbl <- pt$seu +tcr_tbl <- pt$tcr + +n_seu_keys <- length(unique(seu_md_dt$cell_id)) +n_tcr_keys <- nrow(unique(per_cell[, .(sample, barcode)])) +best_matches <- eval_strategy(seu_tbl, tcr_tbl) +theoretical_max_pct <- 100 * n_tcr_keys / n_seu_keys + +diag_tbl <- copy(counts) +diag_tbl[, total_cells := n_seu_keys] +diag_tbl[, total_vdj := n_tcr_keys] +diag_tbl[, pct_cells_matched := round(100 * matches / total_cells, 2)] +diag_tbl[, pct_tcr_matched := round(100 * matches / total_vdj, 2)] + +save_table_safe(diag_tbl, "tcr_merge_match_diagnostics.tsv") +``` + +## per-sample-coverage +```{r} +#| label: per-sample-coverage +matched_keys <- intersect(unique(seu_tbl$key), unique(tcr_tbl$key)) + +seu_sample_raw <- seu@meta.data$META_SAMPLE %||% seu@meta.data[[sample_col_seu]] +seu_sample_base <- sub("\\.\\d+$", "", as.character(seu_sample_raw)) +seu_by_samp <- data.table(sample = seu_sample_base)[, .N, by = sample] +setnames(seu_by_samp, "N", "gex_cells") + +tcr_by_samp <- unique(per_cell[, .(sample, barcode)])[, .N, by = sample] +setnames(tcr_by_samp, "N", "tcr_cells") + +matched_by_samp <- tcr_tbl[key %in% matched_keys, .N, by = sample] +setnames(matched_by_samp, "N", "matched_keys") + +per_sample_cov <- Reduce(function(x, y) merge(x, y, by = "sample", all = TRUE), + list(seu_by_samp, tcr_by_samp, matched_by_samp)) + +for (cc in c("gex_cells", "tcr_cells", "matched_keys")) { + if (!cc %in% names(per_sample_cov)) per_sample_cov[[cc]] <- 0L +} +per_sample_cov[is.na(gex_cells), gex_cells := 0L] +per_sample_cov[is.na(tcr_cells), tcr_cells := 0L] +per_sample_cov[is.na(matched_keys), matched_keys := 0L] + +per_sample_cov[, `:=`( + gex_cov_pct = ifelse(gex_cells > 0, round(100 * matched_keys / gex_cells, 2), 0), + tcr_cov_pct = ifelse(tcr_cells > 0, round(100 * matched_keys / tcr_cells, 2), 0) +)] + +save_table_safe(per_sample_cov, "tcr_merge_per_sample_coverage.tsv") +``` + +## final-merge-to-seurat +```{r} +#| label: final-merge-to-seurat +tcr_annot <- merge( + per_cell, + tcr_tbl[, .(key, sample, barcode)], + by = c("sample", "barcode"), + all.y = TRUE +) + +bring <- c("CTaa", "cdr3a", "cdr3b", "trav", "traj", "trbv", "trbj", + "clone_id_raw", "patient_id", "condition", "timepoint") + +for (col in bring) if (!col %in% names(tcr_annot)) tcr_annot[[col]] <- NA_character_ + +seu_join <- merge( + seu_tbl[, .(cell_id, META_SAMPLE, key)], + tcr_annot[, c("key", bring), with = FALSE], + by = "key", + all.x = TRUE +) + +setkey(seu_join, cell_id) +md_dt <- as.data.table(seu@meta.data, keep.rownames = "cell_id") +setkey(md_dt, cell_id) +md_dt <- seu_join[md_dt] + +# Map metadata to internal keys +if (has_col(md_dt, "patient_id")) md_dt[, META_PATIENT := coalesce(as.character(META_PATIENT), as.character(patient_id))] +if (has_col(md_dt, "condition")) md_dt[, META_TIMECOND := coalesce(as.character(META_TIMECOND), as.character(condition))] +if (has_col(md_dt, "timepoint")) md_dt[, META_TIMECOND := coalesce(as.character(META_TIMECOND), as.character(timepoint))] + +md_dt[, clone_id := CTaa] +md_dt[, has_tcr := !is.na(clone_id)] +md_dt[, clone_size := ifelse(is.na(clone_id), 0L, .N), by = clone_id] +md_dt[, paired_tcr := !is.na(cdr3a) & !is.na(cdr3b)] + +# ROBUST BINNING: Always create clone_size_bin +md_dt[, clone_size_bin := NA_character_] +if (any(md_dt$clone_size > 0)) { + try({ + qs <- unique(quantile(md_dt$clone_size[md_dt$clone_size > 0], probs = params$clone_bins_quantiles, na.rm = TRUE)) + if (length(qs) >= 2) { + md_dt[clone_size > 0, clone_size_bin := as.character(cut(clone_size, breaks = qs, include.lowest = TRUE))] + } else { + md_dt[clone_size > 0, clone_size_bin := as.character(clone_size)] + } + }, silent = TRUE) +} + +rn <- md_dt$cell_id +md_dt$cell_id <- NULL +seu@meta.data <- as.data.frame(md_dt) +rownames(seu@meta.data) <- rn + +save_rds_safe(seu, "seurat_with_TCR_integrated.rds") + +``` + +## screpertoire-merge +```{r} +#| label: screpertoire-merge-optional + +screp_combined_expression_ok <- FALSE +seu_screp <- seu +screp_error_msg <- NA_character_ +screp_note <- NA_character_ + +message("--- Attempting optional scRepertoire::combineExpression integration ---") + + +rebuild_clean_seurat_for_screp <- function(obj) { + suppressPackageStartupMessages({ + library(Seurat) + library(SeuratObject) + library(Matrix) + }) + + `%||%` <- function(a, b) if (!is.null(a)) a else b + + get_any_matrix <- function(x, assay_name) { + out <- tryCatch(Seurat::GetAssayData(x, assay = assay_name, slot = "counts"), error = function(e) NULL) + if (!is.null(out) && nrow(out) > 0 && ncol(out) > 0) return(out) + + out <- tryCatch(Seurat::GetAssayData(x, assay = assay_name, slot = "data"), error = function(e) NULL) + if (!is.null(out) && nrow(out) > 0 && ncol(out) > 0) return(out) + + lyr_names <- tryCatch(SeuratObject::Layers(x[[assay_name]]), error = function(e) NULL) + if (!is.null(lyr_names) && length(lyr_names) > 0) { + for (lyr in c("counts", "data", lyr_names)) { + out <- tryCatch(SeuratObject::LayerData(x[[assay_name]], layer = lyr), error = function(e) NULL) + if (!is.null(out) && nrow(out) > 0 && ncol(out) > 0) return(out) + } + } + + NULL + } + + assay_names <- names(obj@assays) + assay_priority <- unique(c(DefaultAssay(obj), "RNA", assay_names)) + + mat <- NULL + chosen_assay <- NULL + + for (aa in assay_priority) { + if (!aa %in% assay_names) next + candidate <- get_any_matrix(obj, aa) + if (!is.null(candidate)) { + mat <- candidate + chosen_assay <- aa + break + } + } + + if (is.null(mat)) { + stop("Could not recover a usable assay matrix for scRepertoire repair.") + } + + if (!inherits(mat, "dgCMatrix")) { + mat <- tryCatch(as(mat, "dgCMatrix"), error = function(e) Matrix::Matrix(as.matrix(mat), sparse = TRUE)) + } + + md <- obj@meta.data[colnames(mat), , drop = FALSE] + + seu_new <- Seurat::CreateSeuratObject( + counts = mat, + assay = chosen_assay, + meta.data = md + ) + + DefaultAssay(seu_new) <- chosen_assay + + old_ids <- tryCatch(Idents(obj), error = function(e) NULL) + if (!is.null(old_ids)) { + old_ids <- as.character(old_ids) + names(old_ids) <- names(Idents(obj)) + ids_vec <- old_ids[colnames(seu_new)] + names(ids_vec) <- colnames(seu_new) + Idents(seu_new) <- factor(ids_vec) + } + + validObject(seu_new) + seu_new +} + +combineExpression_safe <- function(combined_obj, seu_obj, clone_call = "aa", add_label = FALSE) { + fun <- get("combineExpression", asNamespace("scRepertoire")) + fml <- names(formals(fun)) + + args <- list( + combined_obj, + seu_obj + ) + + if ("cloneCall" %in% fml) args$cloneCall <- clone_call + if ("proportion" %in% fml) args$proportion <- FALSE + if ("addLabel" %in% fml) args$addLabel <- add_label + if ("filterMode" %in% fml) args$filterMode <- "union" + if ("cloneSize" %in% fml) { + args$cloneSize <- c(Single = 1, Small = 5, Medium = 20, Large = 100, Hyperexpanded = 500) + } + + do.call(fun, args) +} + +tryCatch({ + + combined_harmonized <- lapply(names(combined), function(sid) { + df <- combined[[sid]] + + if (best == "cell_id==SAMPLE_BARCODE") { + df$barcode <- paste(sid, df$barcode, sep = "_") + } else if (best == "META_SAMPLE+cell_id==sample+barcode") { + df$barcode <- df$barcode + } else if (best == "strip_prefix_before_underscore") { + df$barcode <- df$barcode + } else { + df$barcode <- df$barcode + } + + df$barcode <- as.character(df$barcode) + df + }) + names(combined_harmonized) <- names(combined) + + # rebuild minimal valid Seurat object + seu_screp <- rebuild_clean_seurat_for_screp(seu_screp) + + if (inherits(seu_screp[[DefaultAssay(seu_screp)]], "Assay5")) { + seu_screp <- JoinLayers(seu_screp) + } + + # IMPORTANT: rename Seurat cells to exact barcode namespace expected by scRepertoire + all_tcr_barcodes <- unique(unlist(lapply(combined_harmonized, function(x) as.character(x$barcode)))) + seurat_cells <- colnames(seu_screp) + + make_target_names <- function(cells, sample_vec, strategy) { + if (strategy == "cell_id==SAMPLE_BARCODE") { + return(paste(sample_vec, cells, sep = "_")) + } else if (strategy == "META_SAMPLE+cell_id==sample+barcode") { + return(paste(sample_vec, cells, sep = "_")) + } else { + return(cells) + } + } + + sample_for_cells <- NULL + if ("META_SAMPLE" %in% colnames(seu_screp@meta.data)) { + sample_for_cells <- as.character(seu_screp@meta.data$META_SAMPLE) + } else if (!is.null(sample_col_seu) && sample_col_seu %in% colnames(seu_screp@meta.data)) { + sample_for_cells <- as.character(seu_screp@meta.data[[sample_col_seu]]) + } else { + sample_for_cells <- rep(NA_character_, ncol(seu_screp)) + } + + candidate_names <- make_target_names(seurat_cells, sample_for_cells, best) + + overlap_now <- sum(candidate_names %in% all_tcr_barcodes) + overlap_identity <- sum(seurat_cells %in% all_tcr_barcodes) + + if (overlap_now > overlap_identity) { + colnames(seu_screp) <- candidate_names + } + + # keep only exact overlap cells to avoid scRepertoire metadata write-back issues + common_cells <- intersect(colnames(seu_screp), all_tcr_barcodes) + if (length(common_cells) == 0) { + stop("No overlapping cell names between Seurat object and combined_harmonized barcodes after harmonization.") + } + + seu_screp <- subset(seu_screp, cells = common_cells) + + # final identity repair after subset + ids_vec <- as.character(Idents(seu_screp)) + names(ids_vec) <- colnames(seu_screp) + Idents(seu_screp) <- factor(ids_vec) + + validObject(seu_screp) + + seu_screp <- combineExpression_safe( + combined_harmonized, + seu_screp, + clone_call = switch( + params$clone_call_preference, + aa = "aa", + nt = "nt", + strict = "strict", + gene = "gene", + "aa" + ), + add_label = FALSE + ) + + added_cols <- intersect( + c("CTaa", "CTnt", "CTstrict", "CTgene", "strict", "aa", "nt", "gene"), + colnames(seu_screp@meta.data) + ) + + if (length(added_cols) > 0) { + screp_combined_expression_ok <- TRUE + screp_note <- paste("combineExpression succeeded; detected columns:", paste(added_cols, collapse = ", ")) + message("SUCCESS: scRepertoire integration complete.") + save_rds_safe(seu_screp, "seurat_with_scRepertoire_combineExpression.rds") + } else { + screp_error_msg <- "combineExpression returned without error, but no expected scRepertoire clonotype columns were added to Seurat metadata." + screp_note <- "Optional scRepertoire path returned no usable metadata columns." + message("WARNING: scRepertoire ran but did not add expected metadata columns.") + } + +}, error = function(e) { + screp_error_msg <<- conditionMessage(e) + screp_note <<- "Optional scRepertoire path failed; validated fast-merge outputs remain available." + message("scRepertoire Error: ", screp_error_msg) +}) + +screp_diag_tbl <- tibble::tibble( + metric = c( + "scRepertoire combineExpression status", + "selected_merge_strategy", + "selected_barcode_transform", + "clone_call_preference", + "tcr_barcodes_post_harmonization", + "seurat_cells_before_subset", + "final_overlap", + "final_overlap_rate", + "scRepertoire_note", + "scRepertoire_error" + ), + value = c( + ifelse(screp_combined_expression_ok, "SUCCESS", "FAILED_OR_EMPTY"), + as.character(best), + as.character(selected_transform), + as.character(params$clone_call_preference), + as.character(length(tcr_barcodes)), + as.character(ncol(seu)), + as.character(final_overlap), + sprintf("%.4f", final_overlap_rate), + as.character(screp_note %||% ""), + as.character(screp_error_msg %||% "") + ) +) + +save_table_safe(screp_diag_tbl, "scRepertoire_combineExpression_diagnostics.tsv") + +if (isTRUE(screp_combined_expression_ok)) { + cat("scRepertoire::combineExpression completed successfully.\n") +} else { + cat("scRepertoire::combineExpression did not produce usable output.\n") + if (!is.na(screp_error_msg) && nzchar(screp_error_msg)) { + cat("Reason:\n") + cat(screp_error_msg, "\n") + } + cat("The pipeline continued using the validated fast-merge TCR↔GEX integration outputs.\n") +} +``` + + +## subset-tcells +```{r} +#| label: subset-tcells + + +seu_t <- repair_seurat_for_subset(seu) + +if (isTRUE(params$subset_tcells)) { + seu_t <- subset_tcells_safe( + seu_t, + label_col = label_col_seu, + regex = params$tcell_regex, + fallback_ident = params$use_ident_if_label_missing + ) +} + +reduction_to_use <- params$reduction_use +if (!(reduction_to_use %in% names(seu_t@reductions)) && isTRUE(params$make_umap_if_missing)) { + seu_t <- safe_make_umap( + seu_t, + reduction_name = params$reduction_use, + dims = 1:params$umap_dims_max, + nfeatures = params$umap_nfeatures + ) +} +if (!(reduction_to_use %in% names(seu_t@reductions))) { + reduction_to_use <- if ("umap" %in% names(seu_t@reductions)) "umap" else if ("tsne" %in% names(seu_t@reductions)) "tsne" else if ("pca" %in% names(seu_t@reductions)) "pca" else stop("No usable embedding found.") +} + +``` + +### tcell-clone-size +```{r} +#| label: tcell-clone-size +if (!"clone_size" %in% colnames(seu_t@meta.data)) { + seu_t$clone_size <- 0L +} else { + seu_t$clone_size[is.na(seu_t$clone_size)] <- 0L +} + +save_rds_safe(seu_t, "seurat_tcells_with_TCR.rds") +``` + +## tcell-summaries +```{r} +#| label: tcell-summaries +sample_col_export <- choose_col(seu_t@meta.data, "META_SAMPLE", sample_col_seu) +patient_col_export <- choose_col(seu_t@meta.data, "META_PATIENT", patient_col_seu) +condition_col_export <- choose_col(seu_t@meta.data, "META_TIMECOND", condition_col_seu) +timepoint_col_export <- choose_col(seu_t@meta.data, "META_TIMECOND", timepoint_col_seu) + +umap_df <- as.data.frame(Embeddings(seu_t, reduction_to_use)) %>% + tibble::rownames_to_column("cell_id") + +export_cells <- seu_t@meta.data %>% + tibble::rownames_to_column("cell_id") %>% + dplyr::transmute( + cell_id, + sample = if (!is.null(sample_col_export)) .data[[sample_col_export]] else NA_character_, + patient = if (!is.null(patient_col_export)) .data[[patient_col_export]] else NA_character_, + condition = if (!is.null(condition_col_export)) .data[[condition_col_export]] else NA_character_, + annot = .data[[label_col_seu]], + CTaa = dplyr::coalesce(.data[["CTaa"]], NA_character_), + clone_id = dplyr::coalesce(.data[["clone_id"]], NA_character_), + clone_size = as.integer(.data[["clone_size"]]), + clone_size_bin = .data[["clone_size_bin"]], + paired_tcr = .data[["paired_tcr"]], + has_tcr = .data[["has_tcr"]] + ) %>% + left_join(umap_df, by = "cell_id") + +extra_cols <- intersect(c("cdr3a", "cdr3b", "trav", "trbv", "traj", "trbj"), colnames(seu_t@meta.data)) +if (length(extra_cols) > 0) { + export_cells <- export_cells %>% + left_join( + seu_t@meta.data %>% + tibble::rownames_to_column("cell_id") %>% + dplyr::select(dplyr::all_of(c("cell_id", extra_cols))), + by = "cell_id" + ) +} +save_table_safe(export_cells, "tcr_export_cells_with_embedding.tsv") + +tcell_per_annotation <- export_cells %>% + count(annot, has_tcr, paired_tcr, name = "n_cells") %>% + group_by(annot) %>% + mutate(frac = n_cells / sum(n_cells)) %>% + ungroup() +save_table_safe(tcell_per_annotation, "tcell_per_annotation_summary.tsv") + +top_clones_tbl <- export_cells %>% + filter(!is.na(clone_id), clone_id != "", clone_size >= params$min_clone_size_plot) %>% + count(sample, annot, clone_id, clone_size, name = "n_cells") %>% + arrange(desc(clone_size), desc(n_cells)) %>% + slice_head(n = params$top_n_clone_table) +save_table_safe(top_clones_tbl, "tcell_top_clones.tsv") + +clone_rank_tbl <- export_cells %>% + filter(!is.na(clone_id), clone_id != "", clone_size >= params$min_clone_size_plot) %>% + count(sample, clone_id, name = "n_cells") %>% + group_by(sample) %>% + arrange(desc(n_cells), .by_group = TRUE) %>% + mutate(rank = row_number()) %>% + ungroup() +save_table_safe(clone_rank_tbl, "tcell_clone_rank_abundance.tsv") + +clone_state_tbl <- export_cells %>% + filter(!is.na(clone_id), clone_id != "") %>% + count(clone_id, annot, name = "n_cells") +save_table_safe(clone_state_tbl, "tcell_clone_state_occupancy.tsv") + +summary_rollup <- tibble( + metric = c( + "TCR barcodes pre-harmonization", + "Pre-harmonization overlap", + "Post-harmonization overlap", + "Final overlap rate", + "Selected merge strategy", + "T-cell subset cells", + "TCR-positive T cells", + "Paired TCR T cells" + ), + value = c( + total_pre, + overlap_pre, + final_overlap, + sprintf("%.2f%%", 100 * final_overlap_rate), + best, + ncol(seu_t), + sum(seu_t$has_tcr, na.rm = TRUE), + sum(seu_t$paired_tcr, na.rm = TRUE) + ) +) +save_table_safe(summary_rollup, "tcell_summary_rollup.tsv") +``` + +## overview +```{r} +#| label: overview +overview_tbl <- tibble( + Metric = c( + "Report label", + "Input Seurat cells", + "Input filtered contigs", + "Samples represented in VDJ", + "Annotation column used", + "Sample column used", + "Selected harmonization transform", + "Selected merge strategy", + "Final barcode overlap rate", + "T-cell subset cells", + "TCR-positive T cells", + "Paired TCR T cells" + ), + Value = c( + params$report_label, + comma(ncol(seu)), + comma(nrow(contigs_all_post)), + comma(length(unique(contigs_all_post$sample))), + label_col_seu, + sample_col_seu %||% "NA", + selected_transform, + best, + sprintf("%.2f%%", 100 * final_overlap_rate), + comma(ncol(seu_t)), + comma(sum(seu_t$has_tcr, na.rm = TRUE)), + comma(sum(seu_t$paired_tcr, na.rm = TRUE)) + ) +) + +kable(overview_tbl, caption = "High-level overview of GEX–TCR integration.") %>% + kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed")) +``` + +## harmonization-figures +```{r} +#| label: harmonization-figures +if (isTRUE(params$show_barcode_harmonization)) { + p_overlap <- ggplot(barcode_overlap_by_sample, + aes(x = reorder(sample, overlap_rate), y = overlap_rate)) + + geom_col(fill = "steelblue") + + coord_flip() + + scale_y_continuous(labels = percent_format(accuracy = 1)) + + labs( + title = "TCR to Seurat Barcode Overlap by Sample", + subtitle = "Post-harmonization overlap rate.", + x = NULL, + y = "Overlap rate" + ) + + theme_scratch_pub(params$base_size) + + p_map_stack <- barcode_overlap_by_sample %>% + dplyr::select(sample, overlap, unmapped) %>% + tidyr::pivot_longer(cols = c(overlap, unmapped), names_to = "status", values_to = "n") %>% + mutate(status = recode( + status, + overlap = "Mapped to Seurat", + unmapped = "Not found in Seurat" + )) %>% + ggplot(aes(x = reorder(sample, n), y = n, fill = status)) + + geom_col() + + coord_flip() + + scale_y_continuous(labels = comma) + + labs( + title = "Mapped vs Unmapped TCR Barcodes", + x = NULL, + y = "TCR barcodes", + fill = NULL + ) + + theme_scratch_pub(params$base_size) + + p_scatter <- ggplot(barcode_overlap_by_sample, + aes(x = tcr_barcodes, y = overlap_rate)) + + geom_point(size = 2) + + scale_y_continuous(labels = percent_format(accuracy = 1)) + + labs( + title = "Overlap Rate vs TCR Barcode Count", + x = "Unique TCR barcodes per sample", + y = "Overlap rate" + ) + + theme_scratch_pub(params$base_size) + + (p_overlap | p_map_stack) / p_scatter + save_plot_safe(p_overlap, glue("barcode_overlap_by_sample.{params$figure_format}")) + save_plot_safe(p_map_stack, glue("barcode_mapping_mapped_unmapped.{params$figure_format}")) + save_plot_safe(p_scatter, glue("barcode_overlap_vs_tcr_count.{params$figure_format}")) +} +``` + +## umap-annotation +```{r} +#| label: umap-annotation + + +# 1. Get the raw labels and handle NAs immediately +raw_labels <- as.character(seu_t@meta.data[[label_col_seu]]) +raw_labels[is.na(raw_labels) | raw_labels == ""] <- "Unannotated" + +# 2. Create the unique levels (sorted) +annot_levels <- sort(unique(raw_labels)) + +# 3. Create a clean numeric mapping +annot_map <- setNames(seq_along(annot_levels), annot_levels) + +# 4. Map the labels to numbers and assign to Seurat using AddMetaData (safer than $) +plot_numbers <- annot_map[raw_labels] +names(plot_numbers) <- rownames(seu_t@meta.data) # CRITICAL: Align barcodes + +seu_t <- AddMetaData(seu_t, metadata = plot_numbers, col.name = "plot_number") + +# 5. Build the legend text (e.g., "1: CD4 T cells") +legend_labels <- paste0(seq_along(annot_levels), ": ", annot_levels) + +p_umap_annot <- DimPlot( + seu_t, + reduction = reduction_to_use, + group.by = "plot_number", + label = TRUE, + label.size = 6, + repel = TRUE, + raster = isTRUE(params$raster_large_umap) +) + + ggtitle("T-cell embedding: Numbered Annotations") + + theme_scratch_pub(params$base_size) + + # Map the numbers back to the long strings in the legend + scale_color_discrete(labels = legend_labels) + + guides(color = guide_legend(ncol = 1, override.aes = list(size = 4))) + +p_umap_annot +save_plot_safe(p_umap_annot, glue("umap_tcells_annotation_numbered.{params$figure_format}"), width = 12, height = 8) +# p_umap_annot <- DimPlot( +# seu_t, +# reduction = reduction_to_use, +# group.by = label_col_seu, +# label = params$label_clusters, +# repel = TRUE, +# raster = isTRUE(params$raster_large_umap) +# ) + +# ggtitle("T-cell embedding: annotation") + +# theme_scratch_pub(params$base_size) +# +# p_umap_annot +# save_plot_safe(p_umap_annot, glue("umap_tcells_annotation.{params$figure_format}"), width = 8, height = 6) +``` + +## umap-tcr-positive +```{r} +#| label: umap-tcr-positive +p_umap_tcr <- DimPlot( + seu_t, + reduction = reduction_to_use, + group.by = "has_tcr", + raster = isTRUE(params$raster_large_umap) +) + + ggtitle("T-cell embedding: TCR-positive vs TCR-negative") + + theme_scratch_pub(params$base_size) + +p_umap_tcr +save_plot_safe(p_umap_tcr, glue("umap_tcells_has_tcr.{params$figure_format}"), width = 8, height = 6) +``` + +## umap-paired +```{r} +#| label: umap-paired +p_umap_paired <- DimPlot( + seu_t, + reduction = reduction_to_use, + group.by = "paired_tcr", + raster = isTRUE(params$raster_large_umap) +) + + ggtitle("T-cell embedding: paired TCR status") + + theme_scratch_pub(params$base_size) + +p_umap_paired +save_plot_safe(p_umap_paired, glue("umap_tcells_paired_tcr.{params$figure_format}"), width = 8, height = 6) +``` + +## umap-clone-size +```{r} +#| label: umap-clone-size +p_umap_clone <- FeaturePlot( + seu_t, + reduction = reduction_to_use, + features = "clone_size", + raster = isTRUE(params$raster_large_umap) +) + + ggtitle("T-cell embedding: clone size") + + theme_scratch_pub(params$base_size) + +p_umap_clone +save_plot_safe(p_umap_clone, glue("umap_tcells_clone_size.{params$figure_format}"), width = 8, height = 6) +``` + +## pairing-summary +```{r} +#| label: pairing-summary + +pairing_data <- export_cells %>% + mutate(pairing_group = case_when( + paired_tcr ~ "Paired", + has_tcr ~ "Unpaired", + TRUE ~ "No TCR" + )) %>% + count(annot, pairing_group, name = "n_cells") + +# Always print the table first so something shows up in the report +kable(pairing_data, caption = "Pairing Status Table") %>% kable_styling() + +if (isTRUE(params$show_pairing_summary)) { + p_pairing <- pairing_data %>% + group_by(annot) %>% + mutate(frac = n_cells / sum(n_cells)) %>% + ggplot(aes(x = fct_reorder(annot, frac, .fun = max), y = frac, fill = pairing_group)) + + geom_col(position = "fill") + + coord_flip() + + scale_y_continuous(labels = percent_format(accuracy = 1)) + + theme_scratch_pub(params$base_size) + print(p_pairing) +} + +# if (isTRUE(params$show_pairing_summary)) { +# p_pairing <- export_cells %>% +# mutate(pairing_group = case_when( +# paired_tcr ~ "Paired", +# has_tcr ~ "Unpaired", +# TRUE ~ "No TCR" +# )) %>% +# count(annot, pairing_group, name = "n_cells") %>% +# group_by(annot) %>% +# mutate(frac = n_cells / sum(n_cells)) %>% +# ungroup() %>% +# ggplot(aes(x = fct_reorder(annot, frac, .fun = max), y = frac, fill = pairing_group)) + +# geom_col(position = "fill") + +# coord_flip() + +# scale_y_continuous(labels = percent_format(accuracy = 1)) + +# labs( +# title = "Pairing Status Across T-cell States", +# x = NULL, +# y = "Fraction of cells", +# fill = "TCR status" +# ) + +# theme_scratch_pub(params$base_size) +# +# p_pairing +# save_plot_safe(p_pairing, glue("pairing_status_by_annotation.{params$figure_format}"), width = 10, height = 7) +# } +``` + +## Clonal Homeostasis (Expansion) +```{r} +#| label: clonal-expansion-homeostasis +#| fig-width: 10 +#| fig-height: 7 + +homeo_df <- export_cells %>% + filter(!is.na(sample), sample != "", !is.na(clone_id), clone_id != "", !is.na(clone_size)) %>% + distinct(sample, clone_id, clone_size) %>% + mutate( + Expansion_Class = cut( + clone_size, + breaks = c(0, 1, 5, 20, 100, Inf), + labels = c("Rare (1)", "Small (2-5)", "Medium (6-20)", "Large (21-100)", "Hyperexpanded (>100)"), + right = TRUE + ) + ) %>% + count(sample, Expansion_Class, name = "n_clones") %>% + tidyr::complete(sample, Expansion_Class, fill = list(n_clones = 0)) %>% + group_by(sample) %>% + mutate(fraction = n_clones / sum(n_clones)) %>% + ungroup() + +sample_order_df <- homeo_df %>% + select(sample, Expansion_Class, fraction) %>% + distinct() %>% + tidyr::pivot_wider( + names_from = Expansion_Class, + values_from = fraction, + values_fill = 0 + ) %>% + distinct(sample, .keep_all = TRUE) %>% + arrange( + desc(`Hyperexpanded (>100)`), + desc(`Large (21-100)`), + desc(`Medium (6-20)`), + desc(`Small (2-5)`), + desc(`Rare (1)`) + ) + +sample_levels <- unique(as.character(sample_order_df$sample)) + +homeo_df <- homeo_df %>% + mutate( + sample = factor(as.character(sample), levels = sample_levels), + Expansion_Class = factor( + Expansion_Class, + levels = c("Rare (1)", "Small (2-5)", "Medium (6-20)", "Large (21-100)", "Hyperexpanded (>100)") + ) + ) + +p_homeo <- ggplot(homeo_df, aes(x = sample, y = fraction, fill = Expansion_Class)) + + geom_col(width = 0.8) + + scale_y_continuous(labels = scales::percent_format()) + + scale_fill_manual( + values = c( + "Rare (1)" = "#4575b4", + "Small (2-5)" = "#91bfdb", + "Medium (6-20)" = "#fee090", + "Large (21-100)" = "#fc8d59", + "Hyperexpanded (>100)" = "#d73027" + ) + ) + + theme_scratch_pub(params$base_size) + + labs( + title = "Clonal Expansion Homeostasis", + y = "Relative Abundance", + x = "Sample", + fill = "Expansion_Class" + ) + + theme( + axis.text.x = element_text(angle = 45, hjust = 1) + ) + +print(p_homeo) + +save_plot_safe( + p_homeo, + glue("clonal_homeostasis_expansion.{params$figure_format}"), + width = 10, + height = 7 +) +``` + + +## top-clone-overlays +```{r} +#| label: top-clone-overlays +if (isTRUE(params$show_top_clone_umaps)) { + top_clone_ids <- export_cells %>% + filter(!is.na(clone_id), clone_id != "") %>% + count(clone_id, name = "n_cells") %>% + arrange(desc(n_cells)) %>% + slice_head(n = params$top_n_clones_umap) %>% + pull(clone_id) + + if (length(top_clone_ids) > 0) { + seu_t$top_clone_label <- ifelse(seu_t$clone_id %in% top_clone_ids, seu_t$clone_id, "Other") + p_top_clones <- DimPlot( + seu_t, + reduction = reduction_to_use, + group.by = "top_clone_label", + raster = isTRUE(params$raster_large_umap) + ) + + ggtitle("Top expanded clonotypes on embedding") + + theme_scratch_pub(params$base_size) + + p_top_clones + save_plot_safe(p_top_clones, glue("umap_top_clones.{params$figure_format}"), width = 10, height = 8) + } +} +``` + +## clone-rank-abundance +```{r} +#| label: clone-rank-abundance +if (isTRUE(params$show_clone_rank_plot) && nrow(clone_rank_tbl) > 0) { + p_clone_rank <- ggplot(clone_rank_tbl, aes(x = rank, y = n_cells, color = sample)) + + geom_line(linewidth = 0.9, alpha = 0.8) + + geom_point(size = 1.2, alpha = 0.9) + + scale_x_log10() + + scale_y_log10() + + labs( + title = "Clone Rank-Abundance Curve", + subtitle = glue("Only clones with at least {params$min_clone_size_plot} cells are shown."), + x = "Clone rank (log10)", + y = "Cells per clone (log10)", + color = "Sample" + ) + + theme_scratch_pub(params$base_size) + + p_clone_rank + save_plot_safe(p_clone_rank, glue("clone_rank_abundance.{params$figure_format}")) +} +``` + +## clone-state-heatmap +```{r} +#| label: clone-state-heatmap +#| fig-width: 12 +#| fig-height: 8 + +if (isTRUE(params$show_clone_state_heatmap) && exists("clone_state_tbl") && nrow(clone_state_tbl) > 0) { + + # 1. Identify the top 30 most expanded clones to keep the heatmap readable + top_state_clones <- clone_state_tbl %>% + group_by(clone_id) %>% + summarise(total = sum(n_cells), .groups = "drop") %>% + arrange(desc(total)) %>% + slice_head(n = 30) %>% + pull(clone_id) + + # 2. Pivot data into a wide matrix format + clone_state_mat <- clone_state_tbl %>% + filter(clone_id %in% top_state_clones) %>% + tidyr::pivot_wider(names_from = annot, values_from = n_cells, values_fill = 0) %>% + as.data.frame() + + # 3. Set row names and convert to pure numeric matrix + rownames(clone_state_mat) <- clone_state_mat$clone_id + clone_state_mat$clone_id <- NULL + clone_state_mat <- as.matrix(clone_state_mat) + + # 4. Define a clean color scale + col_fun <- colorRamp2( + c(0, max(clone_state_mat, na.rm = TRUE) / 2, max(clone_state_mat, na.rm = TRUE)), + c("white", "gold", "firebrick") + ) + + # 5. Generate the Heatmap with optimized label sizes + ht_clone_state <- Heatmap( + clone_state_mat, + name = "Cells", + col = col_fun, + cluster_rows = TRUE, + cluster_columns = TRUE, + show_row_dend = TRUE, + show_column_dend = TRUE, + + # Visual Polish for Long Labels + row_names_gp = gpar(fontsize = 7), # Smaller font for Clone IDs + column_names_gp = gpar(fontsize = 7), # Smaller font for ScType labels + column_names_rot = 45, # Rotate labels to prevent overlap + row_names_side = "left", + + column_title = "Clone Occupancy Across Annotated T-cell States", + column_title_gp = gpar(fontsize = 12, fontface = "bold"), + + heatmap_legend_param = list( + title = "Cell Count", + at = c(0, max(clone_state_mat)), + labels = c("0", "Max") + ) + ) + + # 6. Render the plot + draw(ht_clone_state, heatmap_legend_side = "right") + +} else { + cat("Clone-state heatmap skipped: insufficient data or disabled in params.") +} +# if (isTRUE(params$show_clone_state_heatmap) && nrow(clone_state_tbl) > 0) { +# top_state_clones <- clone_state_tbl %>% +# group_by(clone_id) %>% +# summarise(total = sum(n_cells), .groups = "drop") %>% +# arrange(desc(total)) %>% +# slice_head(n = 30) %>% +# pull(clone_id) +# +# clone_state_mat <- clone_state_tbl %>% +# filter(clone_id %in% top_state_clones) %>% +# tidyr::pivot_wider(names_from = annot, values_from = n_cells, values_fill = 0) %>% +# as.data.frame() +# +# rownames(clone_state_mat) <- clone_state_mat$clone_id +# clone_state_mat$clone_id <- NULL +# clone_state_mat <- as.matrix(clone_state_mat) +# +# ht_clone_state <- Heatmap( +# clone_state_mat, +# name = "Cells", +# col = colorRamp2( +# c(0, max(clone_state_mat, na.rm = TRUE) / 2, max(clone_state_mat, na.rm = TRUE)), +# c("white", "gold", "firebrick") +# ), +# cluster_rows = TRUE, +# cluster_columns = TRUE, +# row_names_side = "left", +# column_title = "Clone occupancy across annotated T-cell states", +# heatmap_legend_param = list(title = "Cells") +# ) +# +# draw(ht_clone_state) +# } +``` + +## screpertoire-figures +```{r} +#| label: screpertoire-figures + +# Diagnostic check +if (!exists("seu_screp")) { + message("!!! DIAGNOSTIC: seu_screp object does not exist. Check the merge step.") +} else if (!"strict" %in% colnames(seu_screp@meta.data)) { + message("!!! DIAGNOSTIC: 'strict' column (clonotype) not found in metadata. scRepertoire plots may fail.") +} + +if (isTRUE(params$show_homeostasis) && exists("export_cells")) { + cat("### Clonal Homeostasis\n") + + clonal_group_levels <- c( + "Rare (0 < X <= 1e-04)", + "Small (1e-04 < X <= 0.001)", + "Medium (0.001 < X <= 0.01)", + "Large (0.01 < X <= 0.1)", + "Hyperexpanded (0.1 < X <= 1)" + ) + + homeo_rel_df <- export_cells %>% + filter(!is.na(sample), sample != "", !is.na(clone_id), clone_id != "") %>% + count(sample, clone_id, name = "n_cells") %>% + group_by(sample) %>% + mutate(clone_fraction = n_cells / sum(n_cells)) %>% + ungroup() %>% + mutate( + Clonal_Group = cut( + clone_fraction, + breaks = c(0, 1e-4, 1e-3, 1e-2, 1e-1, 1), + labels = clonal_group_levels, + include.lowest = TRUE, + right = TRUE + ) + ) %>% + group_by(sample, Clonal_Group) %>% + summarise(relative_abundance = sum(clone_fraction, na.rm = TRUE), .groups = "drop") + + sample_order_df2 <- homeo_rel_df %>% + select(sample, Clonal_Group, relative_abundance) %>% + distinct() %>% + tidyr::pivot_wider( + names_from = Clonal_Group, + values_from = relative_abundance, + values_fill = 0 + ) %>% + distinct(sample, .keep_all = TRUE) + + # make sure all ordering columns exist even if absent in this dataset + for (nm in clonal_group_levels) { + if (!nm %in% colnames(sample_order_df2)) { + sample_order_df2[[nm]] <- 0 + } + } + + sample_order_df2 <- sample_order_df2 %>% + arrange( + desc(.data[["Hyperexpanded (0.1 < X <= 1)"]]), + desc(.data[["Large (0.01 < X <= 0.1)"]]), + desc(.data[["Medium (0.001 < X <= 0.01)"]]), + desc(.data[["Small (1e-04 < X <= 0.001)"]]), + desc(.data[["Rare (0 < X <= 1e-04)"]]) + ) + + sample_levels2 <- unique(as.character(sample_order_df2$sample)) + + homeo_rel_df <- homeo_rel_df %>% + mutate( + sample = factor(as.character(sample), levels = sample_levels2), + Clonal_Group = factor(Clonal_Group, levels = clonal_group_levels) + ) + + p_homeo_rel <- ggplot(homeo_rel_df, aes(x = sample, y = relative_abundance, fill = Clonal_Group)) + + geom_col(width = 0.8) + + theme_scratch_pub(params$base_size) + + labs( + title = "Clonal Homeostasis", + x = "Samples", + y = "Relative Abundance", + fill = "Clonal Group" + ) + + theme( + axis.text.x = element_text(angle = 45, hjust = 1, size = 8) + ) + + print(p_homeo_rel) + + save_plot_safe( + p_homeo_rel, + glue("clonal_homeostasis.{params$figure_format}"), + width = 10, + height = 7 + ) +} + +if (isTRUE(params$show_diversity) && exists("export_cells")) { + cat("### Clonal Diversity\n") + + div_df <- export_cells %>% + filter(!is.na(sample), sample != "", !is.na(clone_id), clone_id != "") %>% + group_by(sample, clone_id) %>% + summarise(n = n(), .groups = "drop_last") %>% + summarise( + shannon = -sum((n / sum(n)) * log(n / sum(n))), + richness = dplyr::n(), + .groups = "drop" + ) %>% + arrange(desc(shannon)) + + div_df <- div_df %>% + mutate(sample = factor(sample, levels = unique(as.character(sample)))) + + p_div <- ggplot(div_df, aes(x = sample, y = shannon)) + + geom_col(fill = "gray35", width = 0.8) + + theme_scratch_pub(params$base_size) + + labs( + title = "Clonal Diversity (Shannon Index)", + x = "Sample", + y = "Shannon Index" + ) + + theme( + axis.text.x = element_text(angle = 45, hjust = 1, size = 8) + ) + + print(p_div) + + save_plot_safe( + p_div, + glue("clonal_diversity.{params$figure_format}"), + width = 9, + height = 6 + ) +} + + # p_div <- tryCatch({ + # scRepertoire::clonalDiversity(seu_screp, + # cloneCall = "strict", + # group.by = sample_col_seu %||% "orig.ident", + # n.boots = 20) + # Lower boots for speed + # theme_scratch_pub(params$base_size) + + # theme(axis.text.x = element_text(angle = 45, hjust = 1, fontsize = 8)) + + # ggtitle("Clonal Diversity") + # }, error = function(e) { + # message("Error in clonalDiversity: ", e$message) + # return(NULL) + # }) + # + # if (!is.null(p_div)) { + # print(p_div) + # save_plot_safe(p_div, glue("clonal_diversity.{params$figure_format}"), width = 9, height = 6) + # } +# } + +# if (isTRUE(params$show_homeostasis) && screp_combined_expression_ok) { +# p_homeo <- tryCatch({ +# clonalHomeostasis(seu_screp, cloneCall = "strict", group.by = sample_col_seu %||% "orig.ident") + +# theme_scratch_pub(params$base_size) + +# ggtitle("Clonal homeostasis") +# }, error = function(e) NULL) +# +# if (!is.null(p_homeo)) { +# print(p_homeo) +# save_plot_safe(p_homeo, glue("clonal_homeostasis.{params$figure_format}"), width = 9, height = 6) +# } +# } +# +# if (isTRUE(params$show_diversity) && screp_combined_expression_ok) { +# p_div <- tryCatch({ +# clonalDiversity(seu_screp, cloneCall = "strict", group.by = sample_col_seu %||% "orig.ident") + +# theme_scratch_pub(params$base_size) + +# ggtitle("Clonal diversity") +# }, error = function(e) NULL) +# +# if (!is.null(p_div)) { +# print(p_div) +# save_plot_safe(p_div, glue("clonal_diversity.{params$figure_format}"), width = 9, height = 6) +# } +# } +``` + +## sample-condition-patient +```{r} +#| label: sample-condition-patient +show_sample <- isTRUE(params$show_sample_panels) && (!is.null(sample_col_seu) || "META_SAMPLE" %in% colnames(seu@meta.data)) +show_condition <- isTRUE(params$show_condition_panels) && (!is.null(condition_col_seu) || "META_TIMECOND" %in% colnames(seu@meta.data)) +show_patient <- isTRUE(params$show_patient_panels) && (!is.null(patient_col_seu) || "META_PATIENT" %in% colnames(seu@meta.data)) +show_timepoint <- isTRUE(params$show_timepoint_panels) && (!is.null(timepoint_col_seu) || "META_TIMECOND" %in% colnames(seu@meta.data)) +``` + + +## sample-summary-plot +```{r} +#| results: asis + +if (show_sample) { + sample_summary <- export_cells %>% + filter(!is.na(sample)) %>% + group_by(sample) %>% + summarise( + n_cells = n(), + n_tcr = sum(has_tcr, na.rm = TRUE), + n_paired = sum(paired_tcr, na.rm = TRUE), + pct_tcr = safe_percent(n_tcr, n_cells), + .groups = "drop" + ) + + if (nrow(sample_summary) > 0) { + # FIX: Use knitr::knit_print() instead of print() to ensure HTML renders correctly + # and remove the print() call entirely if possible. + ktab <- kable( + sample_summary, + caption = "Sample-level TCR Stats" + ) %>% + kable_styling( + bootstrap_options = c("striped", "hover", "condensed"), + full_width = FALSE + ) + + # This ensures the table renders as HTML in the final document + knitr::knit_print(ktab) + + # Add a small break between table and plot + cat("\n\n") + + p_sample <- sample_summary %>% + mutate(sample = fct_reorder(sample, pct_tcr)) %>% + ggplot(aes(x = sample, y = pct_tcr)) + + geom_col(fill = "steelblue") + + coord_flip() + + scale_y_continuous(labels = scales::percent_format(accuracy = 1)) + + labs( + title = "TCR-positive fraction by sample", + x = NULL, + y = "Fraction" + ) + + theme_scratch_pub(params$base_size) + + theme( + # INCREASED: Changed from size=7 to base_size to make sample names legible + axis.text.y = element_text(size = params$base_size), + axis.text.x = element_text(size = params$base_size), + axis.title.x = element_text(size = params$base_size + 2, face = "bold"), + plot.title = element_text(size = params$base_size + 4, face = "bold") + ) + + print(p_sample) + save_plot_safe(p_sample, glue("sample_tcr_positive_fraction.{params$figure_format}")) + } else { + message("sample_summary is empty. Check whether the sample column is missing or all NA.") + } +} + +# #| label: sample-summary-plot +# +# if (show_sample) { +# sample_summary <- export_cells %>% +# filter(!is.na(sample)) %>% +# group_by(sample) %>% +# summarise( +# n_cells = n(), +# n_tcr = sum(has_tcr, na.rm = TRUE), +# n_paired = sum(paired_tcr, na.rm = TRUE), +# pct_tcr = safe_percent(n_tcr, n_cells), +# .groups = "drop" +# ) +# +# if (nrow(sample_summary) > 0) { +# # Print table so we see data +# # print(kable(sample_summary, caption = "Sample-level TCR Stats") %>% kable_styling()) +# cat(kable(sample_summary, format = "html", caption = "Sample-level TCR Stats") %>% +# kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = F) %>% +# as.character()) +# p_sample <- sample_summary %>% +# mutate(sample = fct_reorder(sample, pct_tcr)) %>% +# ggplot(aes(x = sample, y = pct_tcr)) + +# geom_col(fill = "steelblue") + +# coord_flip() + +# scale_y_continuous(labels = scales::percent_format(accuracy = 1)) + +# labs(title = "TCR-positive fraction by sample", x = NULL, y = "Fraction") + +# theme_scratch_pub(params$base_size) + +# theme(axis.text.y = element_text(size = 7)) # Small font for many samples +# +# print(p_sample) +# save_plot_safe(p_sample, glue("sample_tcr_positive_fraction.{params$figure_format}")) +# } else { +# message("!!! DIAGNOSTIC: sample_summary is empty. Check if 'sample' column has NAs.") +# } +# } + +# if (show_sample) { +# sample_summary <- export_cells %>% +# group_by(sample) %>% +# summarise( +# n_cells = n(), +# n_tcr = sum(has_tcr, na.rm = TRUE), +# n_paired = sum(paired_tcr, na.rm = TRUE), +# pct_tcr = safe_percent(n_tcr, n_cells), +# pct_paired = safe_percent(n_paired, n_cells), +# median_clone_size = median(clone_size, na.rm = TRUE), +# .groups = "drop" +# ) +# +# save_table_safe(sample_summary, "sample_level_tcell_integration_summary.tsv") +# +# p_sample <- sample_summary %>% +# mutate(sample = fct_reorder(sample, pct_tcr)) %>% +# ggplot(aes(x = sample, y = pct_tcr)) + +# geom_col(fill = "steelblue") + +# coord_flip() + +# scale_y_continuous(labels = percent_format(accuracy = 1)) + +# labs( +# title = "TCR-positive fraction by sample", +# x = NULL, +# y = "TCR-positive cells" +# ) + +# theme_scratch_pub(params$base_size) +# +# p_sample +# save_plot_safe(p_sample, glue("sample_tcr_positive_fraction.{params$figure_format}")) +# } +``` + +## condition-summary-plot +```{r} +#| label: condition-summary-plot +if (show_condition) { + condition_summary <- export_cells %>% + group_by(condition) %>% + summarise( + n_cells = n(), + n_tcr = sum(has_tcr, na.rm = TRUE), + n_paired = sum(paired_tcr, na.rm = TRUE), + pct_tcr = safe_percent(n_tcr, n_cells), + pct_paired = safe_percent(n_paired, n_cells), + .groups = "drop" + ) + + save_table_safe(condition_summary, "condition_level_tcell_integration_summary.tsv") + + p_condition <- condition_summary %>% + mutate(condition = fct_reorder(condition, pct_tcr)) %>% + ggplot(aes(x = condition, y = pct_tcr)) + + geom_col(fill = "purple4") + + coord_flip() + + scale_y_continuous(labels = percent_format(accuracy = 1)) + + labs( + title = "TCR-positive fraction by condition", + x = NULL, + y = "TCR-positive cells" + ) + + theme_scratch_pub(params$base_size) + + p_condition + save_plot_safe(p_condition, glue("condition_tcr_positive_fraction.{params$figure_format}")) +} +``` + +## patient-summary-plot + +## patient-summary-plot +```{r} +#| label: patient-summary-plot + +# Ensure the patient column is defined from the params +# If not already resolved in a 'resolve-columns' chunk: +curr_patient_col <- params$patient_col %||% "patient_id" + +if (show_patient && curr_patient_col %in% colnames(export_cells)) { + + patient_summary <- export_cells %>% + # Use the parameter from the params object + group_by(across(all_of(curr_patient_col))) %>% + summarise( + n_cells = n(), + n_tcr = sum(has_tcr, na.rm = TRUE), + n_paired = sum(paired_tcr, na.rm = TRUE), + pct_tcr = safe_percent(n_tcr, n_cells), + pct_paired = safe_percent(n_paired, n_cells), + .groups = "drop" + ) %>% + # Rename using the parameter + rename(patient = !!sym(curr_patient_col)) + + if (nrow(patient_summary) > 0) { + save_table_safe(patient_summary, "patient_level_tcell_integration_summary.tsv") + + p_patient <- patient_summary %>% + mutate(patient = fct_reorder(patient, pct_tcr)) %>% + ggplot(aes(x = patient, y = pct_tcr)) + + geom_col(fill = "darkgreen") + + coord_flip() + + scale_y_continuous(labels = scales::percent_format(accuracy = 1)) + + labs( + title = "TCR-positive fraction by patient", + subtitle = paste("Column used:", curr_patient_col), + x = NULL, + y = "TCR-positive fraction" + ) + + theme_scratch_pub(params$base_size) + + theme( + axis.text.y = element_text(size = params$base_size), + axis.text.x = element_text(size = params$base_size), + axis.title.x = element_text(size = params$base_size + 2, face = "bold"), + plot.title = element_text(size = params$base_size + 4, face = "bold") + ) + + # Explicitly print inside the if block + print(p_patient) + + save_plot_safe(p_patient, glue("patient_tcr_positive_fraction.{params$figure_format}")) + } +} else { + message("Skipping patient plot: column '", curr_patient_col, "' not found in data.") +} +``` + +## warnings +```{r} +#| label: warnings +warn_tbl <- tibble( + warning = c( + "Low final barcode overlap", + "Few T-cell matches after merge", + "Low paired TCR fraction", + "Annotation column auto-detected", + "Sample column missing", + "No scRepertoire combineExpression output" + ), + triggered = c( + isTRUE(final_overlap_rate < 0.6), + isTRUE(sum(seu_t$has_tcr, na.rm = TRUE) < 100), + isTRUE(mean(seu_t$paired_tcr, na.rm = TRUE) < 0.3), + isTRUE(params$label_col == ""), + isTRUE(is.null(sample_col_seu)), + isTRUE(!screp_combined_expression_ok) + ), + interpretation = c( + "Final TCR to GEX overlap is below 60%. Check barcode prefix/suffix conventions and sample naming.", + "Very few T cells retained matched TCR information. Inspect upstream VDJ QC and join strategy diagnostics.", + "Paired TCR fraction is low. This may reflect assay quality, chain filtering, or incomplete pairing.", + "Annotation column was auto-detected rather than explicitly specified.", + "No reliable sample column was detected in Seurat metadata. Some grouped summaries may be incomplete.", + "Optional scRepertoire combineExpression integration was not produced. The pipeline continued using the validated fast-merge TCR↔GEX integration outputs." + ) +) %>% + filter(triggered) + +if (nrow(warn_tbl) == 0) { + cat("No major automatic warnings were triggered under the current integration settings.") +} else { + kable( + warn_tbl %>% select(-triggered), + caption = "Automatically generated integration warnings and notes." + ) %>% + kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed") + ) +} + +# Show the actual scRepertoire error message if available +if (!isTRUE(screp_combined_expression_ok) && + exists("screp_error_msg") && + !is.null(screp_error_msg) && + !is.na(screp_error_msg) && + nzchar(screp_error_msg)) { + + cat("\n\n### scRepertoire combineExpression diagnostic message\n") + cat("`", screp_error_msg, "`\n", sep = "") +} +``` + +## session-info +```{r} +#| label: session-info +writeLines(capture.output(sessionInfo()), file.path(params$outdir, "sessionInfo.tcell_integration.txt")) +sessionInfo() +``` diff --git a/modules/scratch/TCELL_INTEGRATION/main.nf b/modules/scratch/TCELL_INTEGRATION/main.nf new file mode 100644 index 0000000..e020ba4 --- /dev/null +++ b/modules/scratch/TCELL_INTEGRATION/main.nf @@ -0,0 +1,62 @@ +process TCELL_INTEGRATION { + + tag "${project_name}" + label 'process_medium' + + container "${params.container}" + + publishDir "${params.outdir}/TCell_Integration",mode: 'copy', overwrite: true + // publishDir "${params.outdir}/${params.project_name}", + // mode: 'copy', overwrite: true + + + input: + path (contigs_after_qc) + path (annotated_object) + path (qmd) + val (project_name) + + output: + path "TCell_Integration_Report.html", emit: report_html + path "TCell_Integration_Report/data/seurat_tcells_with_TCR.rds", emit: seurat_tcells_with_tcr + path "TCell_Integration_Report/tables/tcr_export_cells_with_embedding.tsv", emit: export_cells + path("TCell_Integration_Report/tables/*"), emit: tables + path("TCell_Integration_Report/figures/*"), emit: figures + + + + + script: + """ + mkdir -p TCell_Integration_Report + + quarto render ${qmd} \ + -P contigs_file=${contigs_after_qc} \ + -P seurat_rds=${annotated_object} \ + -P outdir=TCell_Integration_Report \ + -P label_col="${params.label_col}" \ + -P sample_col="${params.sample_col}" \ + -P patient_col="${params.patient_col}" \ + -P condition_col="${params.condition_col}" \ + -P timepoint_col="${params.timepoint_col}" \ + -P batch_col="${params.batch_col}" \ + -P cells_mode="${params.cells_mode}" \ + -P filter_to_t_ab=${params.filter_to_t_ab} \ + -P clone_call_preference="${params.clone_call_preference}" \ + -P keep_na_clonotypes=${params.keep_na_clonotypes} \ + -P harmonize_apply_to="${params.harmonize_apply_to}" \ + -P harmonization_overlap_threshold=${params.harmonization_overlap_threshold} \ + -P minimum_final_overlap_fraction=${params.minimum_final_overlap_fraction} \ + -P subset_tcells=${params.subset_tcells} \ + -P tcell_regex="${params.tcell_regex}" \ + -P reduction_use="${params.reduction_use}" \ + -P make_umap_if_missing=${params.make_umap_if_missing} \ + -P umap_dims_max=${params.umap_dims_max} \ + -P umap_nfeatures=${params.umap_nfeatures} \ + -P raster_large_umap=${params.raster_large_umap} \ + -P min_clone_size_plot=${params.min_clone_size_plot} \ + -P top_n_clones_umap=${params.top_n_clones_umap} \ + -P top_n_clone_table=${params.top_n_clone_table} \ + -P report_label="${project_name} TCell Integration" + """ +} \ No newline at end of file diff --git a/modules/scratch/TCRI/TCRi_Report.qmd b/modules/scratch/TCRI/TCRi_Report.qmd new file mode 100644 index 0000000..c057034 --- /dev/null +++ b/modules/scratch/TCRI/TCRi_Report.qmd @@ -0,0 +1,1758 @@ +--- +title: "SCRATCH-TCR: TCRi Report" +author: "Syed Shujaat Ali Zaidi" +format: + html: + toc: true + toc-depth: 3 + number-sections: true + code-fold: true + code-summary: "Show code" + embed-resources: true + theme: cosmo + df-print: paged +execute: + echo: false + warning: false + message: false +params: + # ====================================================== + # Inputs + # ====================================================== + seurat_rds: "data/seurat_tcells_with_TCR.rds" + tcri_scores_file: "data/tcri_scores.tsv" + tcr_export_cells_file: "data/tcr_export_cells_with_embedding.tsv" + outdir: "TCRi_Report" + + python_bin: "" + + # Optional extra inputs + metadata_file: "" + previous_summary_file: "" + + # ====================================================== + # Output controls + # ====================================================== + data_dir: "data" + tables_dir: "tables" + figures_dir: "figures" + save_tables: true + save_figures: true + save_updated_seurat: true + figure_format: "png" + figure_width: 10 + figure_height: 7 + figure_dpi: 300 + base_size: 12 + + # ====================================================== + # Core column mapping + # ====================================================== + cell_id_col: "cell_id" + tcri_score_col: "tcri_score" + tcri_group_col: "tcri_group" + tcri_rank_col: "" + tcri_pvalue_col: "" + tcri_qvalue_col: "" + + cell_id_candidates: !expr c("cell_id","barcode","cell","Cell","cellid") + tcri_score_candidates: !expr c("tcri_score","score","TCRi","tcri","invertibility_score") + tcri_group_candidates: !expr c("tcri_group","group","class","label","TCRi_group") + tcri_rank_candidates: !expr c("rank","tcri_rank","score_rank") + tcri_pvalue_candidates: !expr c("pvalue","p_value","pval","tcri_pvalue") + tcri_qvalue_candidates: !expr c("qvalue","q_value","fdr","padj","adj_p","tcri_qvalue") + + # ====================================================== + # Metadata in Seurat / export table + # ====================================================== + assay_use: "RNA" + label_col: "predicted_labels" + sample_col: "META_SAMPLE" + patient_col: "META_PATIENT" + condition_col: "META_TIMECOND" + timepoint_col: "META_TIMEPOINT" + batch_col: "META_BATCH" + clone_id_col: "clone_id" + clone_size_col: "clone_size" + paired_tcr_col: "paired_tcr" + has_tcr_col: "has_tcr" + + label_candidates: "predicted_labels;predicted.celltype.l2;predicted.celltype.l1;azimuth_labels;celltypist;celltypist_label;majority_voting;scType;sctype;sctype_label;celltype;CellType;annot;Annotation;seurat_clusters" + sample_candidates: "META_SAMPLE;Sample;SampleID;sample_id;orig.ident;sample;library;donor;patient" + patient_candidates: "META_PATIENT;patient_id;patient;Patient;subject;donor;case_id" + condition_candidates: "META_TIMECOND;condition;Condition;group;Group;status;timepoint;Timepoint" + timepoint_candidates: "META_TIMEPOINT;timepoint;Timepoint;visit;Visit;day;Day;META_TIMECOND" + batch_candidates: "META_BATCH;batch;Batch;library;Library;run;Run;lane;Lane" + clone_id_candidates: "clone_id;CTaa;clonotype;clone" + clone_size_candidates: "clone_size;CloneSize;clone_n" + paired_tcr_candidates: "paired_tcr;paired;is_paired" + has_tcr_candidates: "has_tcr;hasTCR;tcr_positive" + + + # ====================================================== + # TCRi modeling + # ====================================================== + subset_to_tcr_positive: true + subset_to_paired_tcr: true + min_cells_per_phenotype: 15 + min_cells_total: 100 + + phenotype_obs_name: "phenotype" + clonotype_obs_name: "clone_id" + covariate_obs_name: "condition" + + + + global_scale: 10.0 + local_scale: 5.0 + prior_temperature: 1.0 + guide_temperature: 1.0 + use_enumeration: false + + margin_scale: 0.0 + margin_value: 2.0 + adaptive_margin: false + + # ====================================================== + # Embedding controls + # ====================================================== + reduction_use: "umap" + make_umap_if_missing: false + umap_dims_max: 30 + umap_nfeatures: 3000 + raster_large_umap: true + label_clusters: true + + # ====================================================== + # Analysis knobs + # Score grouping + # ====================================================== + auto_create_tcri_group_if_missing: true + tcri_high_cutoff: 0.80 + tcri_mid_cutoff: 0.50 + use_quantile_cutoffs_if_score_not_bounded: true + high_quantile: 0.90 + mid_quantile: 0.50 + + min_cells_per_group: 10 + min_clone_size_for_assoc: 2 + top_n_high_tcri_clones: 30 + top_n_groups_for_ridge: 20 + top_n_states_heatmap: 30 + top_n_states_table: 30 + + # ====================================================== + # Figure/report toggles + # ====================================================== + show_feature_plot: true + show_group_umap: true + show_density_plot: true + show_violin_by_annotation: true + show_boxplots_by_sample: true + show_boxplots_by_condition: true + show_boxplots_by_patient: true + show_boxplots_by_timepoint: true + show_score_vs_clone_size: true + show_high_tcri_clone_heatmap: true + show_high_tcri_annotation_enrichment: true + show_summary_tables: true + + report_label: "TCRi" + # ====================================================== + # TCRi modeling (Optimized) + # ====================================================== + n_latent: 16 # Increased for better resolution + n_hidden: 128 # Increased for more "brain power" + max_epochs: 100 # Essential for convergence + batch_size: 128 # Better for smaller/medium datasets + learning_rate: 0.001 + reconstruction_loss_scale: 0.01 + n_steps_kl_warmup: 1000 +--- + +# ================================================================= +# * Integrates precomputed TCRi results into the TCR–GEX Seurat object and aligns scores to individual cells through robust cell ID matching. +# * Generates standardized per-cell, per-annotation, and per-sample summaries of TCRi scores, groups, and clone-associated patterns. +# * Supports flexible operation across datasets with optional auto-generation of TCRi groups when categorical labels are not provided. +# * Produces publication-quality visualizations, including embedding overlays, score distributions, annotation-level comparisons, and clone size associations. +# * Identifies high-TCRi clonotypes and summarizes their phenotypic occupancy and enrichment across annotated T-cell states. +# * Exports an updated Seurat object plus downstream-ready tables and figures for subsequent modules such as CoNGA, clonotype clustering, and repertoire analysis. +# ================================================================= + + +## setup +```{r} +#| label: setup +suppressPackageStartupMessages({ + library(Seurat) + library(SeuratObject) + library(data.table) + library(dplyr) + library(tidyr) + library(stringr) + library(ggplot2) + library(forcats) + library(scales) + library(glue) + library(knitr) + library(kableExtra) + library(ComplexHeatmap) + library(circlize) + library(patchwork) + library(Matrix) + library(reticulate) +}) + +options(stringsAsFactors = FALSE) + +dir.create(params$outdir, recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$data_dir), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$tables_dir), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$figures_dir), recursive = TRUE, showWarnings = FALSE) + +`%||%` <- function(a, b) if (!is.null(a) && length(a) > 0 && !all(is.na(a))) a else b + +# theme_scratch_pub <- function(base_size = 12) { +# theme_bw(base_size = base_size) + +# theme( +# plot.title = element_text(face = "bold", size = base_size + 2, hjust = 0), +# plot.subtitle = element_text(size = base_size, hjust = 0), +# axis.title = element_text(face = "bold"), +# axis.text = element_text(color = "black"), +# panel.grid.minor = element_blank(), +# panel.grid.major = element_line(linewidth = 0.2, color = "grey90"), +# strip.background = element_rect(fill = "grey95", color = "grey80"), +# strip.text = element_text(face = "bold"), +# legend.title = element_text(face = "bold"), +# legend.key = element_blank(), +# plot.caption = element_text(size = base_size - 2, color = "grey40") +# ) +# } + +# theme_scratch_pub <- function(base_size = 12) { +# theme_bw(base_size = base_size) + +# theme( +# plot.title = element_text(face = "bold", size = base_size + 2, hjust = 0), +# plot.subtitle = element_text(size = base_size, hjust = 0), +# axis.title = element_text(face = "bold", size = base_size - 1), +# # Shrink Y-axis labels slightly +# axis.text.y = element_text(color = "black", size = base_size - 3), +# # Rotate X-axis labels 45 degrees and shrink +# axis.text.x = element_text(color = "black", size = base_size - 3, angle = 45, hjust = 1), +# panel.grid.minor = element_blank(), +# panel.grid.major = element_line(linewidth = 0.1, color = "grey90"), +# strip.background = element_rect(fill = "grey95", color = "grey80"), +# strip.text = element_text(face = "bold", size = base_size - 2), +# legend.title = element_text(face = "bold", size = base_size - 2), +# legend.text = element_text(size = base_size - 3), +# plot.caption = element_text(size = base_size - 4, color = "grey40") +# ) +# } + + +theme_scratch_pub <- function(base_size = 12) { + theme_bw(base_size = base_size) + + theme( + plot.title = element_text(face = "bold", size = base_size + 2, hjust = 0), + plot.subtitle = element_text(size = base_size, hjust = 0), + axis.title = element_text(face = "bold", size = base_size - 1), + axis.text.y = element_text(color = "black", size = base_size - 3), + # Fix for Q2 & Q3: Smaller text and 45-degree rotation for X-axis + axis.text.x = element_text(color = "black", size = base_size - 3, angle = 45, hjust = 1), + panel.grid.minor = element_blank(), + panel.grid.major = element_line(linewidth = 0.1, color = "grey90"), + strip.background = element_rect(fill = "grey95", color = "grey80"), + strip.text = element_text(face = "bold", size = base_size - 2), + legend.title = element_text(face = "bold", size = base_size - 2), + legend.text = element_text(size = base_size - 3), + legend.key = element_blank(), + plot.caption = element_text(size = base_size - 4, color = "grey40") + ) +} + +save_plot_safe <- function(plot_obj, filename, + width = params$figure_width, + height = params$figure_height, + dpi = params$figure_dpi) { + if (!isTRUE(params$save_figures)) return(invisible(NULL)) + ggsave( + filename = file.path(params$outdir, params$figures_dir, filename), + plot = plot_obj, + width = width, + height = height, + dpi = dpi, + bg = "white", + limitsize = FALSE + ) +} + +save_table_safe <- function(df, filename) { + if (!isTRUE(params$save_tables)) return(invisible(NULL)) + fwrite(df, file.path(params$outdir, params$tables_dir, filename), sep = "\t") +} + +save_rds_safe <- function(obj, filename) { + if (!isTRUE(params$save_updated_seurat)) return(invisible(NULL)) + saveRDS(obj, file.path(params$outdir, params$data_dir, filename)) +} + +first_existing_col <- function(df, preferred, candidates = character()) { + if (!is.null(preferred) && preferred != "" && preferred %in% colnames(df)) return(preferred) + hits <- intersect(candidates, colnames(df)) + if (length(hits) > 0) return(hits[[1]]) + NULL +} + +safe_percent <- function(x, denom) ifelse(denom > 0, x / denom, NA_real_) + +safe_make_umap <- function(seu, reduction_name = "umap", dims = 1:30, nfeatures = 3000) { + if ("RNA" %in% names(seu@assays)) DefaultAssay(seu) <- "RNA" + npcs <- min(max(dims), max(2, ncol(seu) - 1), 50) + nf <- min(nfeatures, nrow(seu)) + + if (!"pca" %in% Reductions(seu)) { + seu <- FindVariableFeatures(seu, nfeatures = nf, verbose = FALSE) + seu <- ScaleData(seu, verbose = FALSE) + seu <- RunPCA(seu, npcs = npcs, verbose = FALSE) + } + + use_dims <- dims[dims <= npcs] + if (length(use_dims) < 2) use_dims <- 1:min(10, npcs) + + seu <- FindNeighbors(seu, dims = use_dims, verbose = FALSE) + seu <- RunUMAP(seu, dims = use_dims, reduction.name = reduction_name, verbose = FALSE) + seu +} + +get_expr_mat <- function(seu, assay = "RNA", layer = "data") { + DefaultAssay(seu) <- assay + out <- tryCatch( + SeuratObject::LayerData(seu, assay = assay, layer = layer), + error = function(e) NULL + ) + if (is.null(out)) { + out <- tryCatch( + Seurat::GetAssayData(seu, assay = assay, slot = "data"), + error = function(e) NULL + ) + } + if (is.null(out)) stop("Could not retrieve expression matrix from assay/layer.") + out +} + +make_tcri_group <- function(score_vec, + high_cutoff = 0.80, + mid_cutoff = 0.50, + use_quantile = TRUE, + high_quantile = 0.90, + mid_quantile = 0.50) { + x <- suppressWarnings(as.numeric(score_vec)) + out <- rep(NA_character_, length(x)) + + finite_x <- x[is.finite(x)] + if (length(finite_x) == 0) return(out) + + bounded_01 <- min(finite_x, na.rm = TRUE) >= 0 && max(finite_x, na.rm = TRUE) <= 1 + + if (bounded_01 || !use_quantile) { + hi <- high_cutoff + mid <- mid_cutoff + } else { + hi <- as.numeric(quantile(finite_x, probs = high_quantile, na.rm = TRUE)) + mid <- as.numeric(quantile(finite_x, probs = mid_quantile, na.rm = TRUE)) + } + + dplyr::case_when( + is.na(x) ~ NA_character_, + x >= hi ~ "High", + x >= mid ~ "Intermediate", + TRUE ~ "Low" + ) +} + +compute_group_enrichment <- function(df, group_col, label_col, min_cells = 5) { + df %>% + filter(!is.na(.data[[group_col]]), !is.na(.data[[label_col]])) %>% + count(.data[[group_col]], .data[[label_col]], name = "n") %>% + group_by(.data[[group_col]]) %>% + mutate(group_total = sum(n), frac_in_group = n / group_total) %>% + ungroup() %>% + group_by(.data[[label_col]]) %>% + mutate(label_total = sum(n), frac_in_label = n / label_total) %>% + ungroup() %>% + filter(n >= min_cells) +} + +sanitize_param_string <- function(x) { + if (is.null(x) || length(x) == 0) return(NULL) + trimws(gsub("\\u00A0", " ", as.character(x))) +} + +normalize_colnames <- function(df) { + colnames(df) <- trimws(gsub("\\u00A0", " ", colnames(df))) + df +} + +split_candidate_string <- function(x) { + if (is.null(x) || length(x) == 0 || is.na(x) || x == "") return(character()) + x <- gsub('"', "", as.character(x)) + x <- trimws(unlist(strsplit(x, ";", fixed = TRUE))) + x[nzchar(x)] +} + +first_existing_col <- function(df, preferred, candidates = character()) { + df <- normalize_colnames(df) + preferred <- sanitize_param_string(preferred) + + if (length(candidates) == 1 && is.character(candidates)) { + candidates <- split_candidate_string(candidates) + } + candidates <- trimws(gsub("\\u00A0", " ", as.character(candidates))) + + if (!is.null(preferred) && preferred != "" && preferred %in% colnames(df)) return(preferred) + + hits <- intersect(candidates, colnames(df)) + if (length(hits) > 0) return(hits[[1]]) + + NULL +} + +choose_col <- function(df, primary = NULL, fallback = NULL) { + nm <- colnames(df) + if (!is.null(primary) && primary != "" && primary %in% nm) return(primary) + if (!is.null(fallback) && fallback != "" && fallback %in% nm) return(fallback) + NULL +} + +safe_percent <- function(x, denom) ifelse(denom > 0, x / denom, NA_real_) + +# sanitize_param_string <- function(x) { +# if (is.null(x) || length(x) == 0) return(NULL) +# trimws(gsub("\\u00A0", " ", as.character(x))) +# } +# +# normalize_colnames <- function(df) { +# colnames(df) <- trimws(gsub("\\u00A0", " ", colnames(df))) +# df +# } +# +# split_candidate_string <- function(x) { +# if (is.null(x) || length(x) == 0 || is.na(x) || x == "") return(character()) +# x <- gsub('"', "", as.character(x)) +# x <- trimws(unlist(strsplit(x, ";", fixed = TRUE))) +# x[nzchar(x)] +# } +# +# first_existing_col <- function(df, preferred, candidates = character()) { +# df <- normalize_colnames(df) +# preferred <- sanitize_param_string(preferred) +# +# if (length(candidates) == 1 && is.character(candidates)) { +# candidates <- split_candidate_string(candidates) +# } +# candidates <- trimws(gsub("\\u00A0", " ", as.character(candidates))) +# +# if (!is.null(preferred) && preferred != "" && preferred %in% colnames(df)) return(preferred) +# +# hits <- intersect(candidates, colnames(df)) +# if (length(hits) > 0) return(hits[[1]]) +# +# NULL +# } +# +# choose_col <- function(df, primary = NULL, fallback = NULL) { +# nm <- colnames(df) +# if (!is.null(primary) && primary != "" && primary %in% nm) return(primary) +# if (!is.null(fallback) && fallback != "" && fallback %in% nm) return(fallback) +# NULL +# } + +``` + +## load-and-resolve +```{r} +#| label: load-and-resolve +stopifnot(file.exists(params$seurat_rds)) +seu <- readRDS(params$seurat_rds) +md <- normalize_colnames(seu@meta.data) +seu@meta.data <- md + +label_col <- first_existing_col(md, params$label_col, params$label_candidates) +clone_id_col <- first_existing_col(md, params$clone_id_col, params$clone_id_candidates) +clone_size_col <- first_existing_col(md, params$clone_size_col, params$clone_size_candidates) +paired_tcr_col <- first_existing_col(md, params$paired_tcr_col, params$paired_tcr_candidates) +has_tcr_col <- first_existing_col(md, params$has_tcr_col, params$has_tcr_candidates) +sample_col <- first_existing_col(md, params$sample_col, params$sample_candidates) +patient_col <- first_existing_col(md, params$patient_col, params$patient_candidates) +condition_col <- first_existing_col(md, params$condition_col, params$condition_candidates) +timepoint_col <- first_existing_col(md, params$timepoint_col, params$timepoint_candidates) +batch_col <- first_existing_col(md, params$batch_col, params$batch_candidates) + +if (is.null(label_col)) stop("Could not resolve annotation label column.") +if (is.null(clone_id_col)) stop("Could not resolve clone_id column.") +if (is.null(clone_size_col)) stop("Could not resolve clone_size column.") +if (is.null(has_tcr_col)) stop("Could not resolve has_tcr column.") +if (is.null(paired_tcr_col)) stop("Could not resolve paired_tcr column.") +if (is.null(sample_col)) stop("Could not resolve sample column.") +if (is.null(patient_col)) stop("Could not resolve patient column.") +if (is.null(condition_col)) stop("Could not resolve condition column.") +if (is.null(timepoint_col)) stop("Could not resolve timepoint column.") + +if (!"META_SAMPLE" %in% colnames(seu@meta.data) && !is.null(sample_col)) { + seu$META_SAMPLE <- as.character(seu@meta.data[[sample_col]]) +} +if (!"META_PATIENT" %in% colnames(seu@meta.data) && !is.null(patient_col)) { + seu$META_PATIENT <- as.character(seu@meta.data[[patient_col]]) +} +if (!"META_TIMECOND" %in% colnames(seu@meta.data) && !is.null(condition_col)) { + seu$META_TIMECOND <- as.character(seu@meta.data[[condition_col]]) +} +if (!"META_TIMEPOINT" %in% colnames(seu@meta.data) && !is.null(timepoint_col)) { + seu$META_TIMEPOINT <- as.character(seu@meta.data[[timepoint_col]]) +} +if (!"META_BATCH" %in% colnames(seu@meta.data) && !is.null(batch_col)) { + seu$META_BATCH <- as.character(seu@meta.data[[batch_col]]) +} + +print(list( + resolved_label_col = label_col, + resolved_clone_id_col = clone_id_col, + resolved_clone_size_col = clone_size_col, + resolved_sample_col = sample_col, + resolved_patient_col = patient_col, + resolved_condition_col = condition_col, + resolved_timepoint_col = timepoint_col, + resolved_batch_col = batch_col +)) +# #| label: load-and-resolve +# stopifnot(file.exists(params$seurat_rds)) +# seu <- readRDS(params$seurat_rds) +# md <- normalize_colnames(seu@meta.data) +# seu@meta.data <- md +# +# label_col <- first_existing_col(md, params$label_col, params$label_candidates) +# clone_id_col <- first_existing_col(md, params$clone_id_col, params$clone_id_candidates) +# clone_size_col <- first_existing_col(md, params$clone_size_col, params$clone_size_candidates) +# paired_tcr_col <- first_existing_col(md, params$paired_tcr_col, params$paired_tcr_candidates) +# has_tcr_col <- first_existing_col(md, params$has_tcr_col, params$has_tcr_candidates) +# sample_col <- first_existing_col(md, params$sample_col, params$sample_candidates) +# patient_col <- first_existing_col(md, params$patient_col, params$patient_candidates) +# condition_col <- first_existing_col(md, params$condition_col, params$condition_candidates) +# timepoint_col <- first_existing_col(md, params$timepoint_col, params$timepoint_candidates) +# batch_col <- first_existing_col(md, params$batch_col, params$batch_candidates) +# +# if (is.null(label_col)) stop("Could not resolve annotation label column.") +# if (is.null(clone_id_col)) stop("Could not resolve clone_id column.") +# if (is.null(has_tcr_col)) stop("Could not resolve has_tcr column.") +# if (is.null(paired_tcr_col)) stop("Could not resolve paired_tcr column.") +# if (is.null(sample_col)) stop("Could not resolve sample column.") +# if (is.null(patient_col)) stop("Could not resolve patient column.") +# if (is.null(condition_col)) stop("Could not resolve condition column.") +# if (is.null(timepoint_col)) stop("Could not resolve timepoint column.") +# +# if (!"META_SAMPLE" %in% colnames(seu@meta.data) && !is.null(sample_col)) { +# seu$META_SAMPLE <- as.character(seu@meta.data[[sample_col]]) +# } +# if (!"META_PATIENT" %in% colnames(seu@meta.data) && !is.null(patient_col)) { +# seu$META_PATIENT <- as.character(seu@meta.data[[patient_col]]) +# } +# if (!"META_TIMECOND" %in% colnames(seu@meta.data) && !is.null(condition_col)) { +# seu$META_TIMECOND <- as.character(seu@meta.data[[condition_col]]) +# } +# if (!"META_TIMEPOINT" %in% colnames(seu@meta.data) && !is.null(timepoint_col)) { +# seu$META_TIMEPOINT <- as.character(seu@meta.data[[timepoint_col]]) +# } +# if (!"META_BATCH" %in% colnames(seu@meta.data) && !is.null(batch_col)) { +# seu$META_BATCH <- as.character(seu@meta.data[[batch_col]]) +# } +# +# print(list( +# resolved_label_col = label_col, +# resolved_clone_id_col = clone_id_col, +# resolved_sample_col = sample_col, +# resolved_patient_col = patient_col, +# resolved_condition_col = condition_col, +# resolved_timepoint_col = timepoint_col, +# resolved_batch_col = batch_col +# )) +``` + +## subset-and-embeddin +```{r} +#| label: subset-and-embedding +if (isTRUE(params$subset_to_tcr_positive)) { + keep <- !is.na(seu@meta.data[[has_tcr_col]]) & as.logical(seu@meta.data[[has_tcr_col]]) + seu <- subset(seu, cells = rownames(seu@meta.data)[keep]) +} + +if (isTRUE(params$subset_to_paired_tcr)) { + keep <- !is.na(seu@meta.data[[paired_tcr_col]]) & as.logical(seu@meta.data[[paired_tcr_col]]) + seu <- subset(seu, cells = rownames(seu@meta.data)[keep]) +} + +if (ncol(seu) < params$min_cells_total) { + stop("Too few cells after subsetting for TCRi. Found: ", ncol(seu)) +} + +reduction_to_use <- params$reduction_use +if (!(reduction_to_use %in% names(seu@reductions)) && isTRUE(params$make_umap_if_missing)) { + seu <- safe_make_umap( + seu, + reduction_name = params$reduction_use, + dims = 1:params$umap_dims_max, + nfeatures = params$umap_nfeatures + ) +} +if (!(reduction_to_use %in% names(seu@reductions))) { + reduction_to_use <- if ("umap" %in% names(seu@reductions)) "umap" else if ("tsne" %in% names(seu@reductions)) "tsne" else if ("pca" %in% names(seu@reductions)) "pca" else stop("No usable embedding found.") +} +``` + +## prepare-tcri-inputs +```{r} +#| label: prepare-tcri-inputs +assay_use <- sanitize_param_string(params$assay_use) +layer_use <- sanitize_param_string(params$layer_use) + +if (is.null(assay_use) || assay_use == "") { + stop("params$assay_use is missing or empty.") +} +if (is.null(layer_use) || layer_use == "") { + layer_use <- "data" +} + +message(sprintf("1. Extracting '%s' layer from '%s' assay...", layer_use, assay_use)) + +available_assays <- Seurat::Assays(seu) +if (!assay_use %in% available_assays) { + stop(sprintf( + "Assay '%s' not found in Seurat object. Available assays: %s", + assay_use, paste(available_assays, collapse = ", ") + )) +} + +if (inherits(seu[[assay_use]], "Assay5")) { + message("Detected Seurat v5 Assay. Joining layers...") + seu <- tryCatch({ + SeuratObject::JoinLayers(seu, assay = assay_use) + }, error = function(e) { + message("JoinLayers skipped: ", conditionMessage(e)) + seu + }) +} + +avail_layers <- tryCatch( + SeuratObject::Layers(seu[[assay_use]]), + error = function(e) character(0) +) + +avail_layers <- as.character(avail_layers) +avail_layers <- avail_layers[!is.na(avail_layers) & nzchar(avail_layers)] + +message("Available layers detected: ", if (length(avail_layers) > 0) paste(avail_layers, collapse = ", ") else "") + +if (length(avail_layers) > 0 && !(layer_use %in% avail_layers) && ("counts" %in% avail_layers)) { + message(sprintf( + "Layer '%s' is missing! Found 'counts' layer. Running LogNormalize on the fly...", + layer_use + )) + seu <- Seurat::NormalizeData( + seu, + assay = assay_use, + normalization.method = "LogNormalize", + scale.factor = 10000 + ) + + avail_layers <- tryCatch( + SeuratObject::Layers(seu[[assay_use]]), + error = function(e) character(0) + ) + avail_layers <- as.character(avail_layers) + avail_layers <- avail_layers[!is.na(avail_layers) & nzchar(avail_layers)] +} + +expr <- tryCatch({ + Seurat::GetAssayData(seu, assay = assay_use, layer = layer_use) +}, error = function(e) { + tryCatch({ + Seurat::GetAssayData(seu, assay = assay_use, slot = layer_use) + }, error = function(e2) NULL) +}) + +if (is.null(expr) || ncol(expr) == 0) { + avail_layers_txt <- if (length(avail_layers) > 0) paste(avail_layers, collapse = ", ") else "" + stop(sprintf( + "Matrix extraction failed! The layer/slot '%s' is empty or missing in assay '%s'. Available layers are: %s", + layer_use, assay_use, avail_layers_txt + )) +} + +if (is.null(colnames(expr)) && ncol(expr) == ncol(seu)) colnames(expr) <- colnames(seu) +if (is.null(rownames(expr)) && nrow(expr) == nrow(seu)) rownames(expr) <- rownames(seu) + +expr <- suppressWarnings(as(expr, "dgCMatrix")) + +message(sprintf("Extraction successful: %d genes across %d cells.", nrow(expr), ncol(expr))) + +message("2. Building Metadata with Python-Safe Hardening...") +md <- normalize_colnames(seu@meta.data) + +sample_col_export <- choose_col(md, "META_SAMPLE", sample_col) +patient_col_export <- choose_col(md, "META_PATIENT", patient_col) +condition_col_export <- choose_col(md, "META_TIMECOND", condition_col) +timepoint_col_export <- choose_col(md, "META_TIMEPOINT", timepoint_col) +batch_col_export <- choose_col(md, "META_BATCH", batch_col) + +harden_meta <- function(col_name, default_val) { + if (!is.null(col_name) && col_name %in% colnames(md)) { + val <- as.character(md[[col_name]]) + val[is.na(val) | trimws(val) == "" | val == "NA" | val == ""] <- "Unknown" + return(val) + } else { + return(rep(default_val, nrow(md))) + } +} + +obs_df <- data.frame(cell_id = rownames(md), stringsAsFactors = FALSE) + +obs_df$phenotype <- harden_meta(label_col, "Unknown") +obs_df$clone_id <- harden_meta(clone_id_col, "None") +obs_df$clone_size <- if (!is.null(clone_size_col) && clone_size_col %in% colnames(md)) { + suppressWarnings(as.numeric(md[[clone_size_col]])) +} else { + rep(0, nrow(md)) +} + +obs_df$sample <- harden_meta(sample_col_export, "Sample1") +obs_df$patient <- harden_meta(patient_col_export, "Patient1") +obs_df$condition <- harden_meta(condition_col_export, "Condition1") +obs_df$timepoint <- harden_meta(timepoint_col_export, "T1") +obs_df$batch <- harden_meta(batch_col_export, "Batch1") + +safe_extract_log <- function(col) { + if (!is.null(col) && col %in% colnames(md)) { + as.logical(md[[col]]) + } else { + rep(FALSE, nrow(md)) + } +} + +obs_df$paired_tcr <- safe_extract_log(paired_tcr_col) +obs_df$has_tcr <- safe_extract_log(has_tcr_col) + +obs_df <- obs_df[obs_df$phenotype != "Unknown" & obs_df$clone_id != "None", , drop = FALSE] + +message("3. Synchronizing Matrix and Metadata...") +valid_cells <- as.character(intersect(colnames(expr), obs_df$cell_id)) + +if (length(valid_cells) == 0) { + stop("Zero cells matched between the expression matrix and the TCR metadata.") +} + +expr <- expr[, valid_cells, drop = FALSE] +obs_df <- obs_df[obs_df$cell_id %in% valid_cells, , drop = FALSE] + +pheno_counts <- table(obs_df$phenotype) +keep_pheno <- names(pheno_counts)[pheno_counts >= params$min_cells_per_phenotype] +obs_df <- obs_df[obs_df$phenotype %in% keep_pheno, , drop = FALSE] + +final_cells <- as.character(obs_df$cell_id) +expr <- expr[, final_cells, drop = FALSE] + +if (ncol(expr) < params$min_cells_total) { + stop("Too few cells remain after phenotype/clonotype filtering for TCRi.") +} + +message("4. Exporting files...") +var_df <- data.frame(gene = rownames(expr), row.names = rownames(expr), stringsAsFactors = FALSE) + +mm_file <- file.path(params$outdir, params$data_dir, "tcri_expr.mtx") +obs_file <- file.path(params$outdir, params$data_dir, "tcri_obs.tsv") +var_file <- file.path(params$outdir, params$data_dir, "tcri_var.tsv") + +Matrix::writeMM(expr, mm_file) +write.table(obs_df, obs_file, sep = "\t", quote = FALSE, row.names = FALSE) +write.table(var_df, var_file, sep = "\t", quote = FALSE, row.names = FALSE) + +message("TCRi Inputs successfully prepared. Missing metadata columns were filled with placeholders.") +print(colSums(!is.na(obs_df[, c("sample", "patient", "condition", "timepoint", "batch")]))) + +# #| label: prepare-tcri-inputs +# assay_use <- params$assay_use +# layer_use <- params$layer_use +# +# message(sprintf("1. Extracting '%s' layer from '%s' assay...", layer_use, assay_use)) +# +# # Safely check if assay exists +# if (!assay_use %in% Seurat::Assays(seu)) { +# stop(sprintf("Assay '%s' not found in Seurat object. Available assays: %s", +# assay_use, paste(Seurat::Assays(seu), collapse = ", "))) +# } +# +# # If this is a Seurat v5 object with split layers, join them first +# if (inherits(seu[[assay_use]], "Assay5")) { +# message("Detected Seurat v5 Assay. Joining layers...") +# seu <- tryCatch({ +# SeuratObject::JoinLayers(seu, assay = assay_use) +# }, error = function(e) seu) +# } +# +# # --- AUTO-NORMALIZATION FAILSAFE --- +# avail_layers <- SeuratObject::Layers(seu[[assay_use]]) +# if (!layer_use %in% avail_layers && "counts" %in% avail_layers) { +# message(sprintf("Layer '%s' is missing! Found 'counts' layer. Running LogNormalize on the fly...", layer_use)) +# seu <- Seurat::NormalizeData(seu, assay = assay_use, normalization.method = "LogNormalize", scale.factor = 10000) +# } +# +# # Robust Extraction +# expr <- tryCatch({ +# Seurat::GetAssayData(seu, assay = assay_use, layer = layer_use) # v5 argument +# }, error = function(e) { +# tryCatch({ +# Seurat::GetAssayData(seu, assay = assay_use, slot = layer_use) # v4 argument +# }, error = function(e2) NULL) +# }) +# +# # Safety Net +# if (is.null(expr) || ncol(expr) == 0) { +# avail_layers <- paste(SeuratObject::Layers(seu[[assay_use]]), collapse = ", ") +# stop(sprintf("Matrix extraction failed! The layer/slot '%s' is empty or missing in assay '%s'. Available layers are: %s", +# layer_use, assay_use, avail_layers)) +# } +# +# # Attach dimnames only if sizes match perfectly +# if (is.null(colnames(expr)) && ncol(expr) == ncol(seu)) colnames(expr) <- colnames(seu) +# if (is.null(rownames(expr)) && nrow(expr) == nrow(seu)) rownames(expr) <- rownames(seu) +# +# # Force to pure sparse matrix +# expr <- suppressWarnings(as(expr, "dgCMatrix")) +# +# message(sprintf("Extraction successful: %d genes across %d cells.", nrow(expr), ncol(expr))) +# +# message("2. Building Metadata with Python-Safe Hardening...") +# md <- seu@meta.data +# +# # HELPER FUNCTION: Prevents Python 'ValueError' by ensuring columns are not all NA +# # If column is missing, uses default_val. If column has NAs, replaces with "Unknown". +# harden_meta <- function(col_name, default_val) { +# if (!is.null(col_name) && col_name %in% colnames(md)) { +# val <- as.character(md[[col_name]]) +# val[is.na(val) | val == "" | val == "NA"] <- "Unknown" +# return(val) +# } else { +# return(rep(default_val, nrow(md))) +# } +# } +# +# obs_df <- data.frame(cell_id = rownames(md), stringsAsFactors = FALSE) +# +# # Core identifiers (Must be valid) +# obs_df$phenotype <- harden_meta(label_col, "Unknown") +# obs_df$clone_id <- harden_meta(clone_id_col, "None") +# obs_df$clone_size <- if(!is.null(clone_size_col) && clone_size_col %in% colnames(md)) suppressWarnings(as.numeric(md[[clone_size_col]])) else 0 +# +# # Covariates and Batch Keys (The focus of the fix) +# obs_df$sample <- harden_meta(sample_col, "Sample1") +# obs_df$patient <- harden_meta(patient_col, "Patient1") +# obs_df$condition <- harden_meta(condition_col, "Condition1") +# obs_df$timepoint <- harden_meta(timepoint_col, "T1") +# obs_df$batch <- harden_meta(batch_col, "Batch1") +# +# # Boolean flags +# safe_extract_log <- function(col) if (!is.null(col) && col %in% colnames(md)) as.logical(md[[col]]) else FALSE +# obs_df$paired_tcr <- safe_extract_log(paired_tcr_col) +# obs_df$has_tcr <- safe_extract_log(has_tcr_col) +# +# # Filter for valid cells (Must have phenotype and clone_id) +# obs_df <- obs_df[obs_df$phenotype != "Unknown" & obs_df$clone_id != "None", ] +# +# message("3. Synchronizing Matrix and Metadata...") +# valid_cells <- as.character(intersect(colnames(expr), obs_df$cell_id)) +# +# if (length(valid_cells) == 0) { +# stop("Zero cells matched between the expression matrix and the TCR metadata.") +# } +# +# expr <- expr[, valid_cells, drop = FALSE] +# obs_df <- obs_df[obs_df$cell_id %in% valid_cells, ] +# +# # Filter rare phenotypes +# pheno_counts <- table(obs_df$phenotype) +# keep_pheno <- names(pheno_counts)[pheno_counts >= params$min_cells_per_phenotype] +# obs_df <- obs_df[obs_df$phenotype %in% keep_pheno, ] +# +# # Final strict subset +# final_cells <- as.character(obs_df$cell_id) +# expr <- expr[, final_cells, drop = FALSE] +# +# if (ncol(expr) < params$min_cells_total) { +# stop("Too few cells remain after phenotype/clonotype filtering for TCRi.") +# } +# +# message("4. Exporting files...") +# var_df <- data.frame(gene = rownames(expr), row.names = rownames(expr), stringsAsFactors = FALSE) +# +# mm_file <- file.path(params$outdir, params$data_dir, "tcri_expr.mtx") +# obs_file <- file.path(params$outdir, params$data_dir, "tcri_obs.tsv") +# var_file <- file.path(params$outdir, params$data_dir, "tcri_var.tsv") +# +# Matrix::writeMM(expr, mm_file) +# write.table(obs_df, obs_file, sep = "\t", quote = FALSE, row.names = FALSE) +# write.table(var_df, var_file, sep = "\t", quote = FALSE, row.names = FALSE) +# +# message("TCRi Inputs successfully prepared. Missing metadata columns were filled with placeholders.") + +``` + +# run-tcri +```{r} +#| label: run-tcri + +python_bin <- params$python_bin +if (is.null(python_bin) || python_bin == "") { + stop("params$python_bin must point to the working Python executable for TCRi.") +} + +scores_file <- file.path(params$outdir, params$tables_dir, "tcri_scores.tsv") +adata_file <- file.path(params$outdir, params$data_dir, "tcri_model_output.h5ad") +py_script <- file.path(params$outdir, params$data_dir, "run_tcri.py") +log_file <- file.path(params$outdir, params$tables_dir, "tcri_python_stdout_stderr.log") + +cat("Using python_bin:", python_bin, "\n") + +py_lines <- c( + "import os", + "os.environ['TQDM_DISABLE'] = '1'", + "os.environ['PYTHONWARNINGS'] = 'ignore'", + "import scipy.io", + "import pandas as pd", + "import anndata as ad", + "import numpy as np", + "import inspect", + "import tcri", + "from tcri.model import TCRIModel", + "import tcri.preprocessing as pp", + "print('tcri module path:', getattr(tcri, '__file__', 'NA'))", + "print('tcri attrs sample:', [x for x in dir(tcri) if 'TCRI' in x or x in ['pp','pl','tl']])", + "", + sprintf("mm_file = r'''%s'''", mm_file), + sprintf("obs_file = r'''%s'''", obs_file), + sprintf("var_file = r'''%s'''", var_file), + sprintf("scores_file = r'''%s'''", scores_file), + sprintf("adata_file = r'''%s'''", adata_file), + "", + "X = scipy.io.mmread(mm_file).T.tocsr()", + "obs = pd.read_csv(obs_file, sep='\\t')", + "var = pd.read_csv(var_file, sep='\\t')", + "", + "obs = obs.set_index('cell_id')", + "var = var.set_index('gene')", + "", + "adata = ad.AnnData(X=X, obs=obs, var=var)", + "adata.obs_names = obs.index.astype(str)", + "adata.var_names = var.index.astype(str)", + "", + "print('adata.obs non-null counts:')", + "print(adata.obs.notna().sum())", + "print('timepoint unique values:')", + "print(adata.obs['timepoint'].dropna().astype(str).unique() if 'timepoint' in adata.obs.columns else 'MISSING')", + "print('batch unique values:')", + "print(adata.obs['batch'].dropna().astype(str).unique() if 'batch' in adata.obs.columns else 'MISSING')", + "", + "print('TCRIModel.setup_anndata signature:', inspect.signature(TCRIModel.setup_anndata))", + "setup_sig = inspect.signature(TCRIModel.setup_anndata)", + "setup_kwargs = {}", + "", + "def has_real_values(series):", + " s = series.dropna().astype(str).str.strip()", + " s = s[~s.isin(['', 'nan', 'NA', '', 'None'])]", + " return len(s) > 0", + "", + "if 'phenotype_key' in setup_sig.parameters and 'phenotype' in adata.obs.columns:", + " if has_real_values(adata.obs['phenotype']):", + " setup_kwargs['phenotype_key'] = 'phenotype'", + "", + "if 'labels_key' in setup_sig.parameters and 'phenotype' in adata.obs.columns:", + " if has_real_values(adata.obs['phenotype']):", + " setup_kwargs['labels_key'] = 'phenotype'", + "", + "if 'clonotype_key' in setup_sig.parameters and 'clone_id' in adata.obs.columns:", + " if has_real_values(adata.obs['clone_id']):", + " setup_kwargs['clonotype_key'] = 'clone_id'", + "", + "if 'clone_key' in setup_sig.parameters and 'clone_id' in adata.obs.columns:", + " if has_real_values(adata.obs['clone_id']):", + " setup_kwargs['clone_key'] = 'clone_id'", + "", + "if 'tcr_key' in setup_sig.parameters and 'clone_id' in adata.obs.columns:", + " if has_real_values(adata.obs['clone_id']):", + " setup_kwargs['tcr_key'] = 'clone_id'", + "", + "if 'batch_key' in setup_sig.parameters and 'batch' in adata.obs.columns:", + " if has_real_values(adata.obs['batch']):", + " setup_kwargs['batch_key'] = 'batch'", + "", + "if 'covariate_key' in setup_sig.parameters and 'timepoint' in adata.obs.columns:", + " if has_real_values(adata.obs['timepoint']):", + " setup_kwargs['covariate_key'] = 'timepoint'", + "", + "# use adata.X directly for this installed TCRI version", + "if 'layer' in setup_sig.parameters:", + " setup_kwargs['layer'] = None", + "", + "print('setup_kwargs:', setup_kwargs)", + "TCRIModel.setup_anndata(adata, **setup_kwargs)", + "print('setup_anndata completed')", + "", + sprintf( + "model = TCRIModel(adata, n_latent=%d, n_hidden=%d, global_scale=%s, local_scale=%s, prior_temperature=%s, guide_temperature=%s, use_enumeration=%s, device=None)", + as.integer(params$n_latent), + as.integer(params$n_hidden), + as.character(params$global_scale), + as.character(params$local_scale), + as.character(params$prior_temperature), + as.character(params$guide_temperature), + ifelse(isTRUE(params$use_enumeration), "True", "False") + ), + "", + "print('Preparing model.train() arguments...')", + sprintf("max_epochs = %d", as.integer(params$max_epochs)), + sprintf("batch_size = %d", as.integer(params$batch_size)), + sprintf("lr = %s", as.character(params$learning_rate)), + sprintf("margin_scale = %s", as.character(params$margin_scale)), + sprintf("margin_value = %s", as.character(params$margin_value)), + sprintf("adaptive_margin = %s", ifelse(isTRUE(params$adaptive_margin), "True", "False")), + sprintf("reconstruction_loss_scale = %s", as.character(params$reconstruction_loss_scale)), + sprintf("n_steps_kl_warmup = %d", as.integer(params$n_steps_kl_warmup)), + "", + "train_sig = inspect.signature(model.train)", + "train_kwargs = {'max_epochs': max_epochs, 'batch_size': batch_size}", + "", + "plan_vals = {'lr': lr, 'margin_scale': margin_scale, 'margin_value': margin_value,", + " 'adaptive_margin': adaptive_margin, 'reconstruction_loss_scale': reconstruction_loss_scale,", + " 'n_steps_kl_warmup': n_steps_kl_warmup}", + "", + "if 'plan_kwargs' in train_sig.parameters:", + " train_kwargs['plan_kwargs'] = plan_vals", + "else:", + " for p in plan_vals:", + " if p in train_sig.parameters:", + " train_kwargs[p] = plan_vals[p]", + "", + "print('TCRIModel.train signature:', train_sig)", + "print('Final train_kwargs:', train_kwargs)", + "model.train(**train_kwargs)", + "print('model.train completed')", + "", + "# compatibility for installed TCRI preprocessing helpers", + "if 'clone_id' in adata.obs.columns and 'trb_unique' not in adata.obs.columns:", + " adata.obs['trb_unique'] = adata.obs['clone_id'].astype(str)", + "", + sprintf( + "pp.register_model(adata, model, phenotype_prob_slot='X_tcri_phenotypes', phenotype_assignment_obs='tcri_predicted_phenotype', latent_slot='X_tcri', batch_size=%d)", + as.integer(params$batch_size) + ), + "", + "scores = pd.DataFrame(index=adata.obs.index)", + "scores['cell_id'] = scores.index.astype(str)", + "", + "if 'X_tcri_phenotypes' in adata.obsm.keys():", + " probs = np.asarray(adata.obsm['X_tcri_phenotypes'])", + " scores['tcri_score'] = probs.max(axis=1)", + "else:", + " scores['tcri_score'] = np.nan", + "", + "if 'tcri_predicted_phenotype' in adata.obs.columns:", + " scores['tcri_predicted_phenotype'] = adata.obs['tcri_predicted_phenotype'].astype(str)", + "else:", + " scores['tcri_predicted_phenotype'] = np.nan", + "", + "if 'X_tcri' in adata.obsm.keys():", + " latent = np.asarray(adata.obsm['X_tcri'])", + " scores['tcri_latent_norm'] = np.sqrt((latent ** 2).sum(axis=1))", + "else:", + " scores['tcri_latent_norm'] = np.nan", + "", + "scores.to_csv(scores_file, sep='\\t', index=False)", + "", + sprintf("save_h5ad = %s", ifelse(isTRUE(params$save_h5ad), "True", "False")), + "if save_h5ad:", + " adata.write_h5ad(adata_file)" +) + +writeLines(py_lines, py_script) + +res <- system2( + python_bin, + args = c(py_script), + stdout = TRUE, + stderr = TRUE +) + +status <- attr(res, "status") +if (is.null(status)) status <- 0L + +writeLines(res, log_file) + +if (status != 0L) { + stop( + paste0( + "TCRi Python script failed with exit status ", status, ".\n", + "See log: ", log_file, "\n\n", + paste(res, collapse = "\n") + ) + ) +} else { + cat("TCRi Python run completed successfully. Detailed logs were saved to:\n") + cat(log_file, "\n") +} + +# res <- system2( +# python_bin, +# args = c(py_script), +# stdout = TRUE, +# stderr = TRUE +# ) +# +# status <- attr(res, "status") +# if (is.null(status)) status <- 0L +# +# writeLines(res, log_file) +# cat(paste(res, collapse = "\n"), "\n") +# +# if (status != 0L) { +# stop( +# paste0( +# "TCRi Python script failed with exit status ", status, ".\n", +# "See log: ", log_file, "\n\n", +# paste(res, collapse = "\n") +# ) +# ) +# } + +if (!file.exists(scores_file)) { + stop( + paste0( + "TCRi Python script exited without creating scores_file.\n", + "Expected: ", scores_file, "\n", + "See log: ", log_file, "\n\n", + paste(res, collapse = "\n") + ) + ) +} +``` + +# merge-back-into-seurat +```{r} +#| label: merge-back-into-seurat +tcri_scores <- fread(scores_file) %>% as.data.frame() + +tcri_scores <- tcri_scores %>% + mutate( + cell_id = as.character(cell_id), + tcri_score = suppressWarnings(as.numeric(tcri_score)), + tcri_predicted_phenotype = as.character(tcri_predicted_phenotype), + tcri_latent_norm = suppressWarnings(as.numeric(tcri_latent_norm)), + tcri_group = make_tcri_group( + tcri_score, + high_cutoff = params$tcri_high_cutoff, + mid_cutoff = params$tcri_mid_cutoff, + use_quantile = params$use_quantile_cutoffs_if_score_not_bounded, + high_quantile = params$high_quantile, + mid_quantile = params$mid_quantile + ) + ) + +md_dt <- as.data.table(seu@meta.data, keep.rownames = "cell_id") +setkey(md_dt, cell_id) + +tcri_dt <- as.data.table(tcri_scores) +setkey(tcri_dt, cell_id) + +md_dt <- tcri_dt[md_dt] + +rn <- md_dt$cell_id +md_dt$cell_id <- NULL +seu@meta.data <- as.data.frame(md_dt) +rownames(seu@meta.data) <- rn + +save_rds_safe(seu, "seurat_tcells_with_TCRi.rds") +save_table_safe(tcri_scores, "tcri_scores.tsv") +``` + + +## build-export +```{r} +#| label: build-export +embed_df <- as.data.frame(Embeddings(seu, reduction_to_use)) %>% + tibble::rownames_to_column("cell_id") + +sample_col_export <- choose_col(seu@meta.data, "META_SAMPLE", sample_col) +patient_col_export <- choose_col(seu@meta.data, "META_PATIENT", patient_col) +condition_col_export <- choose_col(seu@meta.data, "META_TIMECOND", condition_col) +timepoint_col_export <- choose_col(seu@meta.data, "META_TIMEPOINT", timepoint_col) +batch_col_export <- choose_col(seu@meta.data, "META_BATCH", batch_col) + +export_cells <- seu@meta.data %>% + tibble::rownames_to_column("cell_id") %>% + transmute( + cell_id, + sample = if (!is.null(sample_col_export)) as.character(.data[[sample_col_export]]) else NA_character_, + patient = if (!is.null(patient_col_export)) as.character(.data[[patient_col_export]]) else NA_character_, + condition = if (!is.null(condition_col_export)) as.character(.data[[condition_col_export]]) else NA_character_, + timepoint = if (!is.null(timepoint_col_export)) as.character(.data[[timepoint_col_export]]) else NA_character_, + batch = if (!is.null(batch_col_export)) as.character(.data[[batch_col_export]]) else NA_character_, + annot = as.character(.data[[label_col]]), + clone_id = as.character(.data[[clone_id_col]]), + clone_size = suppressWarnings(as.numeric(.data[[clone_size_col]])), + has_tcr = as.logical(.data[[has_tcr_col]]), + paired_tcr = as.logical(.data[[paired_tcr_col]]), + tcri_score = suppressWarnings(as.numeric(.data[["tcri_score"]])), + tcri_group = as.character(.data[["tcri_group"]]), + tcri_predicted_phenotype = as.character(.data[["tcri_predicted_phenotype"]]), + tcri_latent_norm = suppressWarnings(as.numeric(.data[["tcri_latent_norm"]])) + ) %>% + left_join(embed_df, by = "cell_id") + +print(colSums(!is.na(export_cells[, c("sample", "patient", "condition", "timepoint", "batch")]))) +save_table_safe(export_cells, "tcri_export_cells.tsv") + + +# #| label: build-export +# embed_df <- as.data.frame(Embeddings(seu, reduction_to_use)) %>% +# tibble::rownames_to_column("cell_id") +# +# sample_col_export <- choose_col(seu@meta.data, "META_SAMPLE", sample_col) +# patient_col_export <- choose_col(seu@meta.data, "META_PATIENT", patient_col) +# condition_col_export <- choose_col(seu@meta.data, "META_TIMECOND", condition_col) +# timepoint_col_export <- choose_col(seu@meta.data, "META_TIMEPOINT", timepoint_col) +# batch_col_export <- choose_col(seu@meta.data, "META_BATCH", batch_col) +# +# export_cells <- seu@meta.data %>% +# tibble::rownames_to_column("cell_id") %>% +# transmute( +# cell_id, +# sample = if (!is.null(sample_col_export)) as.character(.data[[sample_col_export]]) else NA_character_, +# patient = if (!is.null(patient_col_export)) as.character(.data[[patient_col_export]]) else NA_character_, +# condition = if (!is.null(condition_col_export)) as.character(.data[[condition_col_export]]) else NA_character_, +# timepoint = if (!is.null(timepoint_col_export)) as.character(.data[[timepoint_col_export]]) else NA_character_, +# batch = if (!is.null(batch_col_export)) as.character(.data[[batch_col_export]]) else NA_character_, +# annot = as.character(.data[[label_col]]), +# clone_id = as.character(.data[[clone_id_col]]), +# clone_size = suppressWarnings(as.numeric(.data[[clone_size_col]])), +# has_tcr = as.logical(.data[[has_tcr_col]]), +# paired_tcr = as.logical(.data[[paired_tcr_col]]), +# tcri_score = suppressWarnings(as.numeric(.data[["tcri_score"]])), +# tcri_group = as.character(.data[["tcri_group"]]), +# tcri_predicted_phenotype = as.character(.data[["tcri_predicted_phenotype"]]), +# tcri_latent_norm = suppressWarnings(as.numeric(.data[["tcri_latent_norm"]])) +# ) %>% +# left_join(embed_df, by = "cell_id") +# +# print(colSums(!is.na(export_cells[, c("sample", "patient", "condition", "timepoint")]))) +# save_table_safe(export_cells, "tcri_export_cells.tsv") + +``` + + +## summary tables +```{r} +#| label: summary-tables +summary_rollup <- tibble( + metric = c( + "Report label", + "Cells modeled", + "Cells with TCRi score", + "Fraction with TCRi score", + "TCR-positive cells", + "Paired TCR cells", + "Unique annotations", + "Unique clones", + "Selected embedding" + ), + value = c( + params$report_label, + nrow(export_cells), + sum(!is.na(export_cells$tcri_score)), + sprintf("%.2f%%", 100 * mean(!is.na(export_cells$tcri_score))), + sum(export_cells$has_tcr, na.rm = TRUE), + sum(export_cells$paired_tcr, na.rm = TRUE), + length(unique(na.omit(export_cells$annot))), + length(unique(na.omit(export_cells$clone_id))), + reduction_to_use + ) +) +save_table_safe(summary_rollup, "tcri_summary_rollup.tsv") + +tcri_group_summary <- export_cells %>% + filter(!is.na(tcri_group)) %>% + count(tcri_group, name = "n_cells") %>% + mutate(frac = n_cells / sum(n_cells)) +save_table_safe(tcri_group_summary, "tcri_group_summary.tsv") + +annotation_tcri_summary <- export_cells %>% + filter(!is.na(tcri_score), !is.na(annot)) %>% + group_by(annot) %>% + summarise( + n_cells = n(), + mean_tcri = mean(tcri_score, na.rm = TRUE), + median_tcri = median(tcri_score, na.rm = TRUE), + sd_tcri = sd(tcri_score, na.rm = TRUE), + n_high = sum(tcri_group == "High", na.rm = TRUE), + frac_high = n_high / n_cells, + .groups = "drop" + ) %>% + arrange(desc(mean_tcri)) +save_table_safe(annotation_tcri_summary, "annotation_tcri_summary.tsv") + +sample_tcri_summary <- export_cells %>% + filter(!is.na(tcri_score), !is.na(sample)) %>% + group_by(sample) %>% + summarise( + n_cells = n(), + mean_tcri = mean(tcri_score, na.rm = TRUE), + median_tcri = median(tcri_score, na.rm = TRUE), + n_high = sum(tcri_group == "High", na.rm = TRUE), + frac_high = n_high / n_cells, + .groups = "drop" + ) %>% + arrange(desc(mean_tcri)) +save_table_safe(sample_tcri_summary, "sample_tcri_summary.tsv") + +if (any(!is.na(export_cells$condition))) { + condition_tcri_summary <- export_cells %>% + filter(!is.na(tcri_score), !is.na(condition)) %>% + group_by(condition) %>% + summarise( + n_cells = n(), + mean_tcri = mean(tcri_score, na.rm = TRUE), + median_tcri = median(tcri_score, na.rm = TRUE), + n_high = sum(tcri_group == "High", na.rm = TRUE), + frac_high = n_high / n_cells, + .groups = "drop" + ) + save_table_safe(condition_tcri_summary, "condition_tcri_summary.tsv") +} + +if (any(!is.na(export_cells$patient))) { + patient_tcri_summary <- export_cells %>% + filter(!is.na(tcri_score), !is.na(patient)) %>% + group_by(patient) %>% + summarise( + n_cells = n(), + mean_tcri = mean(tcri_score, na.rm = TRUE), + median_tcri = median(tcri_score, na.rm = TRUE), + n_high = sum(tcri_group == "High", na.rm = TRUE), + frac_high = n_high / n_cells, + .groups = "drop" + ) + save_table_safe(patient_tcri_summary, "patient_tcri_summary.tsv") +} + +if (any(!is.na(export_cells$timepoint))) { + timepoint_tcri_summary <- export_cells %>% + filter(!is.na(tcri_score), !is.na(timepoint)) %>% + group_by(timepoint) %>% + summarise( + n_cells = n(), + mean_tcri = mean(tcri_score, na.rm = TRUE), + median_tcri = median(tcri_score, na.rm = TRUE), + n_high = sum(tcri_group == "High", na.rm = TRUE), + frac_high = n_high / n_cells, + .groups = "drop" + ) + save_table_safe(timepoint_tcri_summary, "timepoint_tcri_summary.tsv") +} + +high_tcri_clone_summary <- export_cells %>% + filter(!is.na(clone_id), clone_id != "", !is.na(tcri_group), clone_size >= params$min_clone_size_for_assoc) %>% + group_by(clone_id, clone_size) %>% + summarise( + n_cells = n(), + n_high = sum(tcri_group == "High", na.rm = TRUE), + frac_high = n_high / n_cells, + mean_tcri = mean(tcri_score, na.rm = TRUE), + .groups = "drop" + ) %>% + arrange(desc(frac_high), desc(mean_tcri), desc(clone_size)) %>% + slice_head(n = params$top_n_high_tcri_clones) +save_table_safe(high_tcri_clone_summary, "high_tcri_clone_summary.tsv") + +group_annotation_enrichment <- compute_group_enrichment( + export_cells, + group_col = "tcri_group", + label_col = "annot", + min_cells = params$min_cells_per_group +) +save_table_safe(group_annotation_enrichment, "tcri_group_annotation_enrichment.tsv") +``` + +## overview +```{r} +#| label: overview +kable(summary_rollup, caption = "High-level overview of TCRi analysis.") %>% + kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed")) +``` + +# TCRi score distributions +```{r} +#| label: density-plot +if (isTRUE(params$show_density_plot)) { + p_density <- export_cells %>% + filter(!is.na(tcri_score)) %>% + ggplot(aes(x = tcri_score)) + + geom_density(fill = "steelblue", alpha = 0.5, linewidth = 0.8) + + labs( + title = "Distribution of TCRi scores", + subtitle = "Per-cell TCRi score defined as max predicted phenotype probability.", + x = "TCRi score", + y = "Density" + ) + + theme_scratch_pub(params$base_size) + + p_density + save_plot_safe(p_density, glue("tcri_score_density.{params$figure_format}")) +} +``` + +## tcri-feature-plot +```{r} +#| label: feature-plot +if (isTRUE(params$show_feature_plot)) { + p_feature <- FeaturePlot( + seu, + reduction = reduction_to_use, + features = "tcri_score", + raster = isTRUE(params$raster_large_umap) + ) + + ggtitle("Embedding overlay: TCRi score") + + theme_scratch_pub(params$base_size) + + p_feature + save_plot_safe(p_feature, glue("tcri_feature_plot.{params$figure_format}"), width = 8, height = 6) +} +``` + +# tcri-group-umap +```{r} +#| label: group-umap +if (isTRUE(params$show_group_umap)) { + p_group <- DimPlot( + seu, + reduction = reduction_to_use, + group.by = "tcri_group", + label = FALSE, + raster = isTRUE(params$raster_large_umap) + ) + + ggtitle("Embedding overlay: TCRi group") + + theme_scratch_pub(params$base_size) + + p_group + save_plot_safe(p_group, glue("tcri_group_umap.{params$figure_format}"), width = 8, height = 6) +} +``` + +# TCRi by annotation +```{r} +#| label: violin-by-annotation +if (isTRUE(params$show_violin_by_annotation)) { + annot_order <- annotation_tcri_summary %>% + arrange(desc(mean_tcri)) %>% + pull(annot) + + p_violin_annot <- export_cells %>% + filter(!is.na(tcri_score), !is.na(annot)) %>% + mutate(annot = factor(annot, levels = annot_order)) %>% + ggplot(aes(x = annot, y = tcri_score, fill = annot)) + + geom_violin(scale = "width", trim = TRUE, alpha = 0.8) + + geom_boxplot(width = 0.12, outlier.size = 0.2, fill = "white") + + coord_flip() + + guides(fill = "none") + + labs( + title = "TCRi score by annotation", + subtitle = "Violin + boxplot summary across annotated T-cell states.", + x = NULL, + y = "TCRi score" + ) + + theme_scratch_pub(params$base_size) + + p_violin_annot + save_plot_safe(p_violin_annot, glue("tcri_violin_by_annotation.{params$figure_format}"), width = 10, height = 8) +} + +``` + +# boxplots-by-group TCRi by sample / condition / patient / timepoint +```{r} +#| label: boxplots-by-group +plot_box_by_group <- function(df, group_col, title_txt, filename) { + min_cells_use <- if (!is.null(params$min_cells_per_group) && length(params$min_cells_per_group) == 1) { + params$min_cells_per_group + } else { + 10 + } + + tmp <- df %>% + filter(!is.na(tcri_score), !is.na(.data[[group_col]])) %>% + group_by(.data[[group_col]]) %>% + mutate(group_n = n()) %>% + ungroup() %>% + filter(group_n >= min_cells_use) + + if (nrow(tmp) == 0) return(NULL) + + group_summary <- tmp %>% + group_by(.data[[group_col]]) %>% + summarise(mean_tcri = mean(tcri_score, na.rm = TRUE), .groups = "drop") + + group_levels <- group_summary %>% + arrange(desc(mean_tcri)) %>% + pull(.data[[group_col]]) + + p <- tmp %>% + mutate(.group = factor(.data[[group_col]], levels = group_levels)) %>% + ggplot(aes(x = .group, y = tcri_score, fill = .group)) + + geom_boxplot(outlier.size = 0.3, alpha = 0.85) + + coord_flip() + + guides(fill = "none") + + labs( + title = title_txt, + x = NULL, + y = "TCRi score" + ) + + theme_scratch_pub(params$base_size) + + print(p) + save_plot_safe(p, filename) + invisible(p) +} + + +if (isTRUE(params$show_boxplots_by_sample) && any(!is.na(export_cells$sample))) { + plot_box_by_group(export_cells, "sample", "TCRi score by sample", glue("tcri_boxplot_by_sample.{params$figure_format}")) +} +if (isTRUE(params$show_boxplots_by_condition) && any(!is.na(export_cells$condition))) { + plot_box_by_group(export_cells, "condition", "TCRi score by condition", glue("tcri_boxplot_by_condition.{params$figure_format}")) +} +if (isTRUE(params$show_boxplots_by_patient) && any(!is.na(export_cells$patient))) { + plot_box_by_group(export_cells, "patient", "TCRi score by patient", glue("tcri_boxplot_by_patient.{params$figure_format}")) +} +if (isTRUE(params$show_boxplots_by_timepoint) && any(!is.na(export_cells$timepoint))) { + plot_box_by_group(export_cells, "timepoint", "TCRi score by timepoint", glue("tcri_boxplot_by_timepoint.{params$figure_format}")) +} +``` + +## TCRi and clone size +```{r} +#| label: score-vs-clone-size +if (isTRUE(params$show_score_vs_clone_size)) { + p_clone_assoc <- export_cells %>% + filter(!is.na(tcri_score), !is.na(clone_size), clone_size > 0) %>% + ggplot(aes(x = clone_size, y = tcri_score)) + + geom_point(alpha = 0.35, size = 1.1) + + scale_x_log10() + + geom_smooth(method = "lm", se = TRUE, color = "firebrick") + + labs( + title = "Association between clone size and TCRi score", + subtitle = "Clone size shown on log10 scale.", + x = "Clone size (log10 scale)", + y = "TCRi score" + ) + + theme_scratch_pub(params$base_size) + + p_clone_assoc + save_plot_safe(p_clone_assoc, glue("tcri_vs_clone_size.{params$figure_format}")) +} +``` + +# High-TCRi clone summaries +```{r} +#| label: high-tcri-clone-heatmap + +if (isTRUE(params$show_high_tcri_clone_heatmap) && nrow(high_tcri_clone_summary) > 0) { + + top_clone_ids <- high_tcri_clone_summary$clone_id + + mat_df <- export_cells %>% + filter(clone_id %in% top_clone_ids, !is.na(annot), annot != "") %>% + count(clone_id, annot, name = "n_cells") %>% + tidyr::pivot_wider(names_from = annot, values_from = n_cells, values_fill = 0) %>% + as.data.frame() + + if (nrow(mat_df) > 0 && ncol(mat_df) > 1) { + rownames(mat_df) <- mat_df$clone_id + mat_df$clone_id <- NULL + mat <- as.matrix(mat_df) + + # keep row order consistent with high_tcri_clone_summary where possible + row_order_keep <- intersect(top_clone_ids, rownames(mat)) + mat <- mat[row_order_keep, , drop = FALSE] + + max_mat <- max(mat, na.rm = TRUE) + if (!is.finite(max_mat) || max_mat <= 0) max_mat <- 1 + + ht <- ComplexHeatmap::Heatmap( + mat, + name = "Cells", + col = circlize::colorRamp2( + c(0, max_mat / 2, max_mat), + c("white", "gold", "firebrick") + ), + cluster_rows = TRUE, + cluster_columns = TRUE, + row_names_side = "left", + row_names_gp = grid::gpar(fontsize = 6), + column_names_gp = grid::gpar(fontsize = 9), + column_names_rot = 45, + column_title = "Annotation occupancy of top high-TCRi clonotypes", + heatmap_legend_param = list(title = "Cells"), + width = grid::unit(max(5, ncol(mat) * 0.8), "in"), + height = grid::unit(max(5, nrow(mat) * 0.18), "in") + ) + + ComplexHeatmap::draw( + ht, + heatmap_legend_side = "right" + ) + } else { + cat("High-TCRi clone heatmap skipped: insufficient clone-by-annotation matrix after filtering.") + } +} + +# if (isTRUE(params$show_high_tcri_clone_heatmap) && nrow(high_tcri_clone_summary) > 0) { +# top_clone_ids <- high_tcri_clone_summary$clone_id +# +# mat_df <- export_cells %>% +# filter(clone_id %in% top_clone_ids, !is.na(annot)) %>% +# count(clone_id, annot, name = "n_cells") %>% +# tidyr::pivot_wider(names_from = annot, values_from = n_cells, values_fill = 0) %>% +# as.data.frame() +# +# rownames(mat_df) <- mat_df$clone_id +# mat_df$clone_id <- NULL +# mat <- as.matrix(mat_df) +# +# ht <- Heatmap( +# mat, +# name = "Cells", +# col = colorRamp2( +# c(0, max(mat, na.rm = TRUE) / 2, max(mat, na.rm = TRUE)), +# c("white", "gold", "firebrick") +# ), +# cluster_rows = TRUE, +# cluster_columns = TRUE, +# row_names_side = "left", +# column_title = "Annotation occupancy of top high-TCRi clonotypes", +# heatmap_legend_param = list(title = "Cells") +# ) +# +# draw(ht) +# } +``` + +## TCRi group enrichment across annotations +```{r} +#| label: annotation-enrichment +if (isTRUE(params$show_annotation_enrichment) && nrow(group_annotation_enrichment) > 0) { + enrich_plot_df <- group_annotation_enrichment %>% + filter(!is.na(tcri_group), !is.na(annot)) %>% + mutate(annot = fct_reorder(annot, frac_in_group, .fun = max)) + + p_enrich <- ggplot(enrich_plot_df, aes(x = annot, y = frac_in_group, fill = tcri_group)) + + geom_col(position = "dodge") + + coord_flip() + + scale_y_continuous(labels = percent_format(accuracy = 1)) + + labs( + title = "Annotation enrichment across TCRi groups", + subtitle = "Fraction of each TCRi group represented by each annotation.", + x = NULL, + y = "Fraction within TCRi group", + fill = "TCRi group" + ) + + theme_scratch_pub(params$base_size) + + p_enrich + save_plot_safe(p_enrich, glue("tcri_group_annotation_enrichment.{params$figure_format}"), width = 10, height = 8) +} +``` + +# Summary tables +```{r} +#| label: summary-tables-display +kable(tcri_group_summary, caption = "TCRi group composition.") %>% + kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover", "condensed")) + +kable(annotation_tcri_summary %>% slice_head(n = params$top_n_states_table), + caption = "Top annotations ranked by mean TCRi score.") %>% + kable_styling(full_width = TRUE, bootstrap_options = c("striped", "hover", "condensed", "responsive")) + +if (nrow(high_tcri_clone_summary) > 0) { + kable(high_tcri_clone_summary, + caption = "Top clonotypes enriched for high TCRi scores.") %>% + kable_styling(full_width = TRUE, bootstrap_options = c("striped", "hover", "condensed", "responsive")) +} +``` + +# warnings +```{r} +#| label: warnings +condition_missing <- is.null(condition_col_export) || all(is.na(export_cells$condition)) +patient_missing <- is.null(patient_col_export) || all(is.na(export_cells$patient)) +timepoint_missing <- is.null(timepoint_col_export) || all(is.na(export_cells$timepoint)) + +warn_tbl <- tibble( + warning = c( + "Few cells with TCRi scores", + "Low paired TCR fraction among modeled cells", + "Few phenotype classes after filtering", + "Condition metadata unresolved or empty after export", + "Patient metadata unresolved or empty after export", + "Timepoint metadata unresolved or empty after export" + ), + triggered = c( + mean(!is.na(export_cells$tcri_score)) < 0.5, + mean(export_cells$paired_tcr, na.rm = TRUE) < 0.3, + length(unique(na.omit(export_cells$annot))) < 3, + condition_missing, + patient_missing, + timepoint_missing + ), + interpretation = c( + "Less than half the modeled cells received a TCRi score. Verify Python TCRi execution and cell alignment.", + "Among modeled cells, paired TCR fraction is low. Interpretation should consider incomplete receptor pairing.", + "Very few phenotype classes remained after filtering. Lower min_cells_per_phenotype if biologically appropriate.", + "Condition-level summaries were skipped because condition metadata were not resolved or retained correctly in the exported TCRi table.", + "Patient-level summaries were skipped because patient metadata were not resolved or retained correctly in the exported TCRi table.", + "Timepoint-level summaries were skipped because timepoint metadata were not resolved or retained correctly in the exported TCRi table." + ) +) %>% + filter(triggered) + +if (nrow(warn_tbl) == 0) { + cat("No major automatic warnings were triggered under the current TCRi settings.") +} else { + print( + kable( + warn_tbl %>% select(-triggered), + caption = "Automatically generated TCRi warnings and notes." + ) %>% + kable_styling( + full_width = TRUE, + bootstrap_options = c("striped", "hover", "condensed") + ) + ) +} + +``` + +# session-info +```{r} +#| label: session-info + +dir.create(file.path(params$outdir, params$data_dir), recursive = TRUE, showWarnings = FALSE) +saveRDS(seu, file.path(params$outdir, params$data_dir, "seurat_with_TCRi.rds")) + + +writeLines(capture.output(sessionInfo()), file.path(params$outdir, "sessionInfo.tcri.txt")) +sessionInfo() +``` diff --git a/modules/scratch/TCRI/main.nf b/modules/scratch/TCRI/main.nf new file mode 100644 index 0000000..700a720 --- /dev/null +++ b/modules/scratch/TCRI/main.nf @@ -0,0 +1,70 @@ +process TCRI { + + tag "Running TCRi - ${project_name}" + label 'process_medium' + container "${params.container}" + + publishDir "${params.outdir}/TCRi", mode: 'copy', overwrite: true + + input: + path qmd + path seurat_rds + path export_cells + val project_name + + output: + path "TCRi_Report.html", emit: report_html + path "TCRi_Report/data/seurat_with_TCRi.rds", emit: seurat_with_tcri + path "TCRi_Report/data/*", emit: data + path "TCRi_Report/tables/tcri_export_cells.tsv", emit: export_cells + path "TCRi_Report/tables/*", emit: tables + path "TCRi_Report/figures/*", emit: figures + + script: + def tcri_scores = params.tcri_scores_file ?: "" + + """ + mkdir -p TCRi_Report + mkdir -p .cache/quarto + export XDG_CACHE_HOME="\$PWD/.cache" + export QUARTO_CACHE_DIR="\$PWD/.cache/quarto" + export XDG_DATA_HOME="\$PWD/.cache" + export QUARTO_PRINT_STACK=true + export HOME="\$PWD" + + export LD_LIBRARY_PATH="/opt/conda/envs/tcrenv/lib:\$LD_LIBRARY_PATH" + export RETICULATE_PYTHON="/opt/conda/envs/tcrenv/bin/python" + + echo "Testing Python environment natively..." + /opt/conda/envs/tcrenv/bin/python -c "import tcri; print('PYTHON IMPORT SUCCESS!')" + echo "Starting Quarto Render..." + + quarto render ${qmd} \ + -P python_bin="/opt/conda/envs/tcrenv/bin/python" \ + -P seurat_rds="${seurat_rds}" \ + -P tcr_export_cells_file="${export_cells}" \ + -P tcri_scores_file="${tcri_scores}" \ + -P outdir="TCRi_Report" \ + -P label_col="${params.label_col}" \ + -P sample_col="${params.sample_col}" \ + -P patient_col="${params.patient_col}" \ + -P condition_col="${params.condition_col}" \ + -P timepoint_col="${params.timepoint_col}" \ + -P batch_col="${params.batch_col}" \ + -P reduction_use="${params.reduction_use}" \ + -P make_umap_if_missing=${params.make_umap_if_missing} \ + -P umap_dims_max=${params.umap_dims_max} \ + -P umap_nfeatures=${params.umap_nfeatures} \ + -P raster_large_umap=${params.raster_large_umap} \ + -P tcri_high_cutoff=${params.tcri_high_cutoff} \ + -P tcri_mid_cutoff=${params.tcri_mid_cutoff} \ + -P use_quantile_cutoffs_if_score_not_bounded=${params.tcri_use_quantile_cutoffs} \ + -P high_quantile=${params.tcri_high_quantile} \ + -P mid_quantile=${params.tcri_mid_quantile} \ + -P min_cells_per_group=${params.tcri_min_cells_per_group} \ + -P min_clone_size_for_assoc=${params.tcri_min_clone_size_for_assoc} \ + -P top_n_high_tcri_clones=${params.tcri_top_n_high_clones} \ + -P report_label="${project_name} TCRi" + """ +} + diff --git a/modules/scratch/VDJ_QC/VDJ_QC_analysis.qmd b/modules/scratch/VDJ_QC/VDJ_QC_analysis.qmd new file mode 100644 index 0000000..bd9cbd4 --- /dev/null +++ b/modules/scratch/VDJ_QC/VDJ_QC_analysis.qmd @@ -0,0 +1,2205 @@ +--- +title: "SCRATCH-TCR: VDJ QC Analysis" +author: "Syed Shujaat Ali Zaidi" +format: + html: + toc: true + toc-depth: 4 + number-sections: true + code-fold: true + code-summary: "Show code" + embed-resources: true + theme: cosmo + df-print: paged + smooth-scroll: true + anchor-sections: true +execute: + echo: false + warning: false + message: false +params: + # ====================================================== + # Inputs + # ====================================================== + sample_sheet: "vdj_samples.txt" + metadata_file: "" + outdir: "VDJ_QC" + input_annotated_object: "" + + # ====================================================== + # Sample sheet columns + # sample sheet must minimally contain: + # sample, path + # ====================================================== + sample_sheet_sample_col: "sample" + sample_sheet_path_col: "path" + + # ====================================================== + # Metadata mapping (optional) + # ====================================================== + meta_barcode_col: "barcode" + meta_sample_col: "sample" + meta_patient_col: "patient_id" + meta_condition_col: "condition" + meta_timepoint_col: "timepoint" + + meta_barcode_candidates: !expr c("barcode","cell_id","cell","CB") + meta_sample_candidates: !expr c("sample","sample_id","orig.ident","library_id") + meta_patient_candidates: !expr c("patient_id","patient","subject_id") + meta_condition_candidates: !expr c("condition","group","status","DiseaseStatus") + meta_timepoint_candidates: !expr c("timepoint","time","visit","day") + + # ====================================================== + # QC parameters + # ====================================================== + chain_mode: "auto" # auto / T-AB / T-GD / both + require_productive: true + require_high_conf: true + require_full_length: true + min_umis: 1 + min_reads: 0 + keep_one_per_chain: true + keep_paired_only: false + keep_dual_alpha: true + keep_dual_beta: true + cdr3_aa_min: 8 + cdr3_aa_max: 25 + drop_stop_or_frames: true + top_n_samples_table: 50 + + # ====================================================== + # Plot/report settings + # ====================================================== + figures_dir: "figures" + tables_dir: "tables" + save_tables: true + save_figures: true + figure_format: "png" + figure_width: 10 + figure_height: 7 + figure_dpi: 300 + base_size: 12 + + top_n_v: 20 + top_n_j: 20 + min_clone_size_plot: 1 + + show_reads_umis: true + show_patient_panels: true + show_condition_panels: true + show_timepoint_panels: true + show_clone_rank_plot: true + show_vj_heatmaps: true + + report_label: "VDJ QC Analysis" +--- + + +##SETUP +```{r} +#| label: setup +suppressPackageStartupMessages({ + library(data.table) + library(dplyr) + library(tidyr) + library(stringr) + library(forcats) + library(ggplot2) + library(scales) + library(purrr) + library(glue) + library(knitr) + library(kableExtra) + library(ComplexHeatmap) + library(circlize) + library(patchwork) + library(RColorBrewer) + library(plotly) + # library(DT) + library(htmltools) + library(htmlwidgets) + library(tibble) +}) + +options(stringsAsFactors = FALSE) + +dir.create(params$outdir, recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$figures_dir), recursive = TRUE, showWarnings = FALSE) +dir.create(file.path(params$outdir, params$tables_dir), recursive = TRUE, showWarnings = FALSE) + +`%||%` <- function(x, y) if (!is.null(x) && length(x) > 0 && !all(is.na(x))) x else y + +# theme_scratch_pub <- function(base_size = 12) { +# theme_bw(base_size = base_size) + +# theme( +# plot.title = element_text(face = "bold", size = base_size + 2, hjust = 0), +# plot.subtitle = element_text(size = base_size, hjust = 0), +# axis.title = element_text(face = "bold"), +# axis.text = element_text(color = "black"), +# panel.grid.minor = element_blank(), +# panel.grid.major = element_line(linewidth = 0.2, color = "grey90"), +# strip.background = element_rect(fill = "grey95", color = "grey80"), +# strip.text = element_text(face = "bold"), +# legend.title = element_text(face = "bold"), +# legend.key = element_blank(), +# plot.caption = element_text(size = base_size - 2, color = "grey40") +# ) +# } +# theme_scratch_pub <- function(base_size = 12) { +# theme_bw(base_size = base_size) + +# theme( +# plot.title = element_text(face = "bold", size = base_size + 2, hjust = 0), +# plot.subtitle = element_text(size = base_size, hjust = 0), +# axis.title = element_text(face = "bold", size = base_size - 1), +# # Shrink Y-axis labels slightly +# axis.text.y = element_text(color = "black", size = base_size - 3), +# # Rotate X-axis labels 45 degrees and shrink +# axis.text.x = element_text(color = "black", size = base_size - 3, angle = 45, hjust = 1), +# panel.grid.minor = element_blank(), +# panel.grid.major = element_line(linewidth = 0.1, color = "grey90"), +# strip.background = element_rect(fill = "grey95", color = "grey80"), +# strip.text = element_text(face = "bold", size = base_size - 2), +# legend.title = element_text(face = "bold", size = base_size - 2), +# legend.text = element_text(size = base_size - 3), +# plot.caption = element_text(size = base_size - 4, color = "grey40") +# ) +# } +theme_scratch_pub <- function(base_size = 12) { + theme_bw(base_size = base_size) + + theme( + plot.title = element_text(face = "bold", size = base_size + 3, hjust = 0), + plot.subtitle = element_text(size = base_size, hjust = 0, color = "grey30"), + axis.title = element_text(face = "bold", size = base_size), + axis.text.y = element_text(color = "black", size = base_size - 2), + axis.text.x = element_text(color = "black", size = base_size - 2, angle = 45, hjust = 1), + panel.grid.minor = element_blank(), + panel.grid.major = element_line(linewidth = 0.2, color = "grey92"), + strip.background = element_rect(fill = "#eef5f3", color = "#d9e6e1"), + strip.text = element_text(face = "bold", size = base_size - 1), + legend.title = element_text(face = "bold", size = base_size - 1), + legend.text = element_text(size = base_size - 2), + legend.key = element_blank(), + plot.caption = element_text(size = base_size - 3, color = "grey40"), + plot.margin = margin(10, 12, 10, 10) + ) +} + +save_plot_safe <- function(plot_obj, filename, + width = params$figure_width, + height = params$figure_height, + dpi = params$figure_dpi) { + if (!isTRUE(params$save_figures)) return(invisible(NULL)) + ggsave( + filename = file.path(params$outdir, params$figures_dir, filename), + plot = plot_obj, + width = width, + height = height, + dpi = dpi, + bg = "white", + limitsize = FALSE + ) +} + +save_table_safe <- function(df, filename) { + if (!isTRUE(params$save_tables)) return(invisible(NULL)) + fwrite(df, file.path(params$outdir, params$tables_dir, filename), sep = "\t") +} + +safe_read_table <- function(path) { + if (is.null(path) || is.na(path) || path == "" || !file.exists(path)) return(NULL) + ext <- tolower(tools::file_ext(path)) + if (ext %in% c("tsv", "tab", "txt")) { + fread(path, sep = "\t") + } else { + fread(path) + } +} + +first_existing_col <- function(df, preferred, candidates = character()) { + if (!is.null(preferred) && preferred != "" && preferred %in% colnames(df)) return(preferred) + hits <- intersect(candidates, colnames(df)) + if (length(hits) > 0) return(hits[[1]]) + return(NULL) +} + +require_cols <- function(df, cols, object_name = deparse(substitute(df))) { + miss <- setdiff(cols, colnames(df)) + if (length(miss) > 0) { + stop(glue("{object_name} is missing required columns: {paste(miss, collapse = ', ')}")) + } +} + +has_col <- function(df, col) { + !is.null(col) && col %in% colnames(df) +} + +to_logical <- function(v) { + if (is.logical(v)) return(v) + if (is.numeric(v)) return(v != 0) + if (is.character(v)) return(tolower(trimws(v)) %in% c("true", "t", "yes", "y", "1")) + return(as.logical(v)) +} + +numify <- function(x) suppressWarnings(as.numeric(x)) + +clean_chain <- function(x) { + x <- as.character(x) + x <- toupper(trimws(x)) + x +} + +infer_chain_mode <- function(chain_vals) { + chain_vals <- unique(na.omit(chain_vals)) + has_ab <- any(chain_vals %in% c("TRA","TRB")) + has_gd <- any(chain_vals %in% c("TRG","TRD")) + if (has_ab && !has_gd) return("T-AB") + if (!has_ab && has_gd) return("T-GD") + if (has_ab && has_gd) return("both") + "unknown" +} + +filter_chain_mode <- function(df, chain_col, mode = "auto") { + if (!has_col(df, chain_col)) return(df) + mode_use <- mode + if (mode_use == "auto") mode_use <- infer_chain_mode(df[[chain_col]]) + if (mode_use == "T-AB") return(df %>% filter(.data[[chain_col]] %in% c("TRA","TRB"))) + if (mode_use == "T-GD") return(df %>% filter(.data[[chain_col]] %in% c("TRG","TRD"))) + df +} + +safe_pct <- function(x, denom) ifelse(denom == 0, NA_real_, x / denom) + +coalesce_chr <- function(x, y) ifelse(is.na(x) | x == "", y, x) + +detect_vdj_file <- function(outs_dir, fname) { + roots <- c( + outs_dir, + file.path(outs_dir, "vdj_t"), + file.path(outs_dir, "vdj_b") + ) + fp <- file.path(roots, fname) + i <- which(file.exists(fp))[1] + if (is.na(i)) return(NULL) + fp[[i]] +} + +read_samplesheet <- function(path, sample_col = "sample", path_col = "path") { + stopifnot(file.exists(path)) + df <- safe_read_table(path) %>% as.data.frame() + require_cols(df, c(sample_col, path_col), "sample_sheet") + stopifnot(!anyDuplicated(df[[sample_col]])) + + df <- df %>% + mutate( + sample = as.character(.data[[sample_col]]), + raw_path = as.character(.data[[path_col]]) + ) + + df$outs <- vapply(df$raw_path, function(p) { + p <- normalizePath(p, mustWork = FALSE) + if (dir.exists(file.path(p, "outs"))) { + file.path(p, "outs") + } else if (grepl("/outs/?$", p) && dir.exists(p)) { + p + } else { + stop("Could not find 'outs' directory for path: ", p) + } + }, character(1)) + + df %>% select(sample, outs) %>% distinct() +} + +load_vdj_one <- function(sname, outs_dir) { + contigs_fp <- detect_vdj_file(outs_dir, "filtered_contig_annotations.csv") + clonotypes_fp <- detect_vdj_file(outs_dir, "clonotypes.csv") + metrics_fp <- detect_vdj_file(outs_dir, "metrics_summary.csv") + + if (is.null(contigs_fp)) { + stop("[", sname, "] missing filtered_contig_annotations.csv under ", outs_dir) + } + if (is.null(clonotypes_fp)) { + stop("[", sname, "] missing clonotypes.csv under ", outs_dir) + } + + contigs <- fread(contigs_fp) %>% as.data.frame() + clonotypes <- fread(clonotypes_fp) %>% as.data.frame() + metrics <- NULL + if (!is.null(metrics_fp) && file.exists(metrics_fp)) { + metrics <- tryCatch(fread(metrics_fp) %>% as.data.frame(), error = function(e) NULL) + } + + contigs$sample <- sname + clonotypes$sample <- sname + if (!is.null(metrics)) metrics$sample <- sname + + list( + contigs = contigs, + clonotypes = clonotypes, + metrics = metrics + ) +} + + +metric_card <- function(title, value, subtitle = NULL, color = "#eef5f3") { + htmltools::div( + style = paste0( + "background:", color, "; border:1px solid #d8e3df; border-radius:12px; ", + "padding:14px 16px; margin:8px 8px 8px 0; min-width:200px; display:inline-block;" + ), + htmltools::tags$div(style = "font-size:13px; color:#4b5b57; font-weight:600;", title), + htmltools::tags$div(style = "font-size:28px; font-weight:700; color:#1f2d2a;", value), + if (!is.null(subtitle)) htmltools::tags$div(style = "font-size:12px; color:#6b7c77;", subtitle) + ) +} + + +scroll_kable <- function(df, caption = NULL, height = "360px", font_size = 11) { + kableExtra::kbl(df, caption = caption) %>% + kableExtra::kable_styling( + full_width = FALSE, + bootstrap_options = c("striped","hover","condensed"), + font_size = font_size + ) %>% + kableExtra::scroll_box(width = "100%", height = height) +} + +save_widget_safe <- function(widget, filename) { + if (!isTRUE(params$save_figures)) return(invisible(NULL)) + htmlwidgets::saveWidget( + widget = widget, + file = file.path(params$outdir, params$figures_dir, filename), + selfcontained = TRUE + ) +} + + +ggplotly_clean <- function(p, tooltip = c("x", "y")) { + plotly::ggplotly(p, tooltip = tooltip) %>% + plotly::layout( + margin = list(l = 60, r = 30, b = 60, t = 60), + legend = list(orientation = "v") + ) +} + +``` + +#read input +```{r} +#| label: read-inputs + +sample_sheet <- read_samplesheet( + params$sample_sheet, + sample_col = params$sample_sheet_sample_col, + path_col = params$sample_sheet_path_col +) + +metadata_tbl <- safe_read_table(params$metadata_file) + +if ((is.null(metadata_tbl) || nrow(metadata_tbl) == 0) && + !is.null(params$input_annotated_object) && + params$input_annotated_object != "" && + !grepl("NO_FILE", basename(params$input_annotated_object)) && + file.exists(params$input_annotated_object)) { + + suppressPackageStartupMessages({ + library(Seurat) + library(tibble) + library(dplyr) + }) + + seu <- readRDS(params$input_annotated_object) + + metadata_tbl <- seu@meta.data %>% + tibble::rownames_to_column("cell_id_full") %>% + mutate( + sample = as.character(orig.ident), + barcode = if ("raw_cell_id" %in% colnames(.)) { + sub("-1$", "", as.character(raw_cell_id)) + } else { + sub("^[^_]+_", "", cell_id_full) |> sub("-1$", "", x = _) + }, + patient_id = if ("patient_id" %in% colnames(.)) as.character(patient_id) else NA_character_, + condition = if ("condition" %in% colnames(.)) as.character(condition) else NA_character_, + timepoint = if ("timepoint" %in% colnames(.)) as.character(timepoint) else NA_character_ + ) + + cat("\nLoaded metadata from annotated object\n") + print(colnames(metadata_tbl)) + print(head(metadata_tbl[, c("sample","barcode","patient_id","condition","timepoint")], 10)) +} + +``` + +##load files VDJ +```{r} +#| label: load-vdj +vdj_list <- lapply(seq_len(nrow(sample_sheet)), function(i) { + load_vdj_one(sample_sheet$sample[i], sample_sheet$outs[i]) +}) +names(vdj_list) <- sample_sheet$sample + +contigs_all_pre <- rbindlist(lapply(vdj_list, `[[`, "contigs"), use.names = TRUE, fill = TRUE) %>% as.data.frame() +clonotypes_all <- rbindlist(lapply(vdj_list, `[[`, "clonotypes"), use.names = TRUE, fill = TRUE) %>% as.data.frame() + +metrics_list <- lapply(vdj_list, `[[`, "metrics") +metrics_list <- metrics_list[!vapply(metrics_list, is.null, logical(1))] +metrics_all <- if (length(metrics_list) > 0) { + rbindlist(metrics_list, use.names = TRUE, fill = TRUE) %>% as.data.frame() +} else { + NULL +} +``` + +## validate-standardize +```{r} +#| label: validate-and-standardize +need_cols <- c("barcode", "chain", "cdr3", "productive") +require_cols(contigs_all_pre, need_cols, "contigs_all_pre") + +contigs_all_pre <- contigs_all_pre %>% + mutate( + sample = as.character(sample), + barcode = as.character(barcode), + cell_id = as.character(barcode), + chain = clean_chain(chain), + productive = if ("productive" %in% names(.)) to_logical(productive) else productive, + high_confidence = if ("high_confidence" %in% names(.)) to_logical(high_confidence) else high_confidence, + full_length = if ("full_length" %in% names(.)) to_logical(full_length) else full_length, + umis = if ("umis" %in% names(.)) numify(umis) else NA_real_, + reads = if ("reads" %in% names(.)) numify(reads) else NA_real_, + cdr3 = as.character(cdr3), + cdr3_nt = if ("cdr3_nt" %in% names(.)) as.character(cdr3_nt) else NA_character_, + v_gene = if ("v_gene" %in% names(.)) as.character(v_gene) else NA_character_, + j_gene = if ("j_gene" %in% names(.)) as.character(j_gene) else NA_character_, + raw_clonotype_id = if ("raw_clonotype_id" %in% names(.)) as.character(raw_clonotype_id) else if ("clonotype_id" %in% names(.)) as.character(clonotype_id) else NA_character_ + ) +``` + +##merge-optional-metadata +```{r} +#| label: merge-optional-metadata + +contigs_all_pre$patient_id <- NA_character_ +contigs_all_pre$condition <- NA_character_ +contigs_all_pre$timepoint <- NA_character_ + +if (!is.null(metadata_tbl) && nrow(metadata_tbl) > 0) { + message("--- Merging VDJ contigs with GEX metadata ---") + + md_sample_col <- "sample" + md_barcode_col <- "barcode" + md_patient_col <- if ("patient_id" %in% colnames(metadata_tbl)) "patient_id" else NULL + md_condition_col <- if ("condition" %in% colnames(metadata_tbl)) "condition" else NULL + md_timepoint_col <- if ("timepoint" %in% colnames(metadata_tbl)) "timepoint" else NULL + + metadata_prep <- metadata_tbl %>% + mutate( + md_sample = as.character(.data[[md_sample_col]]), + md_barcode = sub("-1$", "", as.character(.data[[md_barcode_col]])) + ) + + # barcode + sample level mapping + barcode_meta_map <- metadata_prep %>% + transmute( + sample = md_sample, + barcode_clean = md_barcode, + patient_id_join = if (!is.null(md_patient_col)) as.character(.data[[md_patient_col]]) else NA_character_, + condition_join = if (!is.null(md_condition_col)) as.character(.data[[md_condition_col]]) else NA_character_, + timepoint_join = if (!is.null(md_timepoint_col)) as.character(.data[[md_timepoint_col]]) else NA_character_ + ) %>% + distinct(sample, barcode_clean, .keep_all = TRUE) + + # sample-level fallback + sample_meta_map <- metadata_prep %>% + transmute( + sample = md_sample, + patient_id_sample = if (!is.null(md_patient_col)) as.character(.data[[md_patient_col]]) else NA_character_, + condition_sample = if (!is.null(md_condition_col)) as.character(.data[[md_condition_col]]) else NA_character_, + timepoint_sample = if (!is.null(md_timepoint_col)) as.character(.data[[md_timepoint_col]]) else NA_character_ + ) %>% + distinct(sample, .keep_all = TRUE) + + contigs_all_pre <- contigs_all_pre %>% + mutate( + sample = as.character(sample), + barcode_clean = sub("-1$", "", as.character(barcode)) + ) %>% + left_join(barcode_meta_map, by = c("sample", "barcode_clean")) %>% + left_join(sample_meta_map, by = "sample") %>% + mutate( + patient_id = dplyr::coalesce(patient_id_join, patient_id_sample), + condition = dplyr::coalesce(condition_join, condition_sample), + timepoint = dplyr::coalesce(timepoint_join, timepoint_sample) + ) %>% + select(-any_of(c( + "barcode_clean", + "patient_id_join", "condition_join", "timepoint_join", + "patient_id_sample", "condition_sample", "timepoint_sample" + ))) + + message(sprintf( + "Merged metadata: patient_id non-NA = %d / %d ; timepoint non-NA = %d / %d", + sum(!is.na(contigs_all_pre$patient_id)), nrow(contigs_all_pre), + sum(!is.na(contigs_all_pre$timepoint)), nrow(contigs_all_pre) + )) +} +``` + +# metadata-debug +```{r} +#| label: metadata-debug +cat("\n===== DEBUG MERGED METADATA =====\n") +print( + contigs_all_pre %>% + select(sample, barcode, patient_id, condition, timepoint) %>% + distinct() %>% + head(20) +) + + +cat("\nPer-sample merged metadata summary:\n") +print( + contigs_all_pre %>% + group_by(sample) %>% + summarise( + patient_vals = paste(unique(na.omit(patient_id)), collapse = ";"), + condition_vals = paste(unique(na.omit(condition)), collapse = ";"), + timepoint_vals = paste(unique(na.omit(timepoint)), collapse = ";"), + .groups = "drop" + ) +) +``` + +##chain mode +```{r} +#| label: chain-mode +detected_chain_mode <- infer_chain_mode(contigs_all_pre$chain) +effective_chain_mode <- if (params$chain_mode == "auto") detected_chain_mode else params$chain_mode + +contigs_all_pre <- filter_chain_mode(contigs_all_pre, "chain", params$chain_mode) +``` + +##QC-filtering +```{r} +#| label: qc-filtering +contigs_all_pre <- contigs_all_pre %>% + mutate( + cdr3_len = nchar(cdr3), + has_stop = ifelse(is.na(cdr3), NA, str_detect(cdr3, "\\*")), + has_frame_issue = ifelse(is.na(cdr3_nt), FALSE, nchar(cdr3_nt) %% 3 != 0) + ) + +contigs_all_post <- contigs_all_pre %>% + { + x <- . + if (params$require_productive && "productive" %in% names(x)) { + x <- x %>% filter(productive) + } + if (params$require_high_conf && "high_confidence" %in% names(x)) { + x <- x %>% filter(high_confidence) + } + if (params$require_full_length && "full_length" %in% names(x)) { + x <- x %>% filter(full_length) + } + if ("umis" %in% names(x)) { + x <- x %>% filter(is.na(umis) | umis >= params$min_umis) + } + if ("reads" %in% names(x)) { + x <- x %>% filter(is.na(reads) | reads >= params$min_reads) + } + + x <- x %>% filter(!is.na(cdr3), cdr3 != "") + + if (isTRUE(params$drop_stop_or_frames)) { + x <- x %>% filter(!grepl("\\*", cdr3)) + if ("cdr3_nt" %in% names(x)) { + x <- x %>% filter(is.na(cdr3_nt) | nchar(cdr3_nt) %% 3 == 0) + } + } + + x <- x %>% + filter( + nchar(cdr3) >= params$cdr3_aa_min, + nchar(cdr3) <= params$cdr3_aa_max + ) + + if (isTRUE(params$keep_one_per_chain)) { + order_vars <- intersect(c("umis", "reads"), names(x)) + if (length(order_vars) > 0) { + x <- x %>% + group_by(sample, barcode, chain) %>% + arrange(across(all_of(order_vars), desc), .by_group = TRUE) %>% + slice(1) %>% + ungroup() + } else { + x <- x %>% distinct(sample, barcode, chain, .keep_all = TRUE) + } + } + + x + } + +if (!isTRUE(params$keep_dual_alpha)) { + contigs_all_post <- contigs_all_post %>% + group_by(sample, barcode) %>% + filter(!(chain == "TRA" & row_number() > 1)) %>% + ungroup() +} + +if (!isTRUE(params$keep_dual_beta)) { + contigs_all_post <- contigs_all_post %>% + group_by(sample, barcode) %>% + filter(!(chain == "TRB" & row_number() > 1)) %>% + ungroup() +} + +if (isTRUE(params$keep_paired_only)) { + if (effective_chain_mode == "T-AB") { + contigs_all_post <- contigs_all_post %>% + group_by(sample, barcode) %>% + filter(any(chain == "TRA") & any(chain == "TRB")) %>% + ungroup() + } else if (effective_chain_mode == "T-GD") { + contigs_all_post <- contigs_all_post %>% + group_by(sample, barcode) %>% + filter(any(chain == "TRG") & any(chain == "TRD")) %>% + ungroup() + } +} +``` + + +## removed-contigs +```{r} +#| label: removed-contigs +key_cols <- intersect( + c("sample","barcode","chain","cdr3","cdr3_nt","v_gene","j_gene","raw_clonotype_id"), + colnames(contigs_all_pre) +) + +if (length(key_cols) == 0) { + removed <- contigs_all_pre[0, , drop = FALSE] +} else { + removed <- dplyr::anti_join( + contigs_all_pre, + contigs_all_post, + by = key_cols + ) +} +``` + + +## QC-Summary before-after +```{r} +#| label: qc-summary-before-after +qc_summary <- contigs_all_pre %>% + mutate(stage = "before") %>% + bind_rows(contigs_all_post %>% mutate(stage = "after")) %>% + group_by(sample, stage) %>% + summarise( + cells_with_any = n_distinct(barcode), + contigs = n(), + mean_contigs_per_cell = contigs / cells_with_any, + .groups = "drop" + ) %>% + pivot_wider( + names_from = stage, + values_from = c(cells_with_any, contigs, mean_contigs_per_cell) + ) + +before_df <- contigs_all_pre %>% count(sample, name = "contigs_before") +after_df <- contigs_all_post %>% count(sample, name = "contigs_after") + +qc_retention <- before_df %>% + left_join(after_df, by = "sample") %>% + mutate( + contigs_after = replace_na(contigs_after, 0L), + dropped = pmax(contigs_before - contigs_after, 0L), + retained_prop = ifelse(contigs_before > 0, contigs_after / contigs_before, NA_real_), + retained_pct = 100 * retained_prop + ) %>% + arrange(desc(contigs_before)) +``` + +### Pairing Summary +```{r} +#| label: pairing-summary +pairing_all <- contigs_all_post %>% + group_by(sample, cell_id = barcode) %>% + summarise( + patient_id = first(na.omit(patient_id)) %||% NA_character_, + condition = first(na.omit(condition)) %||% NA_character_, + timepoint = first(na.omit(timepoint)) %||% NA_character_, + n_prod = n(), + n_TRA = sum(chain == "TRA"), + n_TRB = sum(chain == "TRB"), + n_TRG = sum(chain == "TRG"), + n_TRD = sum(chain == "TRD"), + has_TRA = any(chain == "TRA"), + has_TRB = any(chain == "TRB"), + has_TRG = any(chain == "TRG"), + has_TRD = any(chain == "TRD"), + alpha_ct = sum(chain == "TRA"), + beta_ct = sum(chain == "TRB"), + gamma_ct = sum(chain == "TRG"), + delta_ct = sum(chain == "TRD"), + cdr3a = paste(unique(cdr3[chain == "TRA"]), collapse = ";"), + cdr3b = paste(unique(cdr3[chain == "TRB"]), collapse = ";"), + cdr3g = paste(unique(cdr3[chain == "TRG"]), collapse = ";"), + cdr3d = paste(unique(cdr3[chain == "TRD"]), collapse = ";"), + trav = paste(unique(v_gene[chain == "TRA"]), collapse = ";"), + trbv = paste(unique(v_gene[chain == "TRB"]), collapse = ";"), + trgv = paste(unique(v_gene[chain == "TRG"]), collapse = ";"), + trdv = paste(unique(v_gene[chain == "TRD"]), collapse = ";"), + traj = paste(unique(j_gene[chain == "TRA"]), collapse = ";"), + trbj = paste(unique(j_gene[chain == "TRB"]), collapse = ";"), + trgj = paste(unique(j_gene[chain == "TRG"]), collapse = ";"), + trdj = paste(unique(j_gene[chain == "TRD"]), collapse = ";"), + clone_id = first(na.omit(raw_clonotype_id)) %||% NA_character_, + max_reads = suppressWarnings(max(reads, na.rm = TRUE)), + max_umis = suppressWarnings(max(umis, na.rm = TRUE)), + .groups = "drop" + ) %>% + mutate( + pairing = case_when( + effective_chain_mode == "T-AB" & has_TRA & has_TRB ~ "Alpha+Beta", + effective_chain_mode == "T-AB" & has_TRA & !has_TRB ~ "Alpha_only", + effective_chain_mode == "T-AB" & !has_TRA & has_TRB ~ "Beta_only", + effective_chain_mode == "T-GD" & has_TRG & has_TRD ~ "Gamma+Delta", + effective_chain_mode == "T-GD" & has_TRG & !has_TRD ~ "Gamma_only", + effective_chain_mode == "T-GD" & !has_TRG & has_TRD ~ "Delta_only", + effective_chain_mode == "both" & (has_TRA & has_TRB) ~ "Alpha+Beta", + effective_chain_mode == "both" & (has_TRG & has_TRD) ~ "Gamma+Delta", + TRUE ~ "Other" + ), + multi_alpha = alpha_ct > 1, + multi_beta = beta_ct > 1, + multi_gamma = gamma_ct > 1, + multi_delta = delta_ct > 1 + ) +``` + + + +##CDR3 lengths +```{r} +#| label: cdr3-lengths +cdr3_len_all <- contigs_all_post %>% + filter(!is.na(cdr3), cdr3 != "") %>% + mutate( + len = nchar(cdr3), + chain_grp = case_when( + chain == "TRA" ~ "TRA", + chain == "TRB" ~ "TRB", + chain == "TRG" ~ "TRG", + chain == "TRD" ~ "TRD", + TRUE ~ "Other" + ) + ) +``` + + +##vj-usage +```{r} +#| label: vj-usage-and-clone-rank +v_usage <- NULL +j_usage <- NULL + +if (isTRUE(params$show_vj_heatmaps) && any(!is.na(contigs_all_post$v_gene))) { + v_usage <- contigs_all_post %>% + filter(!is.na(v_gene), v_gene != "") %>% + count(sample, v_gene, name = "n") %>% + group_by(sample) %>% + mutate(freq = n / sum(n)) %>% + ungroup() + + top_v <- v_usage %>% + group_by(v_gene) %>% + summarise(total = sum(n), .groups = "drop") %>% + arrange(desc(total)) %>% + slice_head(n = params$top_n_v) %>% + pull(v_gene) + + v_usage <- v_usage %>% filter(v_gene %in% top_v) +} + +if (isTRUE(params$show_vj_heatmaps) && any(!is.na(contigs_all_post$j_gene))) { + j_usage <- contigs_all_post %>% + filter(!is.na(j_gene), j_gene != "") %>% + count(sample, j_gene, name = "n") %>% + group_by(sample) %>% + mutate(freq = n / sum(n)) %>% + ungroup() + + top_j <- j_usage %>% + group_by(j_gene) %>% + summarise(total = sum(n), .groups = "drop") %>% + arrange(desc(total)) %>% + slice_head(n = params$top_n_j) %>% + pull(j_gene) + + j_usage <- j_usage %>% filter(j_gene %in% top_j) +} + +clone_rank_tbl <- NULL +if (isTRUE(params$show_clone_rank_plot)) { + clone_rank_tbl <- pairing_all %>% + filter(!is.na(clone_id), clone_id != "") %>% + count(sample, clone_id, name = "n_cells") %>% + group_by(sample) %>% + arrange(desc(n_cells), .by_group = TRUE) %>% + mutate(rank = row_number()) %>% + ungroup() %>% + filter(n_cells >= params$min_clone_size_plot) +} +``` + +##Compact Summary +```{r} +#| label: compact-sample-summary +pairing_rates <- pairing_all %>% + count(sample, pairing, name = "cells") %>% + group_by(sample) %>% + mutate(frac = cells / sum(cells)) %>% + pivot_wider(names_from = pairing, values_from = c(cells, frac), values_fill = 0) %>% + ungroup() + +sample_qc <- pairing_all %>% + group_by(sample) %>% + summarise( + patient_id = first(na.omit(patient_id)) %||% NA_character_, + condition = first(na.omit(condition)) %||% NA_character_, + timepoint = first(na.omit(timepoint)) %||% NA_character_, + n_cells_total = n(), + n_cells_paired = sum(pairing %in% c("Alpha+Beta","Gamma+Delta")), + n_cells_multichain = sum(multi_alpha | multi_beta | multi_gamma | multi_delta, na.rm = TRUE), + pct_cells_paired = safe_pct(n_cells_paired, n_cells_total), + pct_cells_multichain = safe_pct(n_cells_multichain, n_cells_total), + n_unique_clones = n_distinct(clone_id[!is.na(clone_id) & clone_id != ""]), + median_max_reads = suppressWarnings(median(max_reads[is.finite(max_reads)], na.rm = TRUE)), + median_max_umis = suppressWarnings(median(max_umis[is.finite(max_umis)], na.rm = TRUE)), + .groups = "drop" + ) %>% + left_join(qc_summary, by = "sample") %>% + left_join(qc_retention, by = "sample") %>% + left_join(pairing_rates, by = "sample") +``` + + +## Optional metadata summaries +```{r} +#| label: metadata-panels +show_patient <- isTRUE(params$show_patient_panels) && any(!is.na(sample_qc$patient_id)) +show_condition <- isTRUE(params$show_condition_panels) && any(!is.na(sample_qc$condition)) +# show_timepoint <- isTRUE(params$show_timepoint_panels) && any(!is.na(sample_qc$timepoint)) +show_timepoint <- isTRUE(params$show_timepoint_panels) && + any(!is.na(sample_qc$timepoint)) && + dplyr::n_distinct(na.omit(sample_qc$timepoint)) >= 2 +``` + +##save outputs +```{r} +#| label: save-tables +save_table_safe(sample_sheet, "sample_sheet_resolved.tsv") +save_table_safe(contigs_all_pre, "contigs_before_qc.tsv") +save_table_safe(contigs_all_post, "contigs_after_qc.tsv") +save_table_safe(removed, "contigs_removed_by_qc.tsv") +save_table_safe(qc_summary, "vdj_qc_summary_before_after.tsv") +save_table_safe(qc_retention, "qc_contigs_before_after_summary.tsv") +save_table_safe(pairing_all, "pairing_status_all.tsv") +save_table_safe(sample_qc, "vdj_qc_per_sample_compact.tsv") +save_table_safe(cdr3_len_all, "cdr3_lengths_all.tsv") +save_table_safe(clonotypes_all, "clonotypes_all.tsv") +if (!is.null(metrics_all)) save_table_safe(metrics_all, "vdj_metrics_summary_all.tsv") +if (!is.null(v_usage)) save_table_safe(v_usage, "v_gene_usage.tsv") +if (!is.null(j_usage)) save_table_safe(j_usage, "j_gene_usage.tsv") +if (!is.null(clone_rank_tbl)) save_table_safe(clone_rank_tbl, "clone_rank_abundance.tsv") +``` + + + +## What this module does + +::: {.callout-note} +This module performs quality control on Cell Ranger VDJ contig outputs before downstream clonotype analysis. +It standardizes contig annotations, merges optional sample-level metadata, applies explicit QC filters, and summarizes the retained receptor repertoire at the contig, cell, and sample levels. +This matters because downstream tools such as GLIPH2, TCRdist3, GIANA, repertoire analysis, and consensus clustering are only as reliable as the receptor calls entering them. +The report is based primarily on filtered contig annotations, clonotype assignments, and optional sample metadata, and it evaluates retention, pairing, multi-chain burden, CDR3 properties, V/J usage, and clone size structure. +::: + +## How to read this report + +::: {.callout-tip} +- **Higher contig retention** usually indicates that most receptor calls survived the QC rules and were technically consistent with the expected assay. +- **Higher paired-chain fraction** is generally desirable for alpha-beta or gamma-delta analyses because paired receptors are more interpretable biologically. +- **High multi-chain burden** can reflect true biology in some cases, but can also indicate doublets, ambient contamination, or ambiguous contig recovery. +- **CDR3 length distributions** should look biologically plausible and fairly consistent across samples; strong shifts or extreme tails are potential red flags. +- **Clone rank-abundance curves** reveal whether repertoires are broad versus dominated by a few expanded clonotypes. +- **V/J usage heatmaps** help spot sample-specific biases, batch effects, or biologically enriched receptor programs. +::: + +## Red flags and biologically interesting patterns + +::: {.callout-warning} +**Potential red flags** +- very low retention after QC +- very low paired fraction +- unusually high multi-chain burden +- missing read or UMI support fields +- strong outlier samples in CDR3 length or V/J usage patterns + +**Potentially interesting biology** +- strong clonal dominance in a subset of samples +- reproducible V/J enrichment across related samples +- distinct pairing behavior by patient, condition, or timepoint +- broad versus focused repertoire structure across the cohort +::: + + + +## Overview +```{r} +#| label: Key summary metrics +#| results: asis + +n_samples <- n_distinct(sample_sheet$sample) +n_contigs_before <- nrow(contigs_all_pre) +n_contigs_after <- nrow(contigs_all_post) +n_cells_after <- n_distinct(pairing_all$cell_id) +pct_retained_num <- n_contigs_after / max(1, n_contigs_before) +pct_retained <- percent(pct_retained_num, accuracy = 0.1) + +paired_frac <- mean(pairing_all$pairing %in% c("Alpha+Beta","Gamma+Delta"), na.rm = TRUE) +multichain_frac <- mean(pairing_all$multi_alpha | pairing_all$multi_beta | pairing_all$multi_gamma | pairing_all$multi_delta, na.rm = TRUE) + +top_sample_by_cells <- sample_qc %>% + arrange(desc(n_cells_total)) %>% + slice_head(n = 1) %>% + pull(sample) + +top_sample_by_retention <- qc_retention %>% + arrange(desc(retained_prop)) %>% + slice_head(n = 1) %>% + pull(sample) + +htmltools::browsable( + htmltools::tagList( + metric_card("Resolved samples", comma(n_samples), "libraries in sample sheet"), + metric_card("Contigs before QC", comma(n_contigs_before), "raw filtered contigs"), + metric_card("Contigs after QC", comma(n_contigs_after), "passing QC"), + metric_card("Retention rate", pct_retained, "fraction of contigs retained"), + metric_card("Cells after QC", comma(n_cells_after), "unique barcodes retained"), + metric_card("Paired-chain fraction", percent(paired_frac, accuracy = 0.1), "post-QC cells"), + metric_card("Multi-chain burden", percent(multichain_frac, accuracy = 0.1), "cells with >1 productive chain type"), + metric_card("Detected chain mode", detected_chain_mode, "inferred from contigs"), + metric_card("Effective chain mode", effective_chain_mode, "used for filtering"), + metric_card("Top sample by cells", top_sample_by_cells %||% "NA", "largest retained sample"), + metric_card("Top sample by retention", top_sample_by_retention %||% "NA", "highest QC retention") + ) +) + +overview_tbl <- tibble( + Metric = c( + "Report label", + "Resolved samples", + "Detected chain mode", + "Effective chain mode", + "Contigs before QC", + "Contigs after QC", + "Retention rate", + "Cells after QC", + "Unique clonotypes loaded" + ), + Value = c( + params$report_label, + comma(n_samples), + detected_chain_mode, + effective_chain_mode, + comma(n_contigs_before), + comma(n_contigs_after), + pct_retained, + comma(n_cells_after), + comma(nrow(clonotypes_all)) + ) +) +print( + scroll_kable( + overview_tbl, + caption = "High-level overview of VDJ QC analysis.", + height = "260px" + ) +) + +save_table_safe(overview_tbl, "vdj_qc_overview_metrics.tsv") +# kable(overview_tbl, caption = "High-level overview of VDJ QC analysis.") %>% +# kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover","condensed")) +``` + +##sample-level Summary +```{r} +#| label: sample-summary-table +#| results: asis + +# print(colnames(sample_qc)) + +top_n_samples_table <- params$top_n_samples_table +if (is.null(top_n_samples_table) || is.na(top_n_samples_table)) { + top_n_samples_table <- 50 +} +top_n_samples_table <- as.integer(top_n_samples_table) + +sample_qc_display <- sample_qc %>% + mutate( + retained_pct = sprintf("%.1f%%", retained_pct), + pct_cells_paired = percent(pct_cells_paired, accuracy = 0.1), + pct_cells_multichain = percent(pct_cells_multichain, accuracy = 0.1) + ) + +if ("contigs_after" %in% colnames(sample_qc_display)) { + sample_qc_display <- sample_qc_display %>% + arrange(desc(contigs_after)) +} else { + sample_qc_display <- sample_qc_display %>% + arrange(desc(n_cells_total)) +} + +sample_qc_display <- sample_qc_display %>% + slice_head(n = top_n_samples_table) + +print( + scroll_kable( + sample_qc_display, + caption = "Per-sample VDJ QC compact summary.", + height = "420px", + font_size = 11 + ) +) + +# kable(sample_qc_display, caption = "Per-sample VDJ QC compact summary.") %>% +# kable_styling(full_width = TRUE, bootstrap_options = c("striped","hover","condensed","responsive")) + +# #| label: sample-summary-table +# print(colnames(sample_qc)) +# +# sample_qc_display <- sample_qc %>% +# mutate( +# retained_pct = sprintf("%.1f%%", retained_pct), +# pct_cells_paired = percent(pct_cells_paired, accuracy = 0.1), +# pct_cells_multichain = percent(pct_cells_multichain, accuracy = 0.1) +# ) +# +# if ("contigs_after" %in% colnames(sample_qc_display)) { +# sample_qc_display <- sample_qc_display %>% +# arrange(desc(contigs_after)) +# } else { +# sample_qc_display <- sample_qc_display %>% +# arrange(desc(n_cells_total)) +# } +# +# sample_qc_display <- sample_qc_display %>% +# slice_head(n = params$top_n_samples_table) +# +# kable(sample_qc_display, caption = "Per-sample VDJ QC compact summary.") %>% +# kable_styling(full_width = TRUE, bootstrap_options = c("striped","hover","condensed","responsive")) +``` + +## QC figure panels {.tabset} + +### Retention and support + +::: {.callout-note} +These plots summarize how strongly QC filtering affected each sample and whether retained contigs have adequate read and UMI support. +::: + + +```{r} +#| label: before-after-figures +p_before_counts <- qc_retention %>% + ggplot(aes(x = reorder(sample, contigs_before), y = contigs_before)) + + geom_col(fill = "grey50") + + coord_flip() + + scale_y_continuous(labels = comma) + + labs(title = "Contigs Before QC", x = NULL, y = "Contigs") + + theme_scratch_pub(params$base_size) + +p_after_counts <- qc_retention %>% + ggplot(aes(x = reorder(sample, contigs_before), y = contigs_after)) + + geom_col(fill = "steelblue") + + coord_flip() + + scale_y_continuous(labels = comma) + + labs(title = "Contigs After QC", x = NULL, y = "Contigs") + + theme_scratch_pub(params$base_size) + +p_retained <- qc_retention %>% + ggplot(aes(x = reorder(sample, retained_prop), y = retained_pct)) + + geom_col(fill = "darkgreen") + + coord_flip() + + geom_text(aes(label = sprintf("%.1f%%", retained_pct)), hjust = 1.05, size = 3, color = "white") + + scale_y_continuous(limits = c(0, 100)) + + labs(title = "Retention After QC", x = NULL, y = "Retained (%)") + + theme_scratch_pub(params$base_size) + +patch_before_after <- p_before_counts + p_after_counts + p_retained + plot_layout(ncol = 3) +patch_before_after +save_plot_safe(patch_before_after, glue("qc_before_after_retention.{params$figure_format}"), width = 16, height = 5) +``` + +```{r} +#| label: read-umi-plots +if (isTRUE(params$show_reads_umis) && any(!is.na(contigs_all_post$reads))) { + p_reads <- contigs_all_post %>% + filter(!is.na(reads), reads > 0) %>% + ggplot(aes(x = reads, fill = chain)) + + geom_histogram(bins = 50, alpha = 0.8, position = "identity") + + scale_x_log10(labels = label_number(scale_cut = cut_short_scale())) + + labs( + title = "Read Support of Passing Contigs", + subtitle = "Histogram shown on log10 scale.", + x = "Reads", + y = "Contig count", + fill = "Chain" + ) + + theme_scratch_pub(params$base_size) + ggplotly_clean(p_reads) + # print(p_reads) + save_plot_safe(p_reads, glue("reads_support_histogram.{params$figure_format}")) +} + +if (isTRUE(params$show_reads_umis) && any(!is.na(contigs_all_post$umis))) { + p_umis <- contigs_all_post %>% + filter(!is.na(umis), umis > 0) %>% + ggplot(aes(x = umis, fill = chain)) + + geom_histogram(bins = 50, alpha = 0.8, position = "identity") + + scale_x_log10(labels = label_number(scale_cut = cut_short_scale())) + + labs( + title = "UMI Support of Passing Contigs", + subtitle = "Histogram shown on log10 scale.", + x = "UMIs", + y = "Contig count", + fill = "Chain" + ) + + theme_scratch_pub(params$base_size) + ggplotly_clean(p_umis) + # print(p_umis) + save_plot_safe(p_umis, glue("umis_support_histogram.{params$figure_format}")) +} +``` + + +### Pairing and chain structure + +::: {.callout-note} +Paired-chain recovery is one of the most important QC outcomes for TCR analysis. High paired fractions support reliable receptor interpretation, while elevated single-chain or multi-chain patterns can indicate technical dropout or ambiguous receptor assignment. +::: + +#### Pairing status by sample +```{r} +#| label: pairing-plot-dropdown +#| warning: false +#| message: false + +pairing_plot_tbl <- pairing_all %>% + count(sample, pairing, name = "n") %>% + mutate( + pairing = factor(pairing, levels = unique(pairing)) + ) + +samples_pair <- sort(unique(pairing_plot_tbl$sample)) +pairing_levels <- levels(pairing_plot_tbl$pairing) + +# make sure every sample has all pairing categories +pairing_plot_tbl_complete <- tidyr::expand_grid( + sample = samples_pair, + pairing = pairing_levels +) %>% + left_join(pairing_plot_tbl, by = c("sample", "pairing")) %>% + mutate(n = ifelse(is.na(n), 0, n)) + +pairing_colors <- c( + "Alpha_only" = "#F8766D", + "Alpha+Beta" = "#00BA38", + "Beta_only" = "#619CFF" +) + +# fallback if labels differ slightly +for (nm in unique(as.character(pairing_plot_tbl_complete$pairing))) { + if (!nm %in% names(pairing_colors)) { + pairing_colors[[nm]] <- "#999999" + } +} + +fig_pair <- plotly::plot_ly() + +for (i in seq_along(samples_pair)) { + s <- samples_pair[i] + df_s <- pairing_plot_tbl_complete %>% dplyr::filter(sample == s) + + fig_pair <- fig_pair %>% + plotly::add_bars( + data = df_s, + x = ~pairing, + y = ~n, + color = ~pairing, + colors = pairing_colors, + text = ~paste0( + "Sample: ", s, + "
Pairing: ", pairing, + "
Cells: ", scales::comma(n) + ), + hoverinfo = "text", + visible = if (i == 1) TRUE else FALSE, + showlegend = if (i == 1) TRUE else FALSE + ) +} + +n_traces_per_sample <- length(pairing_levels) + +buttons_pair <- lapply(seq_along(samples_pair), function(i) { + vis <- rep(FALSE, length(samples_pair) * n_traces_per_sample) + idx <- ((i - 1) * n_traces_per_sample + 1):(i * n_traces_per_sample) + vis[idx] <- TRUE + + list( + method = "update", + args = list( + list(visible = vis, showlegend = vis), + list( + title = list(text = paste0("Pairing status: ", samples_pair[i])) + ) + ), + label = samples_pair[i] + ) +}) + +fig_pair <- fig_pair %>% + plotly::layout( + title = list(text = paste0("Pairing status: ", samples_pair[1])), + barmode = "group", + xaxis = list(title = ""), + yaxis = list(title = "Cells"), + updatemenus = list( + list( + type = "dropdown", + active = 0, + x = 0.02, + y = 1.15, + xanchor = "left", + yanchor = "top", + buttons = buttons_pair + ) + ), + annotations = list( + list( + x = 0.0, + y = 1.19, + xref = "paper", + yref = "paper", + text = "Select sample:", + showarrow = FALSE, + xanchor = "left", + yanchor = "top" + ) + ), + legend = list(title = list(text = "Pairing")), + margin = list(t = 110) + ) + +fig_pair + +``` + +#### Multiple productive chains by sample +```{r} +#| label: multichain-plot-dropdown +#| warning: false +#| message: false + +multi_chain_long <- pairing_all %>% + pivot_longer( + cols = c("multi_alpha", "multi_beta", "multi_gamma", "multi_delta"), + names_to = "type", + values_to = "flag" + ) %>% + mutate( + type = recode( + type, + multi_alpha = "Multiple TRA", + multi_beta = "Multiple TRB", + multi_gamma = "Multiple TRG", + multi_delta = "Multiple TRD" + ), + flag = factor(flag, levels = c(FALSE, TRUE), labels = c("No", "Yes")) + ) %>% + count(sample, type, flag, name = "n") + +samples_multi <- sort(unique(multi_chain_long$sample)) +type_levels <- unique(multi_chain_long$type) +flag_levels <- levels(multi_chain_long$flag) + +multi_chain_complete <- tidyr::expand_grid( + sample = samples_multi, + type = type_levels, + flag = flag_levels +) %>% + left_join(multi_chain_long, by = c("sample", "type", "flag")) %>% + mutate(n = ifelse(is.na(n), 0, n)) + +# keep a saved static all-samples version for export +n_samples_multi <- dplyr::n_distinct(multi_chain_complete$sample) +facet_ncol <- min(4, max(1, n_samples_multi)) +facet_nrow <- ceiling(n_samples_multi / facet_ncol) + +plot_width <- max(14, facet_ncol * 3.8) +plot_height <- max(16, facet_nrow * 5.5) + +p_multi_by <- ggplot(multi_chain_complete, aes(x = type, y = n, fill = flag)) + + geom_col(position = "stack") + + facet_wrap(~ sample, scales = "free_y", ncol = facet_ncol) + + labs( + title = "Multiple Productive Chains", + subtitle = "Post-QC multiple-chain flags by sample.", + x = NULL, + y = "Cells", + fill = "Flag" + ) + + theme_scratch_pub(params$base_size) + + theme( + axis.text.x = element_text(size = params$base_size - 4, angle = 45, hjust = 1), + axis.text.y = element_text(size = params$base_size - 2), + axis.title.y = element_text(size = params$base_size + 1, face = "bold"), + strip.text = element_text(size = params$base_size, face = "bold"), + plot.margin = margin(15, 15, 15, 15) + ) + +save_plot_safe( + p_multi_by, + glue("multiple_chains_by_sample.{params$figure_format}"), + width = plot_width, + height = plot_height +) + +flag_colors <- c("No" = "#F8766D", "Yes" = "#00BFC4") + +fig_multi <- plotly::plot_ly() + +for (i in seq_along(samples_multi)) { + s <- samples_multi[i] + df_s <- multi_chain_complete %>% dplyr::filter(sample == s) + + fig_multi <- fig_multi %>% + plotly::add_bars( + data = df_s, + x = ~type, + y = ~n, + color = ~flag, + colors = flag_colors, + text = ~paste0( + "Sample: ", s, + "
Type: ", type, + "
Flag: ", flag, + "
Cells: ", scales::comma(n) + ), + hoverinfo = "text", + visible = if (i == 1) TRUE else FALSE, + showlegend = if (i == 1) TRUE else FALSE + ) +} + +n_traces_per_sample <- length(flag_levels) + +buttons_multi <- lapply(seq_along(samples_multi), function(i) { + vis <- rep(FALSE, length(samples_multi) * n_traces_per_sample) + idx <- ((i - 1) * n_traces_per_sample + 1):(i * n_traces_per_sample) + vis[idx] <- TRUE + + list( + method = "update", + args = list( + list(visible = vis, showlegend = vis), + list( + title = list(text = paste0("Multiple productive chains: ", samples_multi[i])) + ) + ), + label = samples_multi[i] + ) +}) + +fig_multi <- fig_multi %>% + plotly::layout( + title = list(text = paste0("Multiple productive chains: ", samples_multi[1])), + barmode = "stack", + xaxis = list(title = ""), + yaxis = list(title = "Cells"), + updatemenus = list( + list( + type = "dropdown", + active = 0, + x = 0.02, + y = 1.15, + xanchor = "left", + yanchor = "top", + buttons = buttons_multi + ) + ), + annotations = list( + list( + x = 0.0, + y = 1.19, + xref = "paper", + yref = "paper", + text = "Select sample:", + showarrow = FALSE, + xanchor = "left", + yanchor = "top" + ) + ), + legend = list(title = list(text = "Flag")), + margin = list(t = 110) + ) + +fig_multi +``` + + +### CDR3 properties + +::: {.callout-note} +These plots assess whether post-QC CDR3 amino acid lengths are biologically plausible and reasonably stable across samples. +::: + +```{r} +#| label: cdr3-length +p_len_all <- ggplot(cdr3_len_all, aes(x = len, fill = chain_grp)) + + geom_histogram(position = "identity", alpha = 0.7, bins = 40) + + labs( + title = "CDR3 Length Distribution", + subtitle = "Post-QC pooled across all samples.", + x = "CDR3 amino acid length", + y = "Count", + fill = "Chain" + ) + + theme_scratch_pub(params$base_size) + +p_len_all +save_plot_safe(p_len_all, glue("cdr3_length_hist_all.{params$figure_format}")) +``` + +#### CDR3 length distribution by sample + +```{r} +#| label: cdr3-length-dropdown +#| warning: false +#| message: false + +n_samples_facet <- dplyr::n_distinct(cdr3_len_all$sample) +facet_ncol <- min(4, max(1, n_samples_facet)) +facet_nrow <- ceiling(n_samples_facet / facet_ncol) + +plot_width <- max(14, facet_ncol * 3.8) +plot_height <- max(16, facet_nrow * 5.5) + +# keep static all-samples faceted histogram for export +p_len_facet <- ggplot(cdr3_len_all, aes(x = len, fill = chain_grp)) + + geom_histogram(position = "identity", alpha = 0.7, bins = 40) + + facet_wrap(~ sample, scales = "free_y", ncol = facet_ncol) + + labs( + title = "CDR3 Length Distribution by Sample", + subtitle = "Post-QC distribution faceted by sample.", + x = "CDR3 amino acid length", + y = "Count", + fill = "Chain" + ) + + theme_scratch_pub(params$base_size) + + theme( + axis.text.x = element_text(size = params$base_size - 4, angle = 45, hjust = 1), + axis.text.y = element_text(size = params$base_size - 2), + strip.text = element_text(size = params$base_size, face = "bold"), + plot.margin = margin(15, 15, 15, 15) + ) + +save_plot_safe( + p_len_facet, + glue("cdr3_length_hist_by_sample.{params$figure_format}"), + width = plot_width, + height = plot_height +) + +# keep static boxplot for export +cdr3_box <- ggplot(cdr3_len_all, aes(x = sample, y = len, fill = chain_grp)) + + geom_boxplot(outlier.size = 0.4, alpha = 0.8) + + labs( + title = "CDR3 Length by Sample", + x = NULL, + y = "CDR3 amino acid length", + fill = "Chain" + ) + + theme_scratch_pub(params$base_size) + + theme( + axis.text.x = element_text(angle = 45, hjust = 1) + ) + +save_plot_safe( + cdr3_box, + glue("cdr3_length_boxplot_by_sample.{params$figure_format}"), + width = 14, + height = 6 +) + +samples_cdr3 <- sort(unique(cdr3_len_all$sample)) +chain_levels <- unique(cdr3_len_all$chain_grp) + +# consistent colors +chain_colors <- c( + "TRA" = "#F8766D", + "TRB" = "#00BFC4", + "TRG" = "#7CAE00", + "TRD" = "#C77CFF", + "Other" = "#999999" +) + +for (nm in chain_levels) { + if (!nm %in% names(chain_colors)) { + chain_colors[[nm]] <- "#999999" + } +} + +# build one histogram per sample +hist_plots <- lapply(samples_cdr3, function(s) { + df_s <- cdr3_len_all %>% dplyr::filter(sample == s) + + p_one <- ggplot(df_s, aes(x = len, fill = chain_grp)) + + geom_histogram(position = "identity", alpha = 0.7, bins = 40) + + labs( + title = paste0("CDR3 length distribution: ", s), + x = "CDR3 amino acid length", + y = "Count", + fill = "Chain" + ) + + scale_fill_manual(values = chain_colors, drop = FALSE) + + theme_scratch_pub(params$base_size) + + plotly::ggplotly(p_one, tooltip = c("x", "y", "fill")) %>% + plotly::layout(height = 520) +}) + +# dropdown selector +fig_cdr3 <- hist_plots[[1]] + +buttons_cdr3 <- lapply(seq_along(samples_cdr3), function(i) { + list( + method = "restyle", + args = list("visible", TRUE), + label = samples_cdr3[i], + execute = FALSE + ) +}) + +# use htmlwidgets onRender to switch full plot objects via dropdown is harder than bars; +# instead use subplot traces approach below + +fig_cdr3 <- plotly::plot_ly() + +trace_counts <- integer(length(samples_cdr3)) + +for (i in seq_along(samples_cdr3)) { + s <- samples_cdr3[i] + df_s <- cdr3_len_all %>% dplyr::filter(sample == s) + + for (ch in chain_levels) { + df_ch <- df_s %>% dplyr::filter(chain_grp == ch) + + fig_cdr3 <- fig_cdr3 %>% + plotly::add_histogram( + data = df_ch, + x = ~len, + name = ch, + marker = list(color = unname(chain_colors[ch])), + opacity = 0.7, + nbinsx = 40, + visible = if (i == 1) TRUE else FALSE, + showlegend = if (i == 1) TRUE else FALSE, + hovertemplate = paste0( + "Sample: ", s, + "
Chain: ", ch, + "
CDR3 length: %{x}", + "
Count: %{y}" + ) + ) + } + + trace_counts[i] <- length(chain_levels) +} + +buttons_cdr3 <- lapply(seq_along(samples_cdr3), function(i) { + vis <- rep(FALSE, sum(trace_counts)) + start_idx <- sum(trace_counts[seq_len(i - 1)]) + 1 + end_idx <- sum(trace_counts[seq_len(i)]) + vis[start_idx:end_idx] <- TRUE + + list( + method = "update", + args = list( + list(visible = vis, showlegend = vis), + list( + title = list(text = paste0("CDR3 length distribution: ", samples_cdr3[i])) + ) + ), + label = samples_cdr3[i] + ) +}) + +fig_cdr3 <- fig_cdr3 %>% + plotly::layout( + title = list(text = paste0("CDR3 length distribution: ", samples_cdr3[1])), + barmode = "overlay", + xaxis = list(title = "CDR3 amino acid length"), + yaxis = list(title = "Count"), + updatemenus = list( + list( + type = "dropdown", + active = 0, + x = 0.02, + y = 1.15, + xanchor = "left", + yanchor = "top", + buttons = buttons_cdr3 + ) + ), + annotations = list( + list( + x = 0.0, + y = 1.19, + xref = "paper", + yref = "paper", + text = "Select sample:", + showarrow = FALSE, + xanchor = "left", + yanchor = "top" + ) + ), + legend = list(title = list(text = "Chain")), + margin = list(t = 110) + ) + +fig_cdr3 + +``` + +#### Cohort-wide CDR3 length boxplot + +```{r} +#| label: cdr3-boxplot-display +plotly::plot_ly( + data = cdr3_len_all, + x = ~sample, + y = ~len, + color = ~chain_grp, + colors = c("TRA" = "#F8766D", "TRB" = "#00BFC4", "TRG" = "#7CAE00", "TRD" = "#C77CFF", "Other" = "#999999"), + type = "box", + boxpoints = "outliers", + hovertemplate = paste( + "Sample: %{x}
", + "CDR3 length: %{y}
", + "Chain: %{color}" + ) +) %>% + plotly::layout( + title = list(text = "CDR3 Length by Sample"), + xaxis = list(title = ""), + yaxis = list(title = "CDR3 amino acid length") + ) + + +``` + + +### Gene usage + +::: {.callout-note} +These heatmaps summarize post-QC V and J gene segment usage across samples and can highlight biological enrichment or sample-specific receptor bias. +::: + +```{r} +#| label: v-gene-heatmap +if (!is.null(v_usage) && nrow(v_usage) > 0) { + v_mat <- v_usage %>% + select(sample, v_gene, freq) %>% + pivot_wider(names_from = sample, values_from = freq, values_fill = 0) %>% + as.data.frame() + + rownames(v_mat) <- v_mat$v_gene + v_mat$v_gene <- NULL + v_mat <- as.matrix(v_mat) + + max_v <- max(v_mat, na.rm = TRUE) + if (!is.finite(max_v) || max_v <= 0) max_v <- 1 + + ht_v <- Heatmap( + v_mat, + name = "Freq", + col = colorRamp2( + c(0, max_v / 2, max_v), + c("white", "gold", "firebrick") + ), + cluster_rows = TRUE, + cluster_columns = TRUE, + row_names_side = "left", + row_names_gp = grid::gpar(fontsize = 8), + column_names_gp = grid::gpar(fontsize = 7), + column_names_rot = 45, + column_title = "Top V gene usage across samples", + column_title_gp = grid::gpar(fontsize = 12, fontface = "bold"), + heatmap_legend_param = list(title = "Frequency") + ) + + draw(ht_v, padding = grid::unit(c(5, 5, 5, 12), "mm")) +} + +# #| label: v-gene-heatmap +# if (!is.null(v_usage) && nrow(v_usage) > 0) { +# v_mat <- v_usage %>% +# select(sample, v_gene, freq) %>% +# pivot_wider(names_from = sample, values_from = freq, values_fill = 0) %>% +# as.data.frame() +# +# rownames(v_mat) <- v_mat$v_gene +# v_mat$v_gene <- NULL +# v_mat <- as.matrix(v_mat) +# +# ht_v <- Heatmap( +# v_mat, +# name = "Freq", +# col = colorRamp2( +# c(0, max(v_mat, na.rm = TRUE) / 2, max(v_mat, na.rm = TRUE)), +# c("white", "gold", "firebrick") +# ), +# cluster_rows = TRUE, +# cluster_columns = TRUE, +# row_names_side = "left", +# column_title = "Top V gene usage across samples", +# heatmap_legend_param = list(title = "Frequency") +# ) +# +# draw(ht_v) +# } +``` + +```{r} +#| label: j-gene-heatmap +if (!is.null(j_usage) && nrow(j_usage) > 0) { + j_mat <- j_usage %>% + select(sample, j_gene, freq) %>% + pivot_wider(names_from = sample, values_from = freq, values_fill = 0) %>% + as.data.frame() + + rownames(j_mat) <- j_mat$j_gene + j_mat$j_gene <- NULL + j_mat <- as.matrix(j_mat) + + max_j <- max(j_mat, na.rm = TRUE) + if (!is.finite(max_j) || max_j <= 0) max_j <- 1 + + ht_j <- Heatmap( + j_mat, + name = "Freq", + col = colorRamp2( + c(0, max_j / 2, max_j), + c("white", "skyblue", "navy") + ), + cluster_rows = TRUE, + cluster_columns = TRUE, + row_names_side = "left", + row_names_gp = grid::gpar(fontsize = 8), + column_names_gp = grid::gpar(fontsize = 7), + column_names_rot = 45, + column_title = "Top J gene usage across samples", + column_title_gp = grid::gpar(fontsize = 12, fontface = "bold"), + heatmap_legend_param = list(title = "Frequency") + ) + + draw(ht_j, padding = grid::unit(c(5, 5, 5, 12), "mm")) +} + +# #| label: j-gene-heatmap +# if (!is.null(j_usage) && nrow(j_usage) > 0) { +# j_mat <- j_usage %>% +# select(sample, j_gene, freq) %>% +# pivot_wider(names_from = sample, values_from = freq, values_fill = 0) %>% +# as.data.frame() +# +# rownames(j_mat) <- j_mat$j_gene +# j_mat$j_gene <- NULL +# j_mat <- as.matrix(j_mat) +# +# ht_j <- Heatmap( +# j_mat, +# name = "Freq", +# col = colorRamp2( +# c(0, max(j_mat, na.rm = TRUE) / 2, max(j_mat, na.rm = TRUE)), +# c("white", "skyblue", "navy") +# ), +# cluster_rows = TRUE, +# cluster_columns = TRUE, +# row_names_side = "left", +# column_title = "Top J gene usage across samples", +# heatmap_legend_param = list(title = "Frequency") +# ) +# +# draw(ht_j) +# } +``` + +### Clonality + +::: {.callout-note} +Clone rank-abundance summarizes whether the repertoire is broadly distributed or dominated by expanded clonotypes. +::: + + +```{r} +#| label: clone-rank-plot +#| fig-width: 16 +#| fig-height: !expr max(12, 7 + ceiling(dplyr::n_distinct(pairing_all$sample) / 6) * 0.8) + +# 1. Create the plotting table +clone_rank_tbl_plot <- pairing_all %>% + filter(!is.na(clone_id), clone_id != "") %>% + count(sample, clone_id, name = "n_cells") %>% + group_by(sample) %>% + arrange(desc(n_cells), .by_group = TRUE) %>% + mutate(rank = row_number()) %>% + ungroup() + +save_table_safe(clone_rank_tbl_plot, "clone_rank_abundance.tsv") + +# 2. Build the plot +if (nrow(clone_rank_tbl_plot) > 0) { + n_samples_clone <- dplyr::n_distinct(clone_rank_tbl_plot$sample) + legend_rows <- ceiling(n_samples_clone / 6) + + # Adjusted height calculation for a more compact look + plot_height <- max(12, 7 + legend_rows * 0.8) + + p_clone_rank <- ggplot(clone_rank_tbl_plot, aes(x = rank, y = n_cells, color = sample)) + + geom_line(linewidth = 0.8, alpha = 0.8) + + geom_point(size = 1.5, alpha = 0.8) + + scale_x_log10() + + scale_y_log10() + + labs( + title = "Clone Rank-Abundance Curve", + subtitle = "All detected clonotypes are shown.", + x = "Clone rank (log10)", + y = "Cells per clone (log10)", + color = "Sample" + ) + + theme_scratch_pub(params$base_size) + + theme( + # LEGEND POSITION: Change "bottom" to "right" if you want it on the side + legend.position = "bottom", + legend.direction = "horizontal", + legend.box = "horizontal", + + # AXIS TEXT (Tick labels: 1, 10, 100) - Increased to +4 + axis.text = element_text(size = params$base_size + 4, color = "black"), + + # AXIS TITLES (Labels: Clone rank, Cells per clone) - Increased to +6 + axis.title = element_text(size = params$base_size + 6, face = "bold"), + + # LEGEND TEXT (Sample IDs) - Increased to +2 + legend.text = element_text(size = params$base_size + 2), + legend.title = element_text(size = params$base_size + 4, face = "bold"), + + plot.title = element_text(size = params$base_size + 8, face = "bold"), + plot.subtitle = element_text(size = params$base_size + 4), + + # Log scales look better without x-axis rotation + axis.text.x = element_text(angle = 0, hjust = 0.5), + panel.grid.major = element_line(linewidth = 0.5, color = "grey90") + ) + + guides(color = guide_legend(nrow = legend_rows, byrow = TRUE)) + + print(p_clone_rank) + save_plot_safe( + p_clone_rank, + glue("clone_rank_abundance.{params$figure_format}"), + width = 16, + height = plot_height + ) +} else { + cat("No clonotypes were available to build the clone rank-abundance curve.") +} + +# #| label: clone-rank +# +# +# clone_rank_tbl_plot <- pairing_all %>% +# filter(!is.na(clone_id), clone_id != "") %>% +# count(sample, clone_id, name = "n_cells") %>% +# group_by(sample) %>% +# arrange(desc(n_cells), .by_group = TRUE) %>% +# mutate(rank = row_number()) %>% +# ungroup() +# +# save_table_safe(clone_rank_tbl_plot, "clone_rank_abundance.tsv") +# +# if (nrow(clone_rank_tbl_plot) > 0) { +# p_clone_rank <- ggplot(clone_rank_tbl_plot, aes(x = rank, y = n_cells, color = sample)) + +# geom_line(linewidth = 0.9, alpha = 0.8) + +# geom_point(size = 1.2, alpha = 0.9) + +# scale_x_log10() + +# scale_y_log10() + +# labs( +# title = "Clone Rank-Abundance Curve", +# subtitle = "All detected clonotypes are shown.", +# x = "Clone rank (log10)", +# y = "Cells per clone (log10)", +# color = "Sample" +# ) + +# theme_scratch_pub(params$base_size) +# +# print(p_clone_rank) +# save_plot_safe(p_clone_rank, glue("clone_rank_abundance.{params$figure_format}")) +# } else { +# cat("No clonotypes were available to build the clone rank-abundance curve.") +# } +# +# # if (!is.null(clone_rank_tbl) && nrow(clone_rank_tbl) > 0) { +# # p_clone_rank <- ggplot(clone_rank_tbl, aes(x = rank, y = n_cells, color = sample)) + +# # geom_line(linewidth = 0.9, alpha = 0.8) + +# # geom_point(size = 1.2, alpha = 0.9) + +# # scale_x_log10() + +# # scale_y_log10() + +# # labs( +# # title = "Clone Rank-Abundance Curve", +# # subtitle = glue("Only clones with at least {params$min_clone_size_plot} cells are shown."), +# # x = "Clone rank (log10)", +# # y = "Cells per clone (log10)", +# # color = "Sample" +# # ) + +# # theme_scratch_pub(params$base_size) +# # +# # p_clone_rank +# # save_plot_safe(p_clone_rank, glue("clone_rank_abundance.{params$figure_format}")) +# # } +``` + +### Metadata-stratified summaries + +::: {.callout-note} +These summaries are shown only when patient, condition, or timepoint metadata are available. +::: + +```{r} +#| label: patient-summary +#| results: asis + + +if (show_patient) { + patient_source <- sample_qc %>% + select(sample, patient_id, n_cells_total, n_unique_clones) %>% + distinct() %>% + left_join( + qc_retention %>% select(sample, contigs_before, contigs_after, retained_prop, retained_pct), + by = "sample" + ) + + patient_tbl <- patient_source %>% + filter(!is.na(patient_id), patient_id != "") %>% + group_by(patient_id) %>% + summarise( + n_samples = n_distinct(sample), + contigs_before = sum(contigs_before, na.rm = TRUE), + contigs_after = sum(contigs_after, na.rm = TRUE), + retained_prop = contigs_after / pmax(1, contigs_before), + retained_pct = 100 * retained_prop, + n_cells_total = sum(n_cells_total, na.rm = TRUE), + n_unique_clones = sum(n_unique_clones, na.rm = TRUE), + .groups = "drop" + ) + + print( + scroll_kable( + patient_tbl, + caption = "Patient-level VDJ QC summary.", + height = "260px" + ) + ) + + p_patient <- patient_tbl %>% + mutate(patient_id = fct_reorder(patient_id, retained_prop)) %>% + ggplot(aes(x = patient_id, y = retained_pct)) + + geom_col(fill = "darkgreen") + + coord_flip() + + labs( + title = "QC Retention by Patient", + x = NULL, + y = "Retained (%)" + ) + + theme_scratch_pub(params$base_size) + + print(p_patient) + save_plot_safe(p_patient, glue("retention_by_patient.{params$figure_format}")) + + save_table_safe(patient_tbl, "patient_level_vdj_qc_summary.tsv") +} + +``` + +```{r} +#| label: condition-summary +#| results: asis + +if (show_condition) { + condition_source <- sample_qc %>% + select(sample, condition, n_cells_total) %>% + distinct() %>% + left_join( + qc_retention %>% select(sample, contigs_before, contigs_after, retained_prop, retained_pct), + by = "sample" + ) + + condition_tbl <- condition_source %>% + filter(!is.na(condition), condition != "") %>% + group_by(condition) %>% + summarise( + n_samples = n_distinct(sample), + contigs_before = sum(contigs_before, na.rm = TRUE), + contigs_after = sum(contigs_after, na.rm = TRUE), + retained_prop = contigs_after / pmax(1, contigs_before), + retained_pct = 100 * retained_prop, + n_cells_total = sum(n_cells_total, na.rm = TRUE), + .groups = "drop" + ) + + print( + scroll_kable( + condition_tbl, + caption = "Condition-level VDJ QC summary.", + height = "260px" + ) + ) + + p_condition <- condition_tbl %>% + mutate(condition = fct_reorder(condition, retained_prop)) %>% + ggplot(aes(x = condition, y = retained_pct)) + + geom_col(fill = "purple4") + + coord_flip() + + labs( + title = "QC Retention by Condition", + x = NULL, + y = "Retained (%)" + ) + + theme_scratch_pub(params$base_size) + + print(p_condition) + save_plot_safe(p_condition, glue("retention_by_condition.{params$figure_format}")) + + save_table_safe(condition_tbl, "condition_level_vdj_qc_summary.tsv") +} +``` + +```{r} +#| label: timepoint-summary +#| results: asis + +if (show_timepoint) { + time_source <- sample_qc %>% + select(sample, timepoint) %>% + distinct() %>% + left_join( + qc_retention %>% select(sample, contigs_before, contigs_after, retained_prop, retained_pct), + by = "sample" + ) + + time_tbl <- time_source %>% + filter(!is.na(timepoint), timepoint != "") %>% + group_by(timepoint) %>% + summarise( + n_samples = n_distinct(sample), + contigs_before = sum(contigs_before, na.rm = TRUE), + contigs_after = sum(contigs_after, na.rm = TRUE), + retained_prop = contigs_after / pmax(1, contigs_before), + retained_pct = 100 * retained_prop, + .groups = "drop" + ) + + print( + scroll_kable( + time_tbl, + caption = "Timepoint-level VDJ QC summary.", + height = "260px" + ) + ) + + p_time <- time_tbl %>% + mutate(timepoint = fct_inorder(timepoint)) %>% + ggplot(aes(x = timepoint, y = retained_pct, group = 1)) + + geom_line(linewidth = 1, color = "steelblue4") + + geom_point(size = 3, color = "steelblue4") + + labs( + title = "QC Retention Across Timepoints", + x = "Timepoint", + y = "Retained (%)" + ) + + theme_scratch_pub(params$base_size) + + print(p_time) + save_plot_safe(p_time, glue("retention_by_timepoint.{params$figure_format}")) + + save_table_safe(time_tbl, "timepoint_level_vdj_qc_summary.tsv") +} +``` + +## Warnings +```{r} +#| label: warnings +warn_tbl <- tibble( + warning = c( + "Low contig retention", + "High multi-chain burden", + "Low paired fraction", + "Missing reads column", + "Missing UMIs column", + "Missing patient metadata", + "Missing condition metadata", + "Missing timepoint metadata" + ), + triggered = c( + n_contigs_after / max(1, n_contigs_before) < 0.5, + mean(pairing_all$multi_alpha | pairing_all$multi_beta | pairing_all$multi_gamma | pairing_all$multi_delta, na.rm = TRUE) > 0.1, + mean(pairing_all$pairing %in% c("Alpha+Beta","Gamma+Delta"), na.rm = TRUE) < 0.3, + all(is.na(contigs_all_post$reads)), + all(is.na(contigs_all_post$umis)), + !show_patient, + !show_condition, + !show_timepoint + ), + interpretation = c( + "Less than half of contigs were retained after QC. Inspect productive, high-confidence, and full-length filters as well as barcode/sample path resolution.", + "More than 10% of cells are multi-chain under the current settings. This may indicate biological dual-chain usage, technical artifacts, or doublets.", + "Paired-chain fraction is low after QC. Check assay recovery, allowed chain mode, and pairing requirements.", + "Read-support plots were skipped because no reads column was available.", + "UMI-support plots were skipped because no UMIs column was available.", + "Patient-level summaries were skipped because patient metadata were unavailable.", + "Condition-level summaries were skipped because condition metadata were unavailable.", + "Timepoint-level summaries were skipped because timepoint metadata were unavailable." + ) +) %>% + filter(triggered) + +if (nrow(warn_tbl) == 0) { + cat("No major automatic warnings were triggered under the current QC settings.") +} else { + kable(warn_tbl %>% select(-triggered), caption = "Automatically generated QC warnings and notes.") %>% + kable_styling(full_width = TRUE, bootstrap_options = c("striped","hover","condensed")) +} +``` + +## session info +```{r} +#| label: session-info +writeLines(capture.output(sessionInfo()), file.path(params$outdir, "sessionInfo.vdj_qc.txt")) +sessionInfo() +``` + + diff --git a/modules/scratch/VDJ_QC/main.nf b/modules/scratch/VDJ_QC/main.nf new file mode 100644 index 0000000..d9e3651 --- /dev/null +++ b/modules/scratch/VDJ_QC/main.nf @@ -0,0 +1,55 @@ +process VDJ_QC { + + tag "Running VDJ QC - ${project_name}" + label 'process_medium' + + container "${params.container}" + + publishDir "${params.outdir}/VDJ_QC",mode: 'copy', overwrite: true + + input: + path(notebook) + path(sample_sheet) + path (input_annotated_object) + val(project_name) + + + output: + path("VDJ_QC_analysis.html"), emit: report_html + path("VDJ_QC/tables/contigs_after_qc.tsv"), emit: contigs_after_qc + path("VDJ_QC/tables/*"), emit: tables + path("VDJ_QC/figures/*"), emit: figures + + script: + """ + mkdir -p VDJ_QC + mkdir -p .cache/quarto + export XDG_CACHE_HOME="\$PWD/.cache" + export QUARTO_CACHE_DIR="\$PWD/.cache/quarto" + export XDG_DATA_HOME="\$PWD/.cache" + export QUARTO_PRINT_STACK=true + export HOME="\$PWD" + + quarto render ${notebook} \ + -P sample_sheet="${sample_sheet}" \ + -P sample_sheet_sample_col="sample" \ + -P sample_sheet_path_col="path" \ + -P metadata_file="${params.metadata_file}" \ + -P input_annotated_object="${input_annotated_object}" \ + -P outdir="VDJ_QC" \ + -P chain_mode="auto" \ + -P require_productive=${params.vdj_require_productive} \ + -P require_high_conf=${params.vdj_require_high_conf} \ + -P require_full_length=${params.vdj_require_full_length} \ + -P min_umis=${params.vdj_min_umis} \ + -P min_reads=${params.vdj_min_reads} \ + -P keep_one_per_chain=${params.vdj_keep_one_per_chain} \ + -P keep_paired_only=${params.vdj_keep_paired_only} \ + -P keep_dual_alpha=${params.vdj_keep_dual_alpha} \ + -P keep_dual_beta=${params.vdj_keep_dual_beta} \ + -P cdr3_aa_min=${params.vdj_cdr3_aa_min} \ + -P cdr3_aa_max=${params.vdj_cdr3_aa_max} \ + -P drop_stop_or_frames=${params.vdj_drop_stop_or_frames} \ + -P report_label="${project_name} VDJ QC" + """ +} \ No newline at end of file diff --git a/nextflow.config b/nextflow.config index ccc4534..84a951b 100644 --- a/nextflow.config +++ b/nextflow.config @@ -40,6 +40,10 @@ params { // Notebooks parameters timepoint_col = 'timepoint' timepoint_order_col = 'timepoint_order' + // Optional ordered comma-separated list of timepoint values (e.g. + // "Base,Week4,EOT") - rank = position in the list. Unset: timepoints are + // ranked numerically if every value parses as a number, else alphabetically. + timepoint_order = '' alias_col = 'alias' subject_col = 'patient' @@ -75,6 +79,159 @@ params { //reports template_qc = "${projectDir}/notebooks/template_qc.qmd" + template_discovery_brief = "${projectDir}/notebooks/template_discovery_brief.qmd" + template_details_part1 = "${projectDir}/notebooks/template_details_part1.qmd" + template_details_part2 = "${projectDir}/notebooks/template_details_part2.qmd" + template_pheno_sc = "${projectDir}/notebooks/template_pheno_sc.qmd" + template_pheno_bulk = "${projectDir}/notebooks/template_pheno_bulk.qmd" + template_patient_clustering_on = "${projectDir}/notebooks/template_patient_clustering_on.qmd" + template_patient_clustering_off = "${projectDir}/notebooks/template_patient_clustering_off.qmd" + + // ══════════════════════════════════════════════════════════════════════ + // SINGLE-CELL modality (integration) — see IMPLEMENTATION_SPEC.md + // All additive; bulk mode ignores these. Dispatched by --mode (default bulk). + // ══════════════════════════════════════════════════════════════════════ + + // Modality dispatch + mode = null // null → 'bulk'; set 'singlecell' for SC mode + + // SC inputs + input_vdj_contigs = null // Cell Ranger VDJ outs/ (required for SC mode) + sample_sheet = null // SC sample sheet (distinct from bulk --samplesheet) + input_annotated_object = null // optional GEX Seurat RDS; presence selects full-SC vs VDJ-only + + // SC container (bulk stays on params.container; SC cell-level processes use this) + sc_container = 'syedsazaidi/scratch-tcr:latest' + + // Shared metadata column names (Seurat slots / sample-sheet columns) + sample_col = 'orig.ident' + patient_col = 'patient_id' + condition_col = 'condition' + batch_col = 'batch' + label_col = '' + + // Pseudobulk stratification: default pools by SAMPLE (patient carried as metadata, + // pooled per-patient downstream in PATIENT). Optional per-cell-type view ({sample}__{annot} + // units) for secondary analysis; under-powered units dropped by the QC gate below. + pseudobulk_by_phenotype = false + + // Pseudobulk QC gate (single-cell → bulk TCR) + pseudobulk_qc_min_clones = 25 + pseudobulk_qc_min_cells = 50 + pseudobulk_qc_mode = 'drop' // 'drop' = skip failing samples | 'hard_stop' = abort + + // Module toggles + run_conga = true + run_consensus = true + run_repertoire = true + run_master_summary = true + run_tcri = true + + // Global plotting / embedding + metadata_file = "${projectDir}/assets/NO_FILE" + reduction_use = 'umap' + make_umap_if_missing = true + umap_dims_max = 30 + umap_nfeatures = 3000 + raster_large_umap = true + min_clone_size_plot = 2 + top_n_clones_umap = 12 + top_n_clone_table = 50 + + // VDJ QC + vdj_chain_mode = 'auto' + vdj_min_umis = 1 + vdj_min_reads = 0 + vdj_cdr3_aa_min = 8 + vdj_cdr3_aa_max = 25 + vdj_require_productive = true + vdj_require_high_conf = true + vdj_require_full_length = true + vdj_keep_one_per_chain = true + vdj_keep_paired_only = false + vdj_keep_dual_alpha = true + vdj_keep_dual_beta = true + vdj_drop_stop_or_frames = true + vdj_meta_sample_col = 'sample' + vdj_meta_patient_col = 'patient_id' + + // T-cell integration + cells_mode = 'T-AB' + filter_to_t_ab = true + clone_call_preference = 'aa' + keep_na_clonotypes = false + harmonize_apply_to = 'tcr' + harmonization_overlap_threshold = 0.80 + minimum_final_overlap_fraction = 0.50 + subset_tcells = true + tcell_regex = '(?i)(t cell|cd4|cd8|treg|trm|tem|naive t|memory t|exhausted t|cytotoxic t|gamma-delta|gd t)' + clone_id_col = 'clone_id' + + // CoNGA + conga_min_cells_per_group = 10 + conga_min_cluster_size_plot = 5 + conga_top_n_clusters = 20 + conga_max_edges_to_plot = 5000 + conga_high_cutoff = 0.8 + conga_mid_cutoff = 0.5 + conga_use_quantile_cutoffs = true + conga_high_quantile = 0.9 + conga_mid_quantile = 0.5 + + // GLIPH2 (SC cluster-mapping level) + gliph_ref_mode = 'local_bundle' + refdb_beta = 'human_v2.0_CD48' + gliph_clone_match_mode = 'auto' + gliph_derive_clone_key_from_CTaa = true + gliph_min_cells_per_group = 10 + gliph_min_cluster_size_plot = 3 + gliph_top_n_clusters = 20 + gliph_top_n_motifs = 20 + + // TCRdist3 (SC cluster-mapping level) + tcrdist_clone_match_mode = 'cdr3b_from_CTaa' + tcrdist_derive_clone_key_from_CTaa = true + tcrdist_radius = 24 + tcrdist_min_cells_per_group = 10 + tcrdist_min_cluster_size_plot = 3 + tcrdist_top_n_clusters = 20 + tcrdist_top_n_neighbors = 50 + + // GIANA (SC cluster-mapping level) + giana_exact_mode = true + giana_clone_match_mode = 'cdr3b_from_CTaa' + giana_derive_clone_key_from_CTaa = true + giana_min_cells_per_group = 10 + giana_min_cluster_size_plot = 3 + giana_top_n_clusters = 20 + + // Consensus clustering + consensus_min_methods = 2 + consensus_use_majority_vote = true + consensus_assign_singleton = false + consensus_label_prefix = 'CONS' + consensus_min_cells_per_group = 10 + consensus_min_cluster_size_plot = 3 + consensus_top_n_clusters = 20 + + // Repertoire + repertoire_min_cells_per_clone_plot = 2 + repertoire_top_n_shared_clones = 50 + repertoire_top_n_flux_clones = 30 + repertoire_shareability_min_n_groups = 2 + repertoire_min_cells_per_group = 10 + repertoire_overlap_metric = 'jaccard' + repertoire_use_relative_clone_freqs = true + + // TCRi + tcri_high_cutoff = 0.80 + tcri_mid_cutoff = 0.50 + tcri_use_quantile_cutoffs = true + tcri_high_quantile = 0.90 + tcri_mid_quantile = 0.50 + tcri_min_cells_per_group = 10 + tcri_min_clone_size_for_assoc = 2 + tcri_top_n_high_clones = 30 } includeConfig 'conf/base.config' diff --git a/nextflow.config.bak b/nextflow.config.bak new file mode 100644 index 0000000..182880b --- /dev/null +++ b/nextflow.config.bak @@ -0,0 +1,226 @@ +// bulk.config + +docker { + enabled = true +} + +plugins { + id 'nf-schema@2.7.2' +} + +params { + samplesheet = null + outdir = 'out' + sobject_gex = null // specify filepath if pseudobulking by phenotype + + publish_dir_mode = 'copy' + + // Max resource options + // Defaults only, expecting to be overwritten + max_memory = 768.GB + max_cpus = 192 + max_time = 48.h + + input_format = "airr" // cellranger, adaptive + airr_schema = "${projectDir}/assets/airr/airr_rearrangement_schema.json" + imgt_lookup = "${projectDir}/assets/airr/imgt_adaptive_lookup.tsv" + + // Sample + compare parameters + workflow_level = "sample,compare" + project_name = "tcrtoolkit_"+ new Date().format("yyyy-MM-dd_HH-mm-ss") + sample_stats_template = "${projectDir}/notebooks/sample_stats_template.qmd" + compare_stats_template = "${projectDir}/notebooks/compare_stats_template.qmd" + + // Sample stats metadata parameters + samplechart_x_col = 'timepoint' + samplechart_color_col = 'origin' + vgene_subject_col = 'patient' + vgene_x_cols = 'origin,timepoint' + + // Notebooks parameters + timepoint_col = 'timepoint' + timepoint_order_col = 'timepoint_order' + alias_col = 'alias' + subject_col = 'patient' + + // OLGA parameters + olga_chunk_length = 100000 // larger chunk size = less parallelization + + // GIANA parameters + threshold = 7.0 + threshold_score = 3.6 + threshold_vgene = 3.7 + + // GLIPH2 parameters + use_gliph2 = true + gliph2_report_template = "${projectDir}/notebooks/gliph2_report_template.qmd" + ref_files = "${projectDir}/assets/gliph2_files" + + local_min_pvalue = "0.001" + p_depth = "1000" + global_convergence_cutoff = "1" + simulation_depth = "1000" + kmer_min_depth = "3" + local_min_OVE = "c(1000, 100, 10)" + algorithm = "GLIPH2" + all_aa_interchangeable = "1" + + // TCRDIST3 parameters + matrix_sparsity = "sparse" + distance_metric = "tcrdist" + db_path = "${projectDir}/assets/tcrdist3_files/alphabeta_gammadelta_db.tsv" + + //container to use for running the workflow + container = "ghcr.io/karchinlab/tcrtoolkit:main" + + //reports + template_qc = "${projectDir}/notebooks/template_qc.qmd" + // ══════════════════════════════════════════════════════════════════════ + // SINGLE-CELL modality (integration) — see IMPLEMENTATION_SPEC.md + // All additive; bulk mode ignores these. Dispatched by --mode (default bulk). + // ══════════════════════════════════════════════════════════════════════ + + // Modality dispatch + mode = null // null → 'bulk'; set 'singlecell' for SC mode + + // SC inputs + input_vdj_contigs = null // Cell Ranger VDJ outs/ (required for SC mode) + sample_sheet = null // SC sample sheet (distinct from bulk --samplesheet) + input_annotated_object = null // optional GEX Seurat RDS; presence selects full-SC vs VDJ-only + + // SC container (bulk stays on params.container; SC cell-level processes use this) + sc_container = 'syedsazaidi/scratch-tcr:latest' + + // Shared metadata column names (Seurat slots / sample-sheet columns) + sample_col = 'orig.ident' + patient_col = 'patient_id' + condition_col = 'condition' + batch_col = 'batch' + label_col = '' + + // Pseudobulk stratification: default pools by SAMPLE (patient carried as metadata, + // pooled per-patient downstream in PATIENT). Optional per-cell-type view ({sample}__{annot} + // units) for secondary analysis; under-powered units dropped by the QC gate below. + pseudobulk_by_phenotype = false + + // Pseudobulk QC gate (single-cell → bulk TCR) + pseudobulk_qc_min_clones = 25 + pseudobulk_qc_min_cells = 50 + pseudobulk_qc_mode = 'drop' // 'drop' = skip failing samples | 'hard_stop' = abort + + // Module toggles + run_conga = true + run_consensus = true + run_repertoire = true + run_master_summary = true + run_tcri = true + + // Global plotting / embedding + metadata_file = "${projectDir}/assets/NO_FILE" + reduction_use = 'umap' + make_umap_if_missing = true + umap_dims_max = 30 + umap_nfeatures = 3000 + raster_large_umap = true + min_clone_size_plot = 2 + top_n_clones_umap = 12 + top_n_clone_table = 50 + + // VDJ QC + vdj_chain_mode = 'auto' + vdj_min_umis = 1 + vdj_min_reads = 0 + vdj_cdr3_aa_min = 8 + vdj_cdr3_aa_max = 25 + vdj_require_productive = true + vdj_require_high_conf = true + vdj_require_full_length = true + vdj_keep_one_per_chain = true + vdj_keep_paired_only = false + vdj_keep_dual_alpha = true + vdj_keep_dual_beta = true + vdj_drop_stop_or_frames = true + vdj_meta_sample_col = 'sample' + vdj_meta_patient_col = 'patient_id' + + // T-cell integration + cells_mode = 'T-AB' + filter_to_t_ab = true + clone_call_preference = 'aa' + keep_na_clonotypes = false + harmonize_apply_to = 'tcr' + harmonization_overlap_threshold = 0.80 + minimum_final_overlap_fraction = 0.50 + subset_tcells = true + tcell_regex = '(?i)(t cell|cd4|cd8|treg|trm|tem|naive t|memory t|exhausted t|cytotoxic t|gamma-delta|gd t)' + clone_id_col = 'clone_id' + + // CoNGA + conga_min_cells_per_group = 10 + conga_min_cluster_size_plot = 5 + conga_top_n_clusters = 20 + conga_max_edges_to_plot = 5000 + conga_high_cutoff = 0.8 + conga_mid_cutoff = 0.5 + conga_use_quantile_cutoffs = true + conga_high_quantile = 0.9 + conga_mid_quantile = 0.5 + + // GLIPH2 (SC cluster-mapping level) + gliph_ref_mode = 'local_bundle' + refdb_beta = 'human_v2.0_CD48' + gliph_clone_match_mode = 'auto' + gliph_derive_clone_key_from_CTaa = true + gliph_min_cells_per_group = 10 + gliph_min_cluster_size_plot = 3 + gliph_top_n_clusters = 20 + gliph_top_n_motifs = 20 + + // TCRdist3 (SC cluster-mapping level) + tcrdist_clone_match_mode = 'cdr3b_from_CTaa' + tcrdist_derive_clone_key_from_CTaa = true + tcrdist_radius = 24 + tcrdist_min_cells_per_group = 10 + tcrdist_min_cluster_size_plot = 3 + tcrdist_top_n_clusters = 20 + tcrdist_top_n_neighbors = 50 + + // GIANA (SC cluster-mapping level) + giana_exact_mode = true + giana_clone_match_mode = 'cdr3b_from_CTaa' + giana_derive_clone_key_from_CTaa = true + giana_min_cells_per_group = 10 + giana_min_cluster_size_plot = 3 + giana_top_n_clusters = 20 + + // Consensus clustering + consensus_min_methods = 2 + consensus_use_majority_vote = true + consensus_assign_singleton = false + consensus_label_prefix = 'CONS' + consensus_min_cells_per_group = 10 + consensus_min_cluster_size_plot = 3 + consensus_top_n_clusters = 20 + + // Repertoire + repertoire_min_cells_per_clone_plot = 2 + repertoire_top_n_shared_clones = 50 + repertoire_top_n_flux_clones = 30 + repertoire_shareability_min_n_groups = 2 + repertoire_min_cells_per_group = 10 + repertoire_overlap_metric = 'jaccard' + repertoire_use_relative_clone_freqs = true + + // TCRi + tcri_high_cutoff = 0.80 + tcri_mid_cutoff = 0.50 + tcri_use_quantile_cutoffs = true + tcri_high_quantile = 0.90 + tcri_mid_quantile = 0.50 + tcri_min_cells_per_group = 10 + tcri_min_clone_size_for_assoc = 2 + tcri_top_n_high_clones = 30 +} + +includeConfig 'conf/base.config' +includeConfig 'conf/modules.config' diff --git a/nextflow_schema.json b/nextflow_schema.json index 26d3c20..981e77f 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -1,231 +1,433 @@ { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://raw.githubusercontent.com/break-through-cancer/tcrtoolkit-pipeline/main/nextflow_schema.json", - "title": "tcrtoolkit pipeline parameters", - "description": "BTC TCR Toolkit pipeline", - "type": "object", - - "$defs": { - "input_output_options": { - "title": "Input/output options", - "type": "object", - "fa_icon": "fas fa-terminal", - "description": "Define where the pipeline should find input data and save output data.", - "required": ["samplesheet", "outdir"], - "properties": { - "samplesheet": { - "type": "string", - "format": "file-path", - "pattern": ".*.csv$", - "description": "Path to the samplesheet describing input AIRR data.", - "help_text": "A CSV of samples and metadata for this TCR analysis.", - "fa_icon": "fas fa-file-csv" - }, - "outdir": { - "type": "string", - "format": "directory-path", - "default": "out", - "description": "Output directory where results will be saved.", - "fa_icon": "fas fa-folder-open" - } - } - }, - - "resource_options": { - "title": "Max resource options", - "type": "object", - "fa_icon": "fab fa-acquisitions-incorporated", - "description": "Set the top limit for requested resources for any single job.", - "properties": { - "max_cpus": { - "type": "integer", - "default": 192, - "description": "Maximum CPUs that can be requested by any process.", - "fa_icon": "fas fa-microchip" - }, - "max_memory": { - "type": "string", - "default": "768.GB", - "pattern": "^\\d+(?:\\.\\d+|\\.)?\\s*(?:[KMGT]?B|[KMGT])$", - "description": "Maximum memory for any process.", - "fa_icon": "fas fa-memory" - }, - "max_time": { - "type": "integer", - "default": 172800000, - "description": "Maximum walltime for any job, converted from Nextflow Duration object.", - "fa_icon": "far fa-clock" - } - } - }, - - "workflow_options": { - "title": "Workflow parameters", - "type": "object", - "fa_icon": "fas fa-project-diagram", - "description": "General pipeline workflow settings.", - "properties": { - "workflow_level": { - "type": "string", - "default": "sample,compare", - "description": "Comma-separated workflow stages.", - "pattern": "^(sample|patient|compare|convert)(,(sample|patient|compare|convert))*$" - }, - "project_name": { - "type": "string", - "description": "Name of this analysis project." - }, - "publish_dir_mode": { - "type": "string", - "default": "copy", - "enum": ["copy", "move", "link", "symlink"], - "description": "Method used by `publishDir` to save outputs." - }, - "container": { - "type": "string", - "description": "Docker/Singularity container image to use for pipeline processes.", - "fa_icon": "fas fa-box" - } - } - }, - - "airr_options": { - "title": "AIRR data options", - "type": "object", - "fa_icon": "fas fa-dna", - "description": "Parameters related to AIRR format and schema references.", - "properties": { - "input_format": { - "type": "string", - "default": "airr", - "enum": ["airr", "adaptive", "cellranger"], - "description": "Input data format." - }, - "airr_schema": { - "type": "string", - "description": "Path to AIRR rearrangement schema JSON." - }, - "imgt_lookup": { - "type": "string", - "description": "Path to imgt lookup table." - }, - "sample_stats_template": { - "type": "string", - "description": "Path to sample notebook template." - }, - "compare_stats_template": { - "type": "string", - "description": "Path to compare notebook template." - }, - "template_qc": { - "type": "string", - "description": "Path to QC notebook template." - } - } - }, - - "plotting_options": { - "title": "Plotting and metadata options", - "type": "object", - "fa_icon": "fas fa-chart-bar", - "description": "Parameters for plotting and metadata columns.", - "properties": { - "samplechart_x_col": { "type": "string", "default": "timepoint" }, - "samplechart_color_col": { "type": "string", "default": "origin" }, - "vgene_subject_col": { "type": "string", "default": "patient" }, - "vgene_x_cols": { "type": "string", "default": "origin,timepoint" }, - "subject_col": { - "type": "string", - "default": "patient", - "description": "Samplesheet column identifying the subject/patient." - }, - "timepoint_col": { - "type": "string", - "default": "timepoint", - "description": "Samplesheet column identifying the timepoint." - }, - "timepoint_order_col": { - "type": "string", - "default": "timepoint_order", - "description": "Samplesheet column giving the sort order of timepoints." - }, - "alias_col": { - "type": "string", - "default": "alias", - "description": "Samplesheet column giving a display alias for each sample." + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/break-through-cancer/tcrtoolkit-pipeline/main/nextflow_schema.json", + "title": "tcrtoolkit pipeline parameters", + "description": "BTC TCR Toolkit pipeline", + "type": "object", + "$defs": { + "input_output_options": { + "title": "Input/output options", + "type": "object", + "fa_icon": "fas fa-terminal", + "description": "Define where the pipeline should find input data and save output data.", + "required": [ + "samplesheet", + "outdir" + ], + "properties": { + "samplesheet": { + "type": "string", + "format": "file-path", + "pattern": ".*.csv$", + "description": "Path to the samplesheet describing input AIRR data.", + "help_text": "A CSV of samples and metadata for this TCR analysis.", + "fa_icon": "fas fa-file-csv" + }, + "outdir": { + "type": "string", + "format": "directory-path", + "default": "out", + "description": "Output directory where results will be saved.", + "fa_icon": "fas fa-folder-open" + } + } + }, + "resource_options": { + "title": "Max resource options", + "type": "object", + "fa_icon": "fab fa-acquisitions-incorporated", + "description": "Set the top limit for requested resources for any single job.", + "properties": { + "max_cpus": { + "type": "integer", + "default": 192, + "description": "Maximum CPUs that can be requested by any process.", + "fa_icon": "fas fa-microchip" + }, + "max_memory": { + "type": "string", + "default": "768.GB", + "pattern": "^\\d+(?:\\.\\d+|\\.)?\\s*(?:[KMGT]?B|[KMGT])$", + "description": "Maximum memory for any process.", + "fa_icon": "fas fa-memory" + }, + "max_time": { + "type": "integer", + "default": 172800000, + "description": "Maximum walltime for any job, converted from Nextflow Duration object.", + "fa_icon": "far fa-clock" + } + } + }, + "workflow_options": { + "title": "Workflow parameters", + "type": "object", + "fa_icon": "fas fa-project-diagram", + "description": "General pipeline workflow settings.", + "properties": { + "workflow_level": { + "type": "string", + "default": "sample,compare", + "description": "Comma-separated workflow stages.", + "pattern": "^(sample|patient|compare|convert)(,(sample|patient|compare|convert))*$" + }, + "project_name": { + "type": "string", + "description": "Name of this analysis project." + }, + "publish_dir_mode": { + "type": "string", + "default": "copy", + "enum": [ + "copy", + "move", + "link", + "symlink" + ], + "description": "Method used by `publishDir` to save outputs." + }, + "container": { + "type": "string", + "description": "Docker/Singularity container image to use for pipeline processes.", + "fa_icon": "fas fa-box" + } + } + }, + "airr_options": { + "title": "AIRR data options", + "type": "object", + "fa_icon": "fas fa-dna", + "description": "Parameters related to AIRR format and schema references.", + "properties": { + "input_format": { + "type": "string", + "default": "airr", + "enum": [ + "airr", + "adaptive", + "cellranger" + ], + "description": "Input data format." + }, + "airr_schema": { + "type": "string", + "description": "Path to AIRR rearrangement schema JSON." + }, + "imgt_lookup": { + "type": "string", + "description": "Path to imgt lookup table." + }, + "sample_stats_template": { + "type": "string", + "description": "Path to sample notebook template." + }, + "compare_stats_template": { + "type": "string", + "description": "Path to compare notebook template." + }, + "template_qc": { + "type": "string", + "description": "Path to QC notebook template." + }, + "template_discovery_brief": { + "type": "string", + "description": "Path to discovery brief notebook template." + }, + "template_details_part1": { + "type": "string", + "description": "Path to details (part 1) notebook template." + }, + "template_details_part2": { + "type": "string", + "description": "Path to details (part 2) notebook template." + }, + "template_pheno_sc": { + "type": "string", + "description": "Path to single-cell phenotype notebook template." + }, + "template_pheno_bulk": { + "type": "string", + "description": "Path to bulk (TCRpheno) phenotype notebook template." + }, + "template_patient_clustering_on": { + "type": "string", + "description": "Path to patient-level clustering (GIANA/GLIPH2) notebook template, used when patient workflow_level is run." + }, + "template_patient_clustering_off": { + "type": "string", + "description": "Path to placeholder notebook template used when patient workflow_level is not run." + } + } + }, + "plotting_options": { + "title": "Plotting and metadata options", + "type": "object", + "fa_icon": "fas fa-chart-bar", + "description": "Parameters for plotting and metadata columns.", + "properties": { + "samplechart_x_col": { + "type": "string", + "default": "timepoint" + }, + "samplechart_color_col": { + "type": "string", + "default": "origin" + }, + "vgene_subject_col": { + "type": "string", + "default": "patient" + }, + "vgene_x_cols": { + "type": "string", + "default": "origin,timepoint" + }, + "subject_col": { + "type": "string", + "default": "patient", + "description": "Samplesheet column identifying the subject/patient." + }, + "timepoint_col": { + "type": "string", + "default": "timepoint", + "description": "Samplesheet column identifying the timepoint." + }, + "timepoint_order_col": { + "type": "string", + "default": "timepoint_order", + "description": "Name of the (computed, not samplesheet-provided) column holding each timepoint's sort rank. See timepoint_order." + }, + "timepoint_order": { + "type": "string", + "default": "", + "description": "Optional ordered comma-separated list of timepoint values (e.g. \"Base,Week4,EOT\"); rank = position in the list. If unset, timepoints are ranked numerically when every value parses as a number, otherwise alphabetically." + }, + "alias_col": { + "type": "string", + "default": "alias", + "description": "Samplesheet column giving a display alias for each sample." + } + } + }, + "olga_options": { + "title": "OLGA options", + "type": "object", + "fa_icon": "fas fa-layer-group", + "properties": { + "olga_chunk_length": { + "type": "integer", + "default": 100000, + "minimum": 1000, + "description": "Number of sequences processed per OLGA chunk. Larger values reduce parallelization and increase memory usage." + } + } + }, + "giana_options": { + "title": "GIANA clustering options", + "type": "object", + "fa_icon": "fas fa-brain", + "properties": { + "threshold": { + "type": "number", + "default": 7.0 + }, + "threshold_score": { + "type": "number", + "default": 3.6 + }, + "threshold_vgene": { + "type": "number", + "default": 3.7 + } + } + }, + "gliph2_options": { + "title": "GLIPH2 clustering options", + "type": "object", + "fa_icon": "fas fa-code-branch", + "properties": { + "use_gliph2": { + "type": "boolean", + "default": true + }, + "gliph2_report_template": { + "type": "string" + }, + "ref_files": { + "type": "string" + }, + "local_min_pvalue": { + "type": "string", + "default": "0.001" + }, + "p_depth": { + "type": "string", + "default": "1000" + }, + "global_convergence_cutoff": { + "type": "string", + "default": "1" + }, + "simulation_depth": { + "type": "string", + "default": "1000" + }, + "kmer_min_depth": { + "type": "string", + "default": "3" + }, + "local_min_OVE": { + "type": "string", + "default": "c(1000, 100, 10)" + }, + "algorithm": { + "type": "string", + "default": "GLIPH2" + }, + "all_aa_interchangeable": { + "type": "string", + "default": "1" + } + } + }, + "tcrdist3_options": { + "title": "TCRdist3 distance options", + "type": "object", + "fa_icon": "fas fa-ruler-combined", + "properties": { + "matrix_sparsity": { + "type": "string", + "default": "sparse", + "enum": [ + "sparse", + "full" + ] + }, + "distance_metric": { + "type": "string", + "default": "tcrdist" + }, + "db_path": { + "type": "string" + } + } + }, + "singlecell_options": { + "title": "Single-cell options", + "type": "object", + "fa_icon": "fas fa-dna", + "description": "Single-cell TCR modality (--mode singlecell). Ignored in bulk mode.", + "properties": { + "mode": { + "type": "string", + "enum": [ + "bulk", + "singlecell" + ], + "default": "bulk", + "description": "Pipeline modality. Default 'bulk' (omit for existing bulk runs)." + }, + "input_vdj_contigs": { + "type": "string", + "format": "path", + "description": "Cell Ranger VDJ contigs (required for --mode singlecell)." + }, + "sample_sheet": { + "type": "string", + "format": "file-path", + "description": "Single-cell sample sheet (distinct from bulk --samplesheet)." + }, + "input_annotated_object": { + "type": "string", + "format": "file-path", + "description": "Optional annotated GEX Seurat RDS. Present \u2192 full-SC route; absent \u2192 VDJ-only route." + }, + "pseudobulk_by_phenotype": { + "type": "boolean", + "default": false, + "description": "Optional: stratify pseudobulk into {sample}__{phenotype} units (secondary per-cell-type view)." + }, + "pseudobulk_qc_min_clones": { + "type": "integer", + "default": 25, + "description": "Min unique clonotypes per pseudobulk unit." + }, + "pseudobulk_qc_min_cells": { + "type": "integer", + "default": 50, + "description": "Min total cells per pseudobulk unit." + }, + "pseudobulk_qc_mode": { + "type": "string", + "enum": [ + "drop", + "hard_stop" + ], + "default": "drop", + "description": "'drop' = skip failing units; 'hard_stop' = abort run." + }, + "patient_col": { + "type": "string", + "default": "patient_id", + "description": "Metadata key used to pool units per patient for clustering." + }, + "sc_container": { + "type": "string", + "default": "syedsazaidi/scratch-tcr:latest", + "description": "Container for cell-level single-cell processes (Seurat/scanpy/CONGA/tcrdist3)." + }, + "run_conga": { + "type": "boolean", + "default": true, + "description": "Run CONGA (full-SC only)." + }, + "run_consensus": { + "type": "boolean", + "default": true, + "description": "Run consensus clustering (full-SC only)." + }, + "run_repertoire": { + "type": "boolean", + "default": true, + "description": "Run repertoire analysis." + }, + "run_master_summary": { + "type": "boolean", + "default": true, + "description": "Run master summary report." + } + } } - } }, - - "olga_options": { - "title": "OLGA options", - "type": "object", - "fa_icon": "fas fa-layer-group", - "properties": { - "olga_chunk_length": { - "type": "integer", - "default": 100000, - "minimum": 1000, - "description": "Number of sequences processed per OLGA chunk. Larger values reduce parallelization and increase memory usage." + "allOf": [ + { + "$ref": "#/$defs/input_output_options" + }, + { + "$ref": "#/$defs/resource_options" + }, + { + "$ref": "#/$defs/workflow_options" + }, + { + "$ref": "#/$defs/airr_options" + }, + { + "$ref": "#/$defs/plotting_options" + }, + { + "$ref": "#/$defs/olga_options" + }, + { + "$ref": "#/$defs/giana_options" + }, + { + "$ref": "#/$defs/gliph2_options" + }, + { + "$ref": "#/$defs/tcrdist3_options" + }, + { + "$ref": "#/$defs/singlecell_options" } - } - }, - - "giana_options": { - "title": "GIANA clustering options", - "type": "object", - "fa_icon": "fas fa-brain", - "properties": { - "threshold": { "type": "number", "default": 7.0 }, - "threshold_score": { "type": "number", "default": 3.6 }, - "threshold_vgene": { "type": "number", "default": 3.7 } - } - }, - - "gliph2_options": { - "title": "GLIPH2 clustering options", - "type": "object", - "fa_icon": "fas fa-code-branch", - "properties": { - "use_gliph2": {"type": "boolean", "default": true}, - "gliph2_report_template": { "type": "string" }, - "ref_files": { "type": "string" }, - "local_min_pvalue": { "type": "string", "default": "0.001" }, - "p_depth": { "type": "string", "default": "1000" }, - "global_convergence_cutoff": { "type": "string", "default": "1" }, - "simulation_depth": { "type": "string", "default": "1000" }, - "kmer_min_depth": { "type": "string", "default": "3" }, - "local_min_OVE": { "type": "string", "default": "c(1000, 100, 10)" }, - "algorithm": { "type": "string", "default": "GLIPH2" }, - "all_aa_interchangeable": { "type": "string", "default": "1" } - } - }, - - "tcrdist3_options": { - "title": "TCRdist3 distance options", - "type": "object", - "fa_icon": "fas fa-ruler-combined", - "properties": { - "matrix_sparsity": { - "type": "string", - "default": "sparse", - "enum": ["sparse", "full"] - }, - "distance_metric": { "type": "string", "default": "tcrdist" }, - "db_path": { "type": "string" } - } - } - }, - - "allOf": [ - { "$ref": "#/$defs/input_output_options" }, - { "$ref": "#/$defs/resource_options" }, - { "$ref": "#/$defs/workflow_options" }, - { "$ref": "#/$defs/airr_options" }, - { "$ref": "#/$defs/plotting_options" }, - { "$ref": "#/$defs/olga_options" }, - { "$ref": "#/$defs/giana_options" }, - { "$ref": "#/$defs/gliph2_options" }, - { "$ref": "#/$defs/tcrdist3_options" } - ] -} \ No newline at end of file + ] +} diff --git a/notebooks/template_details_part1.qmd b/notebooks/template_details_part1.qmd index d615125..6f99d09 100644 --- a/notebooks/template_details_part1.qmd +++ b/notebooks/template_details_part1.qmd @@ -6,7 +6,7 @@ format: toc: true toc_depth: 3 code-fold: true - embed-resources: false + embed-resources: true number-sections: true smooth-scroll: true grid: @@ -47,6 +47,10 @@ sample_table='' timepoint_col = 'timepoint' timepoint_order_col = 'timepoint_order' +# Ordered comma-separated list of timepoint values, e.g. "Base,Week4,EOT" - +# rank = position in the list. Empty string means: rank numerically if every +# timepoint value parses as a number, otherwise alphabetically. +timepoint_order = '' alias_col = 'alias' subject_col = 'subject_id' diff --git a/notebooks/template_details_part2.qmd b/notebooks/template_details_part2.qmd index e66a05a..cec4edd 100644 --- a/notebooks/template_details_part2.qmd +++ b/notebooks/template_details_part2.qmd @@ -6,7 +6,7 @@ format: toc: true toc_depth: 3 code-fold: true - embed-resources: false + embed-resources: true number-sections: true smooth-scroll: true grid: @@ -47,6 +47,10 @@ sample_table='' timepoint_col = 'timepoint' timepoint_order_col = 'timepoint_order' +# Ordered comma-separated list of timepoint values, e.g. "Base,Week4,EOT" - +# rank = position in the list. Empty string means: rank numerically if every +# timepoint value parses as a number, otherwise alphabetically. +timepoint_order = '' alias_col = 'alias' subject_col = 'subject_id' @@ -103,6 +107,4 @@ This pipeline can be used to analyze both **single-cell and bulk TCR data**. Ple {{< include ./template_sharing.qmd >}} -{{< include ./template_giana.qmd >}} - -{{< include ./template_gliph.qmd >}} +{{< include ./template_patient_clustering.qmd >}} diff --git a/notebooks/template_discovery_brief.qmd b/notebooks/template_discovery_brief.qmd index 7956ad9..d0da26d 100644 --- a/notebooks/template_discovery_brief.qmd +++ b/notebooks/template_discovery_brief.qmd @@ -60,6 +60,10 @@ sample_table='' timepoint_col = 'timepoint' timepoint_order_col = 'timepoint_order' +# Ordered comma-separated list of timepoint values, e.g. "Base,Week4,EOT" - +# rank = position in the list. Empty string means: rank numerically if every +# timepoint value parses as a number, otherwise alphabetically. +timepoint_order = '' alias_col = 'alias' subject_col = 'subject_id' @@ -143,6 +147,27 @@ print('Workflow command: ' + workflow_cmd) print('Date and time: ' + str(datetime.datetime.now())) meta = pd.read_csv(sample_table, sep=',') + +# 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 +# listed ones. When timepoint_order is empty, timepoints are ranked numerically +# if every value parses as a number, otherwise alphabetically. +if timepoint_order_col not in meta.columns: + unique_timepoints = meta[timepoint_col].dropna().unique().tolist() + if timepoint_order: + order_list = [t.strip() for t in timepoint_order.split(',')] + rank_map = {t: i for i, t in enumerate(order_list)} + unlisted = sorted(t for t in unique_timepoints if t not in rank_map) + for i, t in enumerate(unlisted): + rank_map[t] = len(order_list) + i + else: + try: + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=lambda x: float(x)))} + except (TypeError, ValueError): + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=str))} + meta[timepoint_order_col] = meta[timepoint_col].map(rank_map) + meta_cols = meta.columns.tolist() df = pd.read_csv(sample_stats_csv, sep=',') @@ -475,20 +500,20 @@ def calculate_overlaps(df, meta): # Ensure patient mapping is available sample_to_patient = meta.set_index('sample')[subject_col].to_dict() df[subject_col] = df['sample'].map(sample_to_patient) - df = df.dropna(subset=[subject_col, 'CDR3b', 'counts']) + df = df.dropna(subset=[subject_col, 'junction_aa', 'duplicate_count']) samples = sorted(df['sample'].unique()) patients = sorted(df[subject_col].unique()) - clones = sorted(df['CDR3b'].unique()) - + clones = sorted(df['junction_aa'].unique()) + clone_map = {c: i for i, c in enumerate(clones)} n_clones = len(clones) # --- 2. MORISITA (Sample-level, Frequency-based) --- sample_map = {s: i for i, s in enumerate(samples)} row_idx_s = df['sample'].map(sample_map).values - col_idx_s = df['CDR3b'].map(clone_map).values - values_s = df['counts'].values + col_idx_s = df['junction_aa'].map(clone_map).values + values_s = df['duplicate_count'].values mat_s = sparse.coo_matrix((values_s, (row_idx_s, col_idx_s)), shape=(len(samples), n_clones)).tocsr() @@ -501,12 +526,12 @@ def calculate_overlaps(df, meta): morisita_df = pd.DataFrame(morisita, index=samples, columns=samples) # --- 3. JACCARD (Patient-level, Binary-based) --- - # Aggregate counts by patient/CDR3b to get unique clones per patient - patient_df = df.groupby([subject_col, 'CDR3b']).size().reset_index() - + # Aggregate counts by patient/junction_aa to get unique clones per patient + patient_df = df.groupby([subject_col, 'junction_aa']).size().reset_index() + pat_map = {p: i for i, p in enumerate(patients)} row_idx_p = patient_df[subject_col].map(pat_map).values - col_idx_p = patient_df['CDR3b'].map(clone_map).values + col_idx_p = patient_df['junction_aa'].map(clone_map).values # Binary matrix: 1 if patient has clone, else 0 mat_p_bin = sparse.coo_matrix((np.ones(len(patient_df)), (row_idx_p, col_idx_p)), @@ -644,8 +669,8 @@ def create_upset_tabs(df, patient_list): patient_df = df[df[subject_col] == patient_id] timepoints = sorted(patient_df[timepoint_col].unique()) - # Extracting clones (CDR3b) by timepoint - clones_by_timepoint = {tp: set(patient_df[patient_df[timepoint_col] == tp]['CDR3b']) for tp in timepoints} + # Extracting clones (junction_aa) by timepoint + clones_by_timepoint = {tp: set(patient_df[patient_df[timepoint_col] == tp]['junction_aa']) for tp in timepoints} upset_data = upsetplot.from_contents(clones_by_timepoint) @@ -740,11 +765,11 @@ for patient_id in upset_patients: patient_df = concat_df[concat_df[subject_col] == patient_id] # 1. Aggregate counts for each clone at each timepoint - # (Using 'counts' as the abundance metric based on your previous structures) - clone_tp_data = patient_df.groupby(['CDR3b', timepoint_col])['counts'].sum().reset_index() + # (Using 'duplicate_count' as the abundance metric based on your previous structures) + clone_tp_data = patient_df.groupby(['junction_aa', timepoint_col])['duplicate_count'].sum().reset_index() # 2. Calculate the number of timepoints and list them for each clone - clone_stats = clone_tp_data.groupby('CDR3b').agg( + clone_stats = clone_tp_data.groupby('junction_aa').agg( n_timepoints=(timepoint_col, 'nunique'), timepoints_present=(timepoint_col, lambda x: ', '.join(sorted(x.astype(str)))) ) @@ -757,12 +782,12 @@ for patient_id in upset_patients: # 4. Pivot the data to get frequencies (counts) as separate columns per timepoint persistent_clones = persistent_stats.index - persistent_counts_df = clone_tp_data[clone_tp_data['CDR3b'].isin(persistent_clones)] + persistent_counts_df = clone_tp_data[clone_tp_data['junction_aa'].isin(persistent_clones)] wide_counts = persistent_counts_df.pivot( - index='CDR3b', + index='junction_aa', columns=timepoint_col, - values='counts' + values='duplicate_count' ).fillna(0) # Fill missing timepoints with 0 counts # Prefix the timepoint columns so they are clearly identifiable as counts @@ -874,17 +899,17 @@ from statsmodels.stats.multitest import multipletests clonotypes_df = concat_df.copy() clonotypes_df = clonotypes_df.dropna(subset=[timepoint_col, 'origin']) -clone_cols = ['CDR3b', 'TRBV', 'TRBJ'] +clone_cols = ['junction_aa', 'v_call', 'j_call'] merge_cols = clone_cols + [subject_col, 'origin'] clonotypes_df['timepoint_rank'] = clonotypes_df[timepoint_order_col] clonotypes_df = clonotypes_df.sort_values('timepoint_rank') # Grouping by subject, origin, and timepoint for accurate total counts -sample_total_counts = clonotypes_df.groupby([subject_col, 'origin', timepoint_col])['counts'].sum().reset_index() -sample_total_counts.rename(columns={'counts': 'total_counts'}, inplace=True) +sample_total_counts = clonotypes_df.groupby([subject_col, 'origin', timepoint_col])['duplicate_count'].sum().reset_index() +sample_total_counts.rename(columns={'duplicate_count': 'total_counts'}, inplace=True) clonotypes_df = pd.merge(clonotypes_df, sample_total_counts, on=[subject_col, 'origin', timepoint_col]) -clonotypes_df['frequency'] = clonotypes_df['counts'] / clonotypes_df['total_counts'] +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): @@ -911,8 +936,8 @@ for (subject, origin), subject_df in clonotypes_df.groupby([subject_col, 'origin continue freq_pivot = subject_df.pivot_table(index=clone_cols, columns=timepoint_col, values='frequency', fill_value=0) - counts_pivot = subject_df.pivot_table(index=clone_cols, columns=timepoint_col, values='counts', fill_value=0) - subject_total_counts = subject_df.groupby(timepoint_col)['counts'].sum() + counts_pivot = subject_df.pivot_table(index=clone_cols, columns=timepoint_col, values='duplicate_count', fill_value=0) + subject_total_counts = subject_df.groupby(timepoint_col)['duplicate_count'].sum() for t_pre, t_post in itertools.combinations(timepoints, 2): # 1. Vectorized DataFrame creation instead of iterrows @@ -1036,8 +1061,8 @@ for (subject, origin), subject_df in clonotypes_df.groupby([subject_col, 'origin continue freq_pivot = subject_df.pivot_table(index=clone_cols, columns=timepoint_col, values='frequency', fill_value=0) - counts_pivot = subject_df.pivot_table(index=clone_cols, columns=timepoint_col, values='counts', fill_value=0) - subject_total_counts = subject_df.groupby(timepoint_col)['counts'].sum() + counts_pivot = subject_df.pivot_table(index=clone_cols, columns=timepoint_col, values='duplicate_count', fill_value=0) + subject_total_counts = subject_df.groupby(timepoint_col)['duplicate_count'].sum() for t_pre, t_post in itertools.combinations(timepoints, 2): # 1. Vectorize @@ -1149,7 +1174,7 @@ import pandas as pd # --- 1. Identify "New" vs "Pre-existing" via Cumulative History --- -present_clones = clonotypes_df[clonotypes_df['counts'] > 0].copy() +present_clones = clonotypes_df[clonotypes_df['duplicate_count'] > 0].copy() present_clones = present_clones.sort_values([subject_col, timepoint_order_col]) dynamic_results = [] @@ -1161,7 +1186,7 @@ if 'detailed_comparisons_table' in locals() and not detailed_comparisons_table.e (detailed_comparisons_table['fold_change'] > fc_thold_up) & (pd.to_numeric(detailed_comparisons_table['q_value'], errors='coerce') < signif_thold) ] - expanded_set = set(zip(sig_exp[subject_col], sig_exp['origin'], sig_exp['t_post'], sig_exp['CDR3b'])) + expanded_set = set(zip(sig_exp[subject_col], sig_exp['origin'], sig_exp['t_post'], sig_exp['junction_aa'])) contracted_set = set() if 'detailed_comparisons_table_cont' in locals() and not detailed_comparisons_table_cont.empty: @@ -1169,7 +1194,7 @@ if 'detailed_comparisons_table_cont' in locals() and not detailed_comparisons_ta (detailed_comparisons_table_cont['fold_change'] < fc_thold_down) & (pd.to_numeric(detailed_comparisons_table_cont['q_value'], errors='coerce') < signif_thold) ] - contracted_set = set(zip(sig_cont[subject_col], sig_cont['origin'], sig_cont['t_post'], sig_cont['CDR3b'])) + contracted_set = set(zip(sig_cont[subject_col], sig_cont['origin'], sig_cont['t_post'], sig_cont['junction_aa'])) # Iterate over both subject AND origin @@ -1179,7 +1204,7 @@ for (subject, origin), subj_df in present_clones.groupby([subject_col, 'origin'] for rank in timepoints: tp_name = subj_df[subj_df[timepoint_order_col] == rank][timepoint_col].iloc[0] - current_clones = subj_df[subj_df[timepoint_order_col] == rank]['CDR3b'].unique() + current_clones = subj_df[subj_df[timepoint_order_col] == rank]['junction_aa'].unique() for clone in current_clones: status = "Stable" # Default @@ -1202,7 +1227,7 @@ for (subject, origin), subj_df in present_clones.groupby([subject_col, 'origin'] 'origin': origin, # Ensure origin is appended here! timepoint_col: tp_name, timepoint_order_col: rank, - 'CDR3b': clone, + 'junction_aa': clone, 'Status': status }) @@ -1356,19 +1381,19 @@ In this visualization, the total unique repertoire at each timepoint is broken d # --- Save Detailed Clone Categories with Info for further analysis --- # Merge the categorical statuses back with the original data to keep counts, frequencies, and gene usage -export_cols = [subject_col, timepoint_col, 'CDR3b', 'TRBV', 'TRBJ', 'counts', 'frequency'] +export_cols = [subject_col, timepoint_col, 'junction_aa', 'v_call', 'j_call', 'duplicate_count', 'frequency'] export_cols = [c for c in export_cols if c in present_clones.columns] # Safely keep only existing columns detailed_export_df = pd.merge( plot_df, # Contains subject, timepoint, timepoint_order, CDR3b, Status present_clones[export_cols], - on=[subject_col, timepoint_col, 'CDR3b'], + on=[subject_col, timepoint_col, 'junction_aa'], how='left' ) # Sort it logically: By Patient -> Chronological Timepoint -> Status -> Largest Clones first detailed_export_df = detailed_export_df.sort_values( - by=[subject_col, timepoint_order_col, 'Status', 'counts'], + by=[subject_col, timepoint_order_col, 'Status', 'duplicate_count'], ascending=[True, True, True, False] ) ``` @@ -1411,10 +1436,10 @@ if 'detailed_comparisons_table_cont' in locals() and not detailed_comparisons_ta # Combine them if sig_stats_list: all_sig_stats = pd.concat(sig_stats_list, ignore_index=True) - stats_to_merge = all_sig_stats[[subject_col, 'origin', 't_post', 'CDR3b', 't_pre', 'freq_pre', 'freq_post', 'p_value']] + stats_to_merge = all_sig_stats[[subject_col, 'origin', 't_post', 'junction_aa', 't_pre', 'freq_pre', 'freq_post', 'p_value']] else: # Fallback if there are absolutely zero significant clones in the entire dataset - stats_to_merge = pd.DataFrame(columns=[subject_col, 'origin', 't_post', 'CDR3b', 't_pre', 'freq_pre', 'freq_post', 'p_value']) + stats_to_merge = pd.DataFrame(columns=[subject_col, 'origin', 't_post', 'junction_aa', 't_pre', 'freq_pre', 'freq_post', 'p_value']) # 4. Merge the plotting classifications with the exact statistical comparisons # Note: A clone might match multiple 't_pre' comparisons. This merge will mathematically branch @@ -1422,16 +1447,16 @@ else: merged_df = pd.merge( dynamic_clones_df, stats_to_merge, - on=[subject_col, 'origin', 't_post', 'CDR3b'], + on=[subject_col, 'origin', 't_post', 'junction_aa'], how='left' ) # 5. Fetch actual frequencies for "New" clones (since they don't have a Fisher test row) -current_freqs = clonotypes_df[[subject_col, 'origin', timepoint_col, 'CDR3b', 'frequency', 'TRBV', 'TRBJ']].rename( +current_freqs = clonotypes_df[[subject_col, 'origin', timepoint_col, 'junction_aa', 'frequency', 'v_call', 'j_call']].rename( columns={timepoint_col: 't_post', 'frequency': 'current_frequency'} ).drop_duplicates() -merged_df = pd.merge(merged_df, current_freqs, on=[subject_col, 'origin', 't_post', 'CDR3b'], how='left') +merged_df = pd.merge(merged_df, current_freqs, on=[subject_col, 'origin', 't_post', 'junction_aa'], how='left') # Fill in the blanks for the "New" clones merged_df['freq_post'] = merged_df['freq_post'].fillna(merged_df['current_frequency']) @@ -1441,7 +1466,7 @@ merged_df['t_pre'] = merged_df['t_pre'].fillna("Not Present Previously") # 6. Clean up and Rename Columns for the final export final_cols = [ - subject_col, 'origin', 'CDR3b', 'TRBV', 'TRBJ', + subject_col, 'origin', 'junction_aa', 'v_call', 'j_call', 'Status', 't_post', 't_pre', 'freq_post', 'freq_pre', 'p_value' ] export_df = merged_df[[c for c in final_cols if c in merged_df.columns]].copy() @@ -1556,7 +1581,7 @@ if 'detailed_comparisons_table' in locals() and not detailed_comparisons_table.e (pd.to_numeric(detailed_comparisons_table['q_value'], errors='coerce') < signif_thold) ] # Store tuple of (subject, origin, t_pre, t_post, CDR3b) - sig_exp_lookup = set(zip(sig_exp[subject_col], sig_exp['origin'], sig_exp['t_pre'], sig_exp['t_post'], sig_exp['CDR3b'])) + sig_exp_lookup = set(zip(sig_exp[subject_col], sig_exp['origin'], sig_exp['t_pre'], sig_exp['t_post'], sig_exp['junction_aa'])) sig_cont_lookup = set() if 'detailed_comparisons_table_cont' in locals() and not detailed_comparisons_table_cont.empty: @@ -1564,7 +1589,7 @@ if 'detailed_comparisons_table_cont' in locals() and not detailed_comparisons_ta (detailed_comparisons_table_cont['fold_change'] < fc_thold_down) & (pd.to_numeric(detailed_comparisons_table_cont['q_value'], errors='coerce') < signif_thold) ] - sig_cont_lookup = set(zip(sig_cont[subject_col], sig_cont['origin'], sig_cont['t_pre'], sig_cont['t_post'], sig_cont['CDR3b'])) + sig_cont_lookup = set(zip(sig_cont[subject_col], sig_cont['origin'], sig_cont['t_pre'], sig_cont['t_post'], sig_cont['junction_aa'])) # Define our color palette color_discrete_map = { @@ -1600,7 +1625,7 @@ for subject, subj_df in clonotypes_df.groupby(subject_col): continue pivot = pair_df.pivot_table( - index=['CDR3b', 'TRBV', 'TRBJ'], + index=['junction_aa', 'v_call', 'j_call'], columns=timepoint_col, values='frequency', fill_value=0 @@ -1629,7 +1654,7 @@ for subject, subj_df in clonotypes_df.groupby(subject_col): # Assign highlight status using our sets def assign_status(row): - key = (subject, origin, t1, t2, row['CDR3b']) + key = (subject, origin, t1, t2, row['junction_aa']) if key in sig_exp_lookup: return "Expanded" elif key in sig_cont_lookup: @@ -1651,8 +1676,8 @@ for subject, subj_df in clonotypes_df.groupby(subject_col): color='Status', color_discrete_map=color_discrete_map, hover_data={ - 'CDR3b': True, - 'TRBV': True, + 'junction_aa': True, + 'v_call': True, t1: ':.6f', # Show the true frequency on hover (including actual 0s) t2: ':.6f', 'freq_t1_plot': False, @@ -1908,7 +1933,7 @@ else: (detailed_export_df[subject_col] == patient) & (detailed_export_df['Status'] == 'Expanded') ] - expanded_tcr_set = set(patient_expanded_df['CDR3b'].tolist()) + expanded_tcr_set = set(patient_expanded_df['junction_aa'].tolist()) # 2. Iterate through connected components and keep only those with at least one expanded clone keep_vids = [] @@ -1988,12 +2013,9 @@ for _, row in meta.iterrows(): filepath = row['file'] if os.path.exists(convert_dir) and os.listdir(convert_dir): - base_name = os.path.basename(row['file']) - name_part, ext_part = os.path.splitext(base_name) - new_file_name = f"{name_part}_airr{ext_part}" - - # Update the path to point to convert_dir - target_file = os.path.join(convert_dir, new_file_name) + # Converted files are named by sample (e.g. "_airr.tsv"), not by + # the original raw input filename. + target_file = os.path.join(convert_dir, f"{row['sample']}_airr.tsv") else: target_file = row['file'] @@ -2200,8 +2222,7 @@ import numpy as np exp_df = detailed_export_df[detailed_export_df['Status'] == 'Expanded'].copy() rename_dict = {} -if 'CDR3b' in exp_df.columns: rename_dict['CDR3b'] = 'cdr3aa' -if subject_col in exp_df.columns and subject_col != subject_col: rename_dict[subject_col] = subject_col +if 'junction_aa' in exp_df.columns: rename_dict['junction_aa'] = 'cdr3aa' exp_df = exp_df.rename(columns=rename_dict) expanded_annotated = pd.merge( @@ -2349,18 +2370,18 @@ import pandas as pd def get_public_clones_df(concat_df, pgen_df): """Calculates and formats the full table of public (shared) clones.""" # Create the "Wide" Matrix (One Column per Patient) - patient_map = concat_df.groupby(['CDR3b', subject_col])[alias_col].apply( + patient_map = concat_df.groupby(['junction_aa', subject_col])[alias_col].apply( lambda x: ', '.join(sorted(x.unique().astype(str))) ).unstack(fill_value='-') # Calculate Summary Stats (Total Patients) - stats_df = concat_df.groupby('CDR3b').agg( + stats_df = concat_df.groupby('junction_aa').agg( total_patients=(subject_col, 'nunique') ).reset_index() # Merge Everything Together - df = pd.merge(stats_df, pgen_df[['CDR3b', 'pgen']], on='CDR3b', how='inner') - df = pd.merge(df, patient_map, on='CDR3b', how='inner') + df = pd.merge(stats_df, pgen_df[['junction_aa', 'pgen']], on='junction_aa', how='inner') + df = pd.merge(df, patient_map, on='junction_aa', how='inner') # Filter for public clones (>1 patient) and Sort df = df[df['total_patients'] > 1].copy() @@ -2369,7 +2390,7 @@ def get_public_clones_df(concat_df, pgen_df): # Formatting for Display/Export df['pgen'] = df['pgen'].apply(lambda x: f"{x:.2e}") df.rename(columns={ - 'CDR3b': 'CDR3b Sequence', + 'junction_aa': 'CDR3b Sequence', 'total_patients': 'No_Shared_Individuals', # Updated column name 'pgen': 'Pgen' }, inplace=True) @@ -2454,10 +2475,10 @@ def plot_public_clones_upset(concat_df, subject_col, min_shared_patients=2): # --- DEFENSIVE FIX 1: Drop NaNs --- # Missing sequences cause duplicate MultiIndex entries, breaking Matplotlib text rendering - df_clean = concat_df.dropna(subset=[subject_col, 'CDR3b']).copy() + df_clean = concat_df.dropna(subset=[subject_col, 'junction_aa']).copy() # --- 1. Extract Unique Clones per Patient --- - patient_clones = df_clean.groupby(subject_col)['CDR3b'].unique().to_dict() + 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()} # --- 2. Convert to UpSet Multi-Index Format --- @@ -2526,6 +2547,4 @@ plot_public_clones_upset(concat_df, subject_col=subject_col, min_shared_patients ``` **Figure 8. UpSet plot of public TCR clones**. This plot illustrates the intersection of unique CDR3b sequences shared among multiple individuals. The horizontal bars on the left indicate the total number of unique clones identified in each individual patient. In the central matrix, connected black dots denote the specific combination of patients sharing a subset of clones. The vertical bars at the top represent the intersection size, indicating the exact number of public clones shared exclusively by the patients marked in the corresponding column below. -{{< include ./template_pheno_sc.qmd >}} - -{{< include ./template_pheno_bulk.qmd >}} \ No newline at end of file +{{< include ./template_pheno.qmd >}} \ No newline at end of file diff --git a/notebooks/template_giana.qmd b/notebooks/template_giana.qmd index e8f1b7e..ec4894b 100644 --- a/notebooks/template_giana.qmd +++ b/notebooks/template_giana.qmd @@ -18,41 +18,13 @@ notebook_results_dir = f"{project_dir}/notebook-analysis/" #| code-fold: true # 1. Load Packages -from IPython.display import Image -import os -import datetime -import sys import pandas as pd -import math -import matplotlib.pyplot as plt -import seaborn as sns -from matplotlib.colors import LinearSegmentedColormap import plotly.express as px import plotly.graph_objects as go -import glob import itertools -import h5py -import igraph as ig -import matplotlib.ticker as ticker import numpy as np import scipy.cluster.hierarchy as sch from IPython.display import display, Markdown -from scipy.sparse import csr_matrix -from scipy.stats import gaussian_kde -from scipy.stats import entropy -from scipy.stats import skew, wasserstein_distance -from scipy.stats import pearsonr -from sklearn.decomposition import PCA -from sklearn.preprocessing import StandardScaler -from scipy import sparse -from sklearn.preprocessing import normalize -from scipy.stats import wilcoxon -from scipy.stats import mannwhitneyu -from itertools import combinations -from scipy.spatial.distance import pdist -from scipy.cluster.hierarchy import linkage, leaves_list -import plotly.figure_factory as ff -import re import warnings warnings.filterwarnings( @@ -89,7 +61,6 @@ GIANA stands out from other TCR analysis tools for several key reasons: import pandas as pd import plotly.graph_objects as go -import os from IPython.display import display, Markdown def plot_giana_tables_per_patient(meta_df, giana_dir, subject_col, timepoint_col, min_unique_thresholds=[0, 1]): @@ -172,8 +143,13 @@ def plot_giana_tables_per_patient(meta_df, giana_dir, subject_col, timepoint_col display(Markdown(f"*[WARNING] No GIANA file found for patient '{patient}' (expected: {giana_file}). Skipping.*\n\n")) continue - # Load this patient's GIANA output - giana_df = pd.read_csv(giana_file, comment='#', sep='\t') + # GIANA writes only "##" comment lines when it finds zero clusters, + # which leaves pandas nothing to parse after comment='#' strips them. + try: + giana_df = pd.read_csv(giana_file, comment='#', sep='\t') + except pd.errors.EmptyDataError: + display(Markdown(f"*[INFO] No GIANA clusters found for patient '{patient}'. Skipping.*\n\n")) + continue # Merge with all samples belonging to this patient patient_meta = meta_df[meta_df[subject_col] == patient][['sample', subject_col, timepoint_col]] @@ -190,7 +166,7 @@ def plot_giana_tables_per_patient(meta_df, giana_dir, subject_col, timepoint_col # Aggregate cluster stats across all samples for this patient cluster_stats = df.groupby('cluster').agg( - Total_Counts=('counts', 'sum'), + Total_Counts=('duplicate_count', 'sum'), Unique_TCRs=('CDR3b', 'nunique') ).reset_index() @@ -248,8 +224,13 @@ for patient in meta[subject_col].unique(): if not os.path.exists(giana_file): continue - # Load this patient's GIANA output - giana_df = pd.read_csv(giana_file, comment='#', sep='\t') + # See the comment on the equivalent read above: GIANA writes only + # comment lines with zero clusters, which leaves nothing for pandas to + # parse once comment='#' strips them. + try: + giana_df = pd.read_csv(giana_file, comment='#', sep='\t') + except pd.errors.EmptyDataError: + continue # Merge with all samples belonging to this patient patient_meta = meta[meta[subject_col] == patient][['sample', subject_col, timepoint_col]] @@ -261,7 +242,7 @@ for patient in meta[subject_col].unique(): # 1. Aggregate cluster stats (All clusters, no min_unique threshold) cluster_stats = df.groupby('cluster').agg( - Total_Counts=('counts', 'sum'), + Total_Counts=('duplicate_count', 'sum'), Unique_TCRs=('CDR3b', 'nunique') ).reset_index() @@ -310,8 +291,6 @@ However, simply identifying the existence of these top clusters is only the firs import os import pandas as pd import plotly.graph_objects as go -import matplotlib.pyplot as plt -from upsetplot import from_contents, plot as upset_plot import warnings import itertools @@ -324,18 +303,25 @@ for patient in meta[subject_col].unique(): giana_file = os.path.join(giana_dir, f"{patient}_giana.txt") if os.path.exists(giana_file): - patient_df = pd.read_csv(giana_file, comment='#', sep='\t') - - # CRITICAL FIX: Make cluster IDs unique to the patient + # See the comment on the first read in this notebook: GIANA writes + # only comment lines with zero clusters, which leaves nothing for + # pandas to parse once comment='#' strips them. + try: + patient_df = pd.read_csv(giana_file, comment='#', sep='\t') + except pd.errors.EmptyDataError: + print(f"*(No GIANA clusters found for: {giana_file})*") + continue + + # CRITICAL FIX: Make cluster IDs unique to the patient # to prevent false merging across different GIANA runs patient_df['cluster'] = str(patient) + "_c" + patient_df['cluster'].astype(str) - + all_giana_data.append(patient_df) else: print(f"*(No GIANA file found for: {giana_file})*") # Combine into a single master dataframe -giana_df = pd.concat(all_giana_data, ignore_index=True) +giana_df = pd.concat(all_giana_data, ignore_index=True) if all_giana_data else pd.DataFrame() # Merge metadata giana_df = giana_df.merge(meta[['sample', subject_col, timepoint_col, alias_col]], on='sample', how='left') @@ -350,7 +336,7 @@ giana_df = giana_df.dropna(subset=[subject_col]) # Level 2: Cluster Level (Innermost Ring) df_cluster = giana_df.groupby([alias_col, 'cluster']).agg( - total_counts=('counts', 'sum'), + total_counts=('duplicate_count', 'sum'), n_unique_tcrs=('CDR3b', 'nunique') ).reset_index() @@ -360,7 +346,7 @@ df_cluster['label'] = df_cluster['cluster'].astype(str) # Level 1: Alias Level (Middle Ring) df_sample = giana_df.groupby(alias_col).agg( - total_counts=('counts', 'sum'), + total_counts=('duplicate_count', 'sum'), n_unique_tcrs=('CDR3b', 'nunique') ).reset_index() @@ -369,7 +355,7 @@ df_sample['parent'] = 'All Samples' df_sample['label'] = df_sample[alias_col] # Level 0: Root Level (Center) -total_counts_root = giana_df['counts'].sum() +total_counts_root = giana_df['duplicate_count'].sum() unique_tcrs_root = giana_df['CDR3b'].nunique() df_root = pd.DataFrame({ @@ -418,7 +404,7 @@ fig.show() # Level 2: Alias Level (Outer Ring) df_alias_level = (giana_df.groupby(['cluster', alias_col]) - .agg(total_counts=('counts', 'sum'), n_unique_tcrs=('CDR3b', 'nunique')) + .agg(total_counts=('duplicate_count', 'sum'), n_unique_tcrs=('CDR3b', 'nunique')) .reset_index() .assign(id=lambda x: x['cluster'].astype(str) + '_' + x[alias_col], parent=lambda x: x['cluster'].astype(str), @@ -426,7 +412,7 @@ df_alias_level = (giana_df.groupby(['cluster', alias_col]) # Level 1: Cluster Level (Middle Ring) df_cluster_level = (giana_df.groupby('cluster') - .agg(total_counts=('counts', 'sum'), n_unique_tcrs=('CDR3b', 'nunique')) + .agg(total_counts=('duplicate_count', 'sum'), n_unique_tcrs=('CDR3b', 'nunique')) .reset_index() .assign(id=lambda x: x['cluster'].astype(str), parent='All Clusters', @@ -437,7 +423,7 @@ df_root = pd.DataFrame([{ 'id': 'All Clusters', 'parent': '', 'label': 'All Clusters', - 'total_counts': giana_df['counts'].sum(), + 'total_counts': giana_df['duplicate_count'].sum(), 'n_unique_tcrs': giana_df['CDR3b'].nunique() }]) @@ -565,9 +551,9 @@ def create_patient_tabs_abundance_scatter(df): print(f"## {patient_id}\n") patient_df = df[df[subject_col] == patient_id] - sample_depths = patient_df.groupby(timepoint_col)['counts'].sum() + sample_depths = patient_df.groupby(timepoint_col)['duplicate_count'].sum() patient_pivot = patient_df.pivot_table( - index='cluster', columns=timepoint_col, values='counts', aggfunc='sum' + index='cluster', columns=timepoint_col, values='duplicate_count', aggfunc='sum' ).fillna(0) patient_cpm = patient_pivot.div(sample_depths, axis=1) * 1e6 diff --git a/notebooks/template_gliph.qmd b/notebooks/template_gliph.qmd index 267a9cd..004975b 100644 --- a/notebooks/template_gliph.qmd +++ b/notebooks/template_gliph.qmd @@ -22,22 +22,36 @@ gliph2_dir = f"{project_dir}/gliph2/" ```{python} #| include: false -from IPython.display import Image, HTML, display import os -import datetime import pandas as pd import matplotlib.pyplot as plt import plotly.express as px import plotly.graph_objects as go -import igraph as ig -import logomaker -import io -import base64 -import json import warnings # - Loading data meta = pd.read_csv(sample_table, sep=',') + +# 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 +# listed ones. When timepoint_order is empty, timepoints are ranked numerically +# if every value parses as a number, otherwise alphabetically. +if timepoint_order_col not in meta.columns: + unique_timepoints = meta[timepoint_col].dropna().unique().tolist() + if timepoint_order: + order_list = [t.strip() for t in timepoint_order.split(',')] + rank_map = {t: i for i, t in enumerate(order_list)} + unlisted = sorted(t for t in unique_timepoints if t not in rank_map) + for i, t in enumerate(unlisted): + rank_map[t] = len(order_list) + i + else: + try: + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=lambda x: float(x)))} + except (TypeError, ValueError): + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=str))} + meta[timepoint_order_col] = meta[timepoint_col].map(rank_map) + 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') @@ -50,27 +64,32 @@ global_similarities_list = [] for patient in meta[subject_col].unique(): p_dir = f"{gliph2_dir}/{patient}" - cd_csv = f"{p_dir}/cluster_member_details.txt" - cn_csv = f"{p_dir}/clone_network.txt" - am_csv = f"{p_dir}/all_motifs.txt" - gs_csv = f"{p_dir}/global_similarities.txt" + cd_csv = f"{p_dir}/{patient}_cluster_member_details.txt" + cn_csv = f"{p_dir}/{patient}_clone_network.txt" + am_csv = f"{p_dir}/{patient}_all_motifs.txt" + gs_csv = f"{p_dir}/{patient}_global_similarities.txt" - if os.path.exists(cd_csv): + # Some GLIPH2 outputs are genuinely 0-byte files when it finds no + # clusters, which crashes pd.read_csv - skip empty files too, not just missing. + def _has_content(path): + return os.path.exists(path) and os.path.getsize(path) > 0 + + if _has_content(cd_csv): df = pd.read_csv(cd_csv, sep='\t') df[subject_col] = patient cluster_details_list.append(df) - if os.path.exists(cn_csv): + if _has_content(cn_csv): # clone_network has no header - df = pd.read_csv(cn_csv, sep='\t', header=None) + df = pd.read_csv(cn_csv, sep='\t', header=None) clone_network_list.append(df) - if os.path.exists(am_csv): + if _has_content(am_csv): df = pd.read_csv(am_csv, sep='\t') df[subject_col] = patient all_motifs_list.append(df) - if os.path.exists(gs_csv): + if _has_content(gs_csv): df = pd.read_csv(gs_csv, sep='\t') df[subject_col] = patient # CRITICAL: Prefix cluster tags to prevent cross-patient collisions @@ -102,89 +121,92 @@ import pandas as pd import igraph as ig # --- 1. Prepare Data & Hover Info --- -merged_meta = pd.merge(cluster_details, meta[['sample', alias_col]], on='sample', how='left').dropna(subset=[subject_col]) - -# A. Categorize Nodes (Public vs Private) -cdr3_subject_counts = merged_meta.groupby('CDR3b')[subject_col].nunique() -public_cdr3s = set(cdr3_subject_counts[cdr3_subject_counts > 1].index) - -# B. Create Detailed Hover Text -def create_hover_string(group): - patient_summaries = [] - for subj, subgroup in group.groupby(subject_col): - aliases = ", ".join(sorted(subgroup[alias_col].unique().astype(str))) - patient_summaries.append(f"{subj} ({aliases})") - return "
".join(patient_summaries) - -hover_map = merged_meta.groupby('CDR3b').apply(create_hover_string, include_groups=False).to_dict() - -# C. Create Category Map -private_df = merged_meta[~merged_meta['CDR3b'].isin(public_cdr3s)] -category_map = dict(zip(private_df['CDR3b'], private_df[subject_col])) - -def get_node_category(cdr3): - if cdr3 in public_cdr3s: return "Shared" - return category_map.get(cdr3, "Unknown") - -# --- 2. Build Graph --- -# Isolate just the 4 core GLIPH columns in case extra were appended during concatenation -clone_network = clone_network.iloc[:, :4] -clone_network.columns = ["source", "target", "type", "group"] -clone_network = clone_network[clone_network['type'] != 'singleton'] +if cluster_details.empty or clone_network.empty: + print("No GLIPH2 data available (use_gliph2 disabled, or no clusters found for any patient) - skipping network plot.") +else: + merged_meta = pd.merge(cluster_details, meta[['sample', alias_col]], on='sample', how='left').dropna(subset=[subject_col]) + + # A. Categorize Nodes (Public vs Private) + cdr3_subject_counts = merged_meta.groupby('CDR3b')[subject_col].nunique() + public_cdr3s = set(cdr3_subject_counts[cdr3_subject_counts > 1].index) + + # B. Create Detailed Hover Text + def create_hover_string(group): + patient_summaries = [] + for subj, subgroup in group.groupby(subject_col): + aliases = ", ".join(sorted(subgroup[alias_col].unique().astype(str))) + patient_summaries.append(f"{subj} ({aliases})") + return "
".join(patient_summaries) + + hover_map = merged_meta.groupby('CDR3b').apply(create_hover_string, include_groups=False).to_dict() + + # C. Create Category Map + private_df = merged_meta[~merged_meta['CDR3b'].isin(public_cdr3s)] + category_map = dict(zip(private_df['CDR3b'], private_df[subject_col])) + + def get_node_category(cdr3): + if cdr3 in public_cdr3s: return "Shared" + return category_map.get(cdr3, "Unknown") + + # --- 2. Build Graph --- + # Isolate just the 4 core GLIPH columns in case extra were appended during concatenation + clone_network = clone_network.iloc[:, :4] + clone_network.columns = ["source", "target", "type", "group"] + clone_network = clone_network[clone_network['type'] != 'singleton'] + + edges = clone_network[["source", "target"]].values.tolist() + g = ig.Graph.TupleList(edges, directed=False) + + g.vs['category'] = [get_node_category(v['name']) for v in g.vs] + g.vs['hover_detail'] = [hover_map.get(v['name'], "No Data") for v in g.vs] + + layout = g.layout("fr") + coords = list(map(tuple, layout)) + + # --- 3. Plotting --- + fig = go.Figure() + + edge_x, edge_y = [], [] + for edge in g.es: + src, tgt = edge.tuple + x0, y0 = coords[src] + x1, y1 = coords[tgt] + edge_x.extend([x0, x1, None]) + edge_y.extend([y0, y1, None]) -edges = clone_network[["source", "target"]].values.tolist() -g = ig.Graph.TupleList(edges, directed=False) - -g.vs['category'] = [get_node_category(v['name']) for v in g.vs] -g.vs['hover_detail'] = [hover_map.get(v['name'], "No Data") for v in g.vs] - -layout = g.layout("fr") -coords = list(map(tuple, layout)) - -# --- 3. Plotting --- -fig = go.Figure() - -edge_x, edge_y = [], [] -for edge in g.es: - src, tgt = edge.tuple - x0, y0 = coords[src] - x1, y1 = coords[tgt] - edge_x.extend([x0, x1, None]) - edge_y.extend([y0, y1, None]) - -fig.add_trace(go.Scatter( - x=edge_x, y=edge_y, line=dict(width=0.5, color='#cccccc'), - hoverinfo='none', mode='lines', showlegend=False -)) + fig.add_trace(go.Scatter( + x=edge_x, y=edge_y, line=dict(width=0.5, color='#cccccc'), + hoverinfo='none', mode='lines', showlegend=False + )) -categories = sorted(list(set(g.vs['category']))) -base_colors = px.colors.qualitative.Bold -color_map = {cat: base_colors[i % len(base_colors)] for i, cat in enumerate(categories)} -color_map["Shared"] = "black" + categories = sorted(list(set(g.vs['category']))) + base_colors = px.colors.qualitative.Bold + color_map = {cat: base_colors[i % len(base_colors)] for i, cat in enumerate(categories)} + color_map["Shared"] = "black" -for cat in categories: - indices = [i for i, v in enumerate(g.vs) if v['category'] == cat] - if not indices: continue + for cat in categories: + indices = [i for i, v in enumerate(g.vs) if v['category'] == cat] + if not indices: continue - node_x = [coords[i][0] for i in indices] - node_y = [coords[i][1] for i in indices] - hover_texts = [f"CDR3: {g.vs[i]['name']}
Found in:
{g.vs[i]['hover_detail']}" for i in indices] + node_x = [coords[i][0] for i in indices] + node_y = [coords[i][1] for i in indices] + hover_texts = [f"CDR3: {g.vs[i]['name']}
Found in:
{g.vs[i]['hover_detail']}" for i in indices] - node_size = 18 if cat == "Shared" else 12 - line_width = 2 if cat == "Shared" else 1 + node_size = 18 if cat == "Shared" else 12 + line_width = 2 if cat == "Shared" else 1 - fig.add_trace(go.Scatter( - x=node_x, y=node_y, mode='markers', name=str(cat), text=hover_texts, hoverinfo='text', - marker=dict(color=color_map[cat], size=node_size, line=dict(width=line_width, color='white')) - )) + fig.add_trace(go.Scatter( + x=node_x, y=node_y, mode='markers', name=str(cat), text=hover_texts, hoverinfo='text', + marker=dict(color=color_map[cat], size=node_size, line=dict(width=line_width, color='white')) + )) -fig.update_layout( - title='TCRB Network', plot_bgcolor='white', width=1000, height=800, - xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), - yaxis=dict(showgrid=False, zeroline=False, showticklabels=False), - margin=dict(t=40, b=20, l=10, r=10), legend_title_text='Category' -) -fig.show() + fig.update_layout( + title='TCRB Network', plot_bgcolor='white', width=1000, height=800, + xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), + yaxis=dict(showgrid=False, zeroline=False, showticklabels=False), + margin=dict(t=40, b=20, l=10, r=10), legend_title_text='Category' + ) + fig.show() ``` @@ -255,7 +277,10 @@ def create_patient_histogram_tabs(all_motifs, subject_col): print(":::\n") # Run -create_patient_histogram_tabs(all_motifs, subject_col) +if all_motifs.empty: + print("No GLIPH2 motif data available - skipping.") +else: + create_patient_histogram_tabs(all_motifs, subject_col) ``` @@ -334,7 +359,10 @@ def create_nested_motif_tabs(all_motifs, subject_col): print(":::\n") # Run -create_nested_motif_tabs(all_motifs, subject_col) +if all_motifs.empty: + print("No GLIPH2 motif data available - skipping.") +else: + create_nested_motif_tabs(all_motifs, subject_col) ``` diff --git a/notebooks/template_overlap.qmd b/notebooks/template_overlap.qmd index 2b7961a..b6079b6 100644 --- a/notebooks/template_overlap.qmd +++ b/notebooks/template_overlap.qmd @@ -22,7 +22,6 @@ concat_csv = f"{project_dir}/annotate/concatenated_cdr3_sorted.tsv" # Load Packages from IPython.display import Image, display, Markdown, HTML from matplotlib.colors import LinearSegmentedColormap -from io import StringIO from scipy.sparse import csr_matrix from scipy.stats import gaussian_kde, fisher_exact from statsmodels.stats.multitest import multipletests @@ -30,7 +29,6 @@ from scipy.cluster.hierarchy import linkage, leaves_list from scipy.spatial.distance import pdist import igraph as ig -import logomaker import base64 import datetime @@ -44,21 +42,37 @@ import matplotlib.ticker as ticker import numpy as np import os import pandas as pd -import pathlib import plotly.express as px import plotly.graph_objects as go import scipy.cluster.hierarchy as sch import seaborn as sns -import shutil import sys -import upsetplot import warnings -from pathlib import Path import re ## Reading sample metadata meta = pd.read_csv(sample_table, sep=',') +# 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 +# listed ones. When timepoint_order is empty, timepoints are ranked numerically +# if every value parses as a number, otherwise alphabetically. +if timepoint_order_col not in meta.columns: + unique_timepoints = meta[timepoint_col].dropna().unique().tolist() + if timepoint_order: + order_list = [t.strip() for t in timepoint_order.split(',')] + rank_map = {t: i for i, t in enumerate(order_list)} + unlisted = sorted(t for t in unique_timepoints if t not in rank_map) + for i, t in enumerate(unlisted): + rank_map[t] = len(order_list) + i + else: + try: + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=lambda x: float(x)))} + except (TypeError, ValueError): + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=str))} + meta[timepoint_order_col] = meta[timepoint_col].map(rank_map) + ## Reading concatenated cdr3 file concat_df = pd.read_csv(concat_csv, sep='\t') concat_df = concat_df.merge(meta[['sample', 'origin', subject_col, alias_col, timepoint_col, timepoint_order_col]], on='sample', how='left') @@ -142,17 +156,17 @@ from statsmodels.stats.multitest import multipletests clonotypes_df = concat_df.copy() clonotypes_df = clonotypes_df.dropna(subset=[timepoint_col, 'origin']) -clone_cols = ['CDR3b', 'TRBV', 'TRBJ'] +clone_cols = ['junction_aa', 'v_call', 'j_call'] merge_cols = clone_cols + [subject_col, 'origin'] clonotypes_df['timepoint_rank'] = clonotypes_df[timepoint_order_col] clonotypes_df = clonotypes_df.sort_values('timepoint_rank') # Grouping by subject, origin, and timepoint for accurate total counts -sample_total_counts = clonotypes_df.groupby([subject_col, 'origin', timepoint_col])['counts'].sum().reset_index() -sample_total_counts.rename(columns={'counts': 'total_counts'}, inplace=True) +sample_total_counts = clonotypes_df.groupby([subject_col, 'origin', timepoint_col])['duplicate_count'].sum().reset_index() +sample_total_counts.rename(columns={'duplicate_count': 'total_counts'}, inplace=True) clonotypes_df = pd.merge(clonotypes_df, sample_total_counts, on=[subject_col, 'origin', timepoint_col]) -clonotypes_df['frequency'] = clonotypes_df['counts'] / clonotypes_df['total_counts'] +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): @@ -179,8 +193,8 @@ for (subject, origin), subject_df in clonotypes_df.groupby([subject_col, 'origin continue freq_pivot = subject_df.pivot_table(index=clone_cols, columns=timepoint_col, values='frequency', fill_value=0) - counts_pivot = subject_df.pivot_table(index=clone_cols, columns=timepoint_col, values='counts', fill_value=0) - subject_total_counts = subject_df.groupby(timepoint_col)['counts'].sum() + counts_pivot = subject_df.pivot_table(index=clone_cols, columns=timepoint_col, values='duplicate_count', fill_value=0) + subject_total_counts = subject_df.groupby(timepoint_col)['duplicate_count'].sum() for t_pre, t_post in itertools.combinations(timepoints, 2): # 1. Vectorized DataFrame creation instead of iterrows @@ -304,8 +318,8 @@ for (subject, origin), subject_df in clonotypes_df.groupby([subject_col, 'origin continue freq_pivot = subject_df.pivot_table(index=clone_cols, columns=timepoint_col, values='frequency', fill_value=0) - counts_pivot = subject_df.pivot_table(index=clone_cols, columns=timepoint_col, values='counts', fill_value=0) - subject_total_counts = subject_df.groupby(timepoint_col)['counts'].sum() + counts_pivot = subject_df.pivot_table(index=clone_cols, columns=timepoint_col, values='duplicate_count', fill_value=0) + subject_total_counts = subject_df.groupby(timepoint_col)['duplicate_count'].sum() for t_pre, t_post in itertools.combinations(timepoints, 2): # 1. Vectorize @@ -500,7 +514,7 @@ else: sorted_tps += [t for t in valid_tps if t not in sorted_tps] # Set Index to CDR3b - matrix_df = orig_df.set_index('CDR3b')[sorted_tps] + matrix_df = orig_df.set_index('junction_aa')[sorted_tps] # Drop completely empty rows matrix_df = matrix_df.loc[~(matrix_df == 0).all(axis=1)] @@ -708,7 +722,7 @@ def get_top_ids(df, direction): subset = group[group['fold_change'] < 1] # Sort by P-value (lowest first) -> Take top 15 Unique CDR3s - top_clones = subset.sort_values('p_value', ascending=True)['CDR3b'].drop_duplicates().head(15).tolist() + top_clones = subset.sort_values('p_value', ascending=True)['junction_aa'].drop_duplicates().head(15).tolist() id_map[name] = set(top_clones) return id_map @@ -718,20 +732,17 @@ contracted_map = get_top_ids(stats_df, 'down') # --- 2. Prepare Master Frequency Data --- # Aggregate by CDR3 including 'origin' so we can split traces later -agg_cols = [subject_col, 'origin', 'CDR3b', timepoint_col] if 'origin' in clonotypes_df.columns else [subject_col, 'CDR3b', timepoint_col] +agg_cols = [subject_col, 'origin', 'junction_aa', timepoint_col] if 'origin' in clonotypes_df.columns else [subject_col, 'junction_aa', timepoint_col] cdr3_df = clonotypes_df.groupby(agg_cols)['frequency'].sum().reset_index() # Log Transform (with pseudocount) min_freq = cdr3_df[cdr3_df['frequency'] > 0]['frequency'].min() cdr3_df['log_freq'] = np.log10(cdr3_df['frequency'].replace(0, min_freq / 2)) -# Map Timepoints -if 'timepoint_order' in locals(): - cdr3_df['timepoint_rank'] = cdr3_df[timepoint_col].map(timepoint_order) -else: - ordered_timepoints = sorted(cdr3_df[timepoint_col].unique()) - timepoint_order_fallback = {t: i for i, t in enumerate(ordered_timepoints)} - cdr3_df['timepoint_rank'] = cdr3_df[timepoint_col].map(timepoint_order_fallback) +# Map Timepoints - reuse the rank already computed onto clonotypes_df via meta, +# rather than re-deriving a fresh (and previously always-alphabetical) mapping. +time_order_map = clonotypes_df.drop_duplicates(timepoint_col).set_index(timepoint_col)[timepoint_order_col].to_dict() +cdr3_df['timepoint_rank'] = cdr3_df[timepoint_col].map(time_order_map) # --- 3. The Plot Generator Function --- @@ -748,7 +759,7 @@ def generate_plot_quarto_tabs(id_map, title_prefix, line_color): def get_status(row): subj = row[subject_col] orig = row['origin'] if 'origin' in row else None - seq = row['CDR3b'] + seq = row['junction_aa'] # Check map using the correct key format key = (subj, orig) if orig is not None else subj @@ -801,7 +812,7 @@ def generate_plot_quarto_tabs(id_map, title_prefix, line_color): # Generate temporary figure to extract traces temp_fig = px.line( orig_df, x=timepoint_col, y="log_freq", color="status", - line_group="CDR3b", hover_name="CDR3b", + line_group="junction_aa", hover_name="junction_aa", color_discrete_map=color_map, category_orders={timepoint_col: orig_tps} ) diff --git a/notebooks/template_patient_clustering_off.qmd b/notebooks/template_patient_clustering_off.qmd new file mode 100644 index 0000000..505afce --- /dev/null +++ b/notebooks/template_patient_clustering_off.qmd @@ -0,0 +1,5 @@ +## Patient-level TCR clustering (GIANA/GLIPH2) + +::: {.callout-note} +This section requires patient-level clonotype clustering (GIANA and GLIPH2), which was not run for this project. Include `patient` in `--workflow_level` to generate it. +::: diff --git a/notebooks/template_patient_clustering_on.qmd b/notebooks/template_patient_clustering_on.qmd new file mode 100644 index 0000000..f56d020 --- /dev/null +++ b/notebooks/template_patient_clustering_on.qmd @@ -0,0 +1,3 @@ +{{< include ./template_giana.qmd >}} + +{{< include ./template_gliph.qmd >}} diff --git a/notebooks/template_pheno_bulk.qmd b/notebooks/template_pheno_bulk.qmd index 4970300..624b2cd 100644 --- a/notebooks/template_pheno_bulk.qmd +++ b/notebooks/template_pheno_bulk.qmd @@ -60,6 +60,27 @@ warnings.filterwarnings( meta = pd.read_csv(sample_table, sep=',') meta.drop(columns=['file'], inplace=True) + +# 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 +# listed ones. When timepoint_order is empty, timepoints are ranked numerically +# if every value parses as a number, otherwise alphabetically. +if timepoint_order_col not in meta.columns: + unique_timepoints = meta[timepoint_col].dropna().unique().tolist() + if timepoint_order: + order_list = [t.strip() for t in timepoint_order.split(',')] + rank_map = {t: i for i, t in enumerate(order_list)} + unlisted = sorted(t for t in unique_timepoints if t not in rank_map) + for i, t in enumerate(unlisted): + rank_map[t] = len(order_list) + i + else: + try: + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=lambda x: float(x)))} + except (TypeError, ValueError): + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=str))} + meta[timepoint_order_col] = meta[timepoint_col].map(rank_map) + meta_cols = meta.columns.tolist() df = pd.read_csv(sample_stats_csv, sep=',') @@ -148,9 +169,9 @@ def create_phenotype_tabs(meta_df): # --- Merge Metadata --- meta_row = meta_df[meta_df['sample'] == sample_id] if not meta_row.empty: - composition['subject_id'] = meta_row.iloc[0]['subject_id'] - composition['alias'] = meta_row.iloc[0]['alias'] - composition['sort_order'] = meta_row.iloc[0]['timepoint_order'] if 'timepoint_order' in meta_df.columns else 0 + composition['subject_id'] = meta_row.iloc[0][subject_col] + composition['alias'] = meta_row.iloc[0][alias_col] + composition['sort_order'] = meta_row.iloc[0][timepoint_order_col] if timepoint_order_col in meta_df.columns else 0 else: composition['subject_id'] = np.nan composition['alias'] = sample_id @@ -321,10 +342,10 @@ def create_weighted_phenotype_plot(meta_df): # Skip samples not in metadata, or define defaults continue - subject_id = meta_row.iloc[0]['subject_id'] - alias = meta_row.iloc[0]['alias'] + subject_id = meta_row.iloc[0][subject_col] + alias = meta_row.iloc[0][alias_col] # Default to 0 if order column is missing - tp_order = meta_row.iloc[0]['timepoint_order'] if 'timepoint_order' in meta_df.columns else 0 + tp_order = meta_row.iloc[0][timepoint_order_col] if timepoint_order_col in meta_df.columns else 0 # --- Load Data --- pheno_df = pd.read_csv(pheno_file_path, sep='\t') @@ -339,10 +360,12 @@ def create_weighted_phenotype_plot(meta_df): continue # --- Merge & Weight --- - # Match phenotype (junction_aa) with counts (CDR3b) - merged_df = pd.merge(pheno_df, sample_counts_df[['CDR3b', 'counts']], - left_on='junction_aa', right_on='CDR3b', how='inner') - + # TCRPHENO output only carries 'sequence_id' (no CDR3/junction_aa column), + # which matches the same "{sample}|sequence{N}" IDs in concat_csv. + merged_df = pd.merge(pheno_df, sample_counts_df[['sequence_id', 'duplicate_count']], + on='sequence_id', how='inner') + merged_df.rename(columns={'duplicate_count': 'counts'}, inplace=True) + merged_df = merged_df.dropna(subset=phenotype_cols + ['counts']) if merged_df.empty: diff --git a/notebooks/template_pheno_sc.qmd b/notebooks/template_pheno_sc.qmd index eaf8f6c..5d30dc1 100644 --- a/notebooks/template_pheno_sc.qmd +++ b/notebooks/template_pheno_sc.qmd @@ -5,11 +5,10 @@ project_dir=f"{project_dir}" -# Define files +# Define files concat_csv = f"{project_dir}/annotate/concatenated_cdr3_sorted.tsv" -concat_csv_pheno=f"{project_dir}/compare_phenotype/concatenated_cdr3.tsv" -sample_table_pheno=f"{project_dir}/pipeline_info/samplesheet_phenotype.csv" +concat_csv_pheno = f"{project_dir}/annotate/concatenated_cdr3_sorted.tsv" # Define dirs tcrpheno_dir = f"{project_dir}/tcrpheno/" @@ -46,11 +45,31 @@ from scipy import stats from itertools import combinations from scipy.stats import mannwhitneyu from plotly.subplots import make_subplots -from pathlib import Path import re meta = pd.read_csv(sample_table, sep=',') meta.drop(columns=['file'], inplace=True) + +# 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 +# listed ones. When timepoint_order is empty, timepoints are ranked numerically +# if every value parses as a number, otherwise alphabetically. +if timepoint_order_col not in meta.columns: + unique_timepoints = meta[timepoint_col].dropna().unique().tolist() + if timepoint_order: + order_list = [t.strip() for t in timepoint_order.split(',')] + rank_map = {t: i for i, t in enumerate(order_list)} + unlisted = sorted(t for t in unique_timepoints if t not in rank_map) + for i, t in enumerate(unlisted): + rank_map[t] = len(order_list) + i + else: + try: + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=lambda x: float(x)))} + except (TypeError, ValueError): + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=str))} + meta[timepoint_order_col] = meta[timepoint_col].map(rank_map) + meta_cols = meta.columns.tolist() concat_df = pd.read_csv(concat_csv, sep='\t') @@ -58,14 +77,8 @@ concat_df = concat_df.merge(meta[['sample', subject_col, alias_col, timepoint_co # Phenotype files -# Samplesheet -metadata_pheno_df = pd.read_csv(sample_table_pheno, sep=',', header=0, index_col="file") -metadata_pheno_df.index = metadata_pheno_df.index.map(lambda p: Path(p).name) -metadata_pheno_df.index = metadata_pheno_df.index.str.replace('_pseudobulk', '', regex=False) -metadata_pheno_df.index = metadata_pheno_df.index.str.replace('_phenotype.tsv', '', regex=False) - # Importing sample metadata (pheno-ps) -clonotypes_pheno_df = pd.read_csv(concat_csv_pheno, sep='\t', header=0, index_col=0).reset_index() +clonotypes_pheno_df = pd.read_csv(concat_csv_pheno, sep='\t', header=0) clonotypes_pheno_df['sample_phenotype'] = clonotypes_pheno_df['sample'] # Add 'sample_phenotype' and 'phenotype' samples = sorted(meta['sample'].astype(str).unique(), key=len, reverse=True) @@ -437,13 +450,13 @@ import sys warnings.filterwarnings("ignore") def create_patient_upset_figure(df, patient_id): - df_clean = df.dropna(subset=['phenotype', 'CDR3b']).copy() + df_clean = df.dropna(subset=['phenotype', 'junction_aa']).copy() phenotypes = sorted(df_clean['phenotype'].unique()) if len(phenotypes) < 2: return None clones_by_pheno = { - p: list(df_clean[df_clean['phenotype'] == p]['CDR3b'].unique()) + p: list(df_clean[df_clean['phenotype'] == p]['junction_aa'].unique()) for p in phenotypes } upset_data = from_contents(clones_by_pheno) diff --git a/notebooks/template_qc.qmd b/notebooks/template_qc.qmd index 5c0d2b3..471f256 100644 --- a/notebooks/template_qc.qmd +++ b/notebooks/template_qc.qmd @@ -52,6 +52,10 @@ sample_table='' timepoint_col = 'timepoint' timepoint_order_col = 'timepoint_order' +# Ordered comma-separated list of timepoint values, e.g. "Base,Week4,EOT" - +# rank = position in the list. Empty string means: rank numerically if every +# timepoint value parses as a number, otherwise alphabetically. +timepoint_order = '' alias_col = 'alias' subject_col = 'patient' @@ -112,6 +116,27 @@ import plotly.figure_factory as ff import warnings meta = pd.read_csv(sample_table, sep=',') + +# 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 +# listed ones. When timepoint_order is empty, timepoints are ranked numerically +# if every value parses as a number, otherwise alphabetically. +if timepoint_order_col not in meta.columns: + unique_timepoints = meta[timepoint_col].dropna().unique().tolist() + if timepoint_order: + order_list = [t.strip() for t in timepoint_order.split(',')] + rank_map = {t: i for i, t in enumerate(order_list)} + unlisted = sorted(t for t in unique_timepoints if t not in rank_map) + for i, t in enumerate(unlisted): + rank_map[t] = len(order_list) + i + else: + try: + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=lambda x: float(x)))} + except (TypeError, ValueError): + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=str))} + meta[timepoint_order_col] = meta[timepoint_col].map(rank_map) + 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 aadde37..aea664e 100644 --- a/notebooks/template_sample.qmd +++ b/notebooks/template_sample.qmd @@ -27,27 +27,14 @@ convergence_dir = f"{project_dir}/convergence/" ```{python} #| code-fold: true -import datetime import glob -import itertools -import math -import os -import sys import h5py -import igraph as ig -import matplotlib.pyplot as plt -import matplotlib.ticker as ticker +import os import numpy as np import pandas as pd import plotly.express as px import plotly.graph_objects as go -import scipy.cluster.hierarchy as sch -import seaborn as sns -from IPython.display import Image -from matplotlib.colors import LinearSegmentedColormap from scipy.sparse import csr_matrix -from scipy.stats import gaussian_kde -import plotly.io as pio from scipy import stats from itertools import combinations from scipy.stats import mannwhitneyu @@ -62,6 +49,27 @@ warnings.filterwarnings( meta = pd.read_csv(sample_table, sep=',') meta.drop(columns=['file'], inplace=True) + +# 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 +# listed ones. When timepoint_order is empty, timepoints are ranked numerically +# if every value parses as a number, otherwise alphabetically. +if timepoint_order_col not in meta.columns: + unique_timepoints = meta[timepoint_col].dropna().unique().tolist() + if timepoint_order: + order_list = [t.strip() for t in timepoint_order.split(',')] + rank_map = {t: i for i, t in enumerate(order_list)} + unlisted = sorted(t for t in unique_timepoints if t not in rank_map) + for i, t in enumerate(unlisted): + rank_map[t] = len(order_list) + i + else: + try: + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=lambda x: float(x)))} + except (TypeError, ValueError): + rank_map = {t: r for r, t in enumerate(sorted(unique_timepoints, key=str))} + meta[timepoint_order_col] = meta[timepoint_col].map(rank_map) + meta_cols = meta.columns.tolist() df = pd.read_csv(sample_stats_csv, sep=',') @@ -104,7 +112,6 @@ $$f_i = \frac{\text{Read count for clone } i}{\text{Total reads for all producti #| echo: false import pandas as pd -import plotly.graph_objects as go def create_expansion_stacked_barplot_per_individual(df, subject_col, origin_col, timepoint_order_col): """ @@ -112,11 +119,11 @@ def create_expansion_stacked_barplot_per_individual(df, subject_col, origin_col, Generates one Quarto tab per individual with dropdowns for origin. Includes a horizontal threshold line for sequencing depth (3000 counts). """ - # --- Ensure 'counts' column is numeric --- + # --- Ensure 'duplicate_count' column is numeric --- try: - df['counts'] = pd.to_numeric(df['counts']) + df['duplicate_count'] = pd.to_numeric(df['duplicate_count']) except ValueError: - print("Error: 'counts' column could not be converted to a numeric type.") + print("Error: 'duplicate_count' column could not be converted to a numeric type.") return subjects = [s for s in df[subject_col].unique() if pd.notna(s)] @@ -138,11 +145,11 @@ def create_expansion_stacked_barplot_per_individual(df, subject_col, origin_col, for sample_name in unique_samples: sample_df = origin_df[origin_df['sample'] == sample_name].copy() - total_reads = sample_df['counts'].sum() + total_reads = sample_df['duplicate_count'].sum() if total_reads == 0: continue - sample_df['frequency'] = (sample_df['counts'] / total_reads) * 100 + sample_df['frequency'] = (sample_df['duplicate_count'] / total_reads) * 100 highly_expanded = sample_df[sample_df['frequency'] > 1] expanded = sample_df[(sample_df['frequency'] >= 0.1) & (sample_df['frequency'] <= 1)] @@ -360,7 +367,6 @@ This table is useful because it reveals the identities of the dominant clones dr #| output: asis #| echo: false -import plotly.graph_objects as go def create_top_clones_tables_by_patient(df, n_clones=15): """ @@ -393,21 +399,21 @@ def create_top_clones_tables_by_patient(df, n_clones=15): # Build a table trace for each timepoint for i, tp in enumerate(unique_timepoints): tp_df = pat_df[pat_df[timepoint_col] == tp].copy() - tp_df = tp_df[tp_df['counts'] > 0] + tp_df = tp_df[tp_df['duplicate_count'] > 0] if tp_df.empty: continue # Grab the top clones - top_n_clones = tp_df.nlargest(n_clones, 'counts') + top_n_clones = tp_df.nlargest(n_clones, 'duplicate_count') # Safely extract columns (avoids KeyErrors if TRBV/TRBJ are missing in some runs) - cols_to_show = ['CDR3b', 'TRBV', 'TRBJ', 'counts'] + cols_to_show = ['junction_aa', 'v_call', 'j_call', 'duplicate_count'] available_cols = [c for c in cols_to_show if c in top_n_clones.columns] display_df = top_n_clones[available_cols].copy() # Clean up headers for display - display_df.columns = [c.capitalize() if c == 'counts' else c for c in display_df.columns] + display_df.columns = [c.capitalize() if c == 'duplicate_count' else c for c in display_df.columns] fig.add_trace( go.Table( @@ -489,8 +495,6 @@ create_top_clones_tables_by_patient(concat_df, n_clones=15) import pandas as pd import numpy as np import plotly.graph_objects as go -from plotly.subplots import make_subplots -from scipy.stats import mannwhitneyu def create_violin_with_heatmap_inset(df, meta_df, origin_col='origin'): """ @@ -503,16 +507,16 @@ def create_violin_with_heatmap_inset(df, meta_df, origin_col='origin'): # --- 1. Data Prep --- try: - df['counts'] = pd.to_numeric(df['counts']) + df['duplicate_count'] = pd.to_numeric(df['duplicate_count']) except ValueError: - print("Error: 'counts' column could not be converted to a numeric type.") + print("Error: 'duplicate_count' column could not be converted to a numeric type.") return # Filter for valid counts - plot_df = df[df['counts'] > 0].copy() + plot_df = df[df['duplicate_count'] > 0].copy() if plot_df.empty: return - plot_df['log10_counts'] = np.log10(plot_df['counts']) + plot_df['log10_counts'] = np.log10(plot_df['duplicate_count']) # --- 2. Merge with Metadata --- # Assuming df already has the metadata merged based on your previous code structure @@ -958,9 +962,11 @@ conv_data = [] for f in files: filename = os.path.basename(f) sample_id = filename.replace('_tcr_convergence.tsv', '') - if sample_id.endswith('_pseudobulk'): + if sample_id.endswith('_pseudobulk'): sample_id = sample_id[:-11] - if sample_id.endswith('_airr'): + if sample_id.endswith('_airr'): + sample_id = sample_id[:-5] + if sample_id.endswith('_cdr3'): sample_id = sample_id[:-5] try: @@ -1098,7 +1104,6 @@ print(":::\n") import plotly.express as px import plotly.graph_objects as go -from scipy.stats import mannwhitneyu from itertools import combinations import pandas as pd @@ -1574,8 +1579,6 @@ import glob import pandas as pd import numpy as np import plotly.graph_objects as go -from plotly.subplots import make_subplots -from scipy.stats import mannwhitneyu def create_pgen_tabs(meta_df, origin_col='origin'): """ @@ -2331,7 +2334,7 @@ def plot_vdjdb_species_specificity(VDJdb_dir, meta_df, subject_col='subject_id') print(":::\n") # Run the function -plot_vdjdb_species_specificity(VDJdb_dir, meta) +plot_vdjdb_species_specificity(VDJdb_dir, meta, subject_col=subject_col) ``` **Figure 14: Top 15 recognized epitopes identified in bulk-TCR data.** The number of unique TCR clonotypes in the sample that match a known epitope in the VDJdb database is shown on the x-axis. The plot highlights the most abundant antigen specificities inferred from the T-cell repertoire. @@ -2515,7 +2518,7 @@ def plot_vdjdb_sunburst_hierarchy(VDJdb_dir, meta_df, subject_col='subject_id'): print(":::\n") # Run the function -plot_vdjdb_sunburst_hierarchy(VDJdb_dir, meta) +plot_vdjdb_sunburst_hierarchy(VDJdb_dir, meta, subject_col=subject_col) ``` **Figure 15: TCR Repertoire Mapping to specific gene epitopes:** Sunburst chart visualizing the relationships between T-cell receptor (TCR) sequences and their known antigen specificities from the VDJdb curated database. The central ring represents the species, branching out to their specific genes, and finally to the individual TCR sequences from your dataset that match those specificities. The size of each segment corresponds to the number of TCRs associated with that particular specificity. @@ -2691,7 +2694,7 @@ def plot_vdjdb_sunburst_reversed(VDJdb_dir, meta_df, subject_col='subject_id'): print(":::\n") # Run the function -plot_vdjdb_sunburst_reversed(VDJdb_dir, meta) +plot_vdjdb_sunburst_reversed(VDJdb_dir, meta, subject_col=subject_col) ``` **Figure 16: Gene Epitopes Mapping to TCR sequences:** Sunburst chart visualizing the relationships antigens and T-cell receptor (TCR) sequences. The central ring represents TCRs sequences, branching out to their specific genes, and finally to the species the antigen belongs to. Only TCRs with a Freq>2 are shown, for visualization purposes. diff --git a/notebooks/template_sharing.qmd b/notebooks/template_sharing.qmd index 0bd0e6b..342f79e 100644 --- a/notebooks/template_sharing.qmd +++ b/notebooks/template_sharing.qmd @@ -48,8 +48,6 @@ import base64 import json from IPython.display import HTML, display import warnings -import matplotlib.pyplot as plt -from upsetplot import from_contents, plot as upset_plot # - Loading data @@ -81,16 +79,16 @@ This plot is generated by first calculating the frequency of each unique TCR clo #| fig-cap: "**TCR Sharing by Maximum Clonal Expansion.** Number of samples where a TCR has an exact match on the aminoacid level. Color represents clonal expansion category using the highest frequency across all samples." # --- 1. Load and Process Actual Data --- -# We assume 'concat_df' has columns ['CDR3b', 'counts', 'sample'] +# We assume 'concat_df' has columns ['junction_aa', 'duplicate_count', 'sample'] # We assume 'meta' has columns ['sample', subject_col] raw_df = concat_df.copy() # Rename columns to standard internal names -raw_df.rename(columns={'CDR3b': 'cdr3_sequence', 'sample': 'sample_id'}, inplace=True) +raw_df.rename(columns={'junction_aa': 'cdr3_sequence', 'sample': 'sample_id'}, inplace=True) # Calculate frequency per SAMPLE first (Expansion is a property of a specific physical sample) -sample_total_counts = raw_df.groupby('sample_id')['counts'].transform('sum') -raw_df['frequency'] = raw_df['counts'] / sample_total_counts +sample_total_counts = raw_df.groupby('sample_id')['duplicate_count'].transform('sum') +raw_df['frequency'] = raw_df['duplicate_count'] / sample_total_counts # --- CRITICAL STEP: Merge with Metadata to get Patient IDs --- # We merge on 'sample_id' (which matches 'sample' in meta) @@ -196,14 +194,14 @@ import plotly.express as px merged_df = pd.merge(concat_df, meta[['sample', subject_col, timepoint_col]], on='sample', how='left') # Group by TCR (CDR3b) and aggregate the exact data we need for plotting and hovering -agg_df = merged_df.groupby('CDR3b').agg( +agg_df = merged_df.groupby('junction_aa').agg( total_patients=(subject_col, 'nunique'), individuals=(subject_col, lambda x: ', '.join(sorted(set(x.dropna().astype(str))))), timepoints=(timepoint_col, lambda x: ', '.join(sorted(set(x.dropna().astype(str))))) ).reset_index() # Merge this with your existing generation probability dataframe -plot_df = pd.merge(prob_generation_df[['CDR3b', 'pgen']], agg_df, on='CDR3b', how='inner') +plot_df = pd.merge(prob_generation_df[['junction_aa', 'pgen']], agg_df, on='junction_aa', how='inner') # Calculate the log10 of the generation probability plot_df['log10_pgen'] = np.log10(plot_df['pgen']) @@ -215,7 +213,7 @@ fig = px.scatter( plot_df, x='log10_pgen', y='total_patients', - hover_name='CDR3b', # Puts the sequence at the top of the tooltip in bold + hover_name='junction_aa', # Puts the sequence at the top of the tooltip in bold hover_data={ 'log10_pgen': ':.2f', # Format to 2 decimal places 'total_patients': False, # Hide this because it's already obvious from the Y-axis diff --git a/params_singlecell.yml b/params_singlecell.yml new file mode 100644 index 0000000..1f0e581 --- /dev/null +++ b/params_singlecell.yml @@ -0,0 +1,41 @@ +# Example params file for single-cell TCR analysis. +# nextflow run . -params-file params_singlecell.yml +# +# Route is auto-detected: providing `input_annotated_object` selects the full-SC +# route (with CoNGA / consensus / repertoire / master summary); omitting it runs +# the VDJ-only route (full bulk analysis + QC/repertoire report, no GEX steps). + +mode: singlecell + +# ── Required inputs ─────────────────────────────────────────────────────────── +input_vdj_contigs: "cellranger/*/outs" # Cell Ranger VDJ output glob +sample_sheet: "sc_samplesheet.csv" # columns: sample, path (+ patient_id, etc.) + +# ── Optional: annotated GEX Seurat object → full single-cell route ──────────── +# input_annotated_object: "annotated_tcells.rds" + +outdir: "out_singlecell" +project_name: "sc_tcr_run" + +# ── Pseudobulk pooling ──────────────────────────────────────────────────────── +pseudobulk_by_phenotype: false # true → {sample}__{phenotype} units +patient_col: "patient_id" # pool units per patient for clustering + +# ── Pseudobulk QC gate ──────────────────────────────────────────────────────── +pseudobulk_qc_min_clones: 25 +pseudobulk_qc_min_cells: 50 +pseudobulk_qc_mode: "drop" # drop | hard_stop + +# ── Analysis levels for the shared bulk engine ─────────────────────────────── +workflow_level: "sample,patient,compare" +use_gliph2: true + +# ── Single-cell report toggles (full-SC route) ─────────────────────────────── +run_conga: true +run_consensus: true +run_repertoire: true +run_master_summary: true + +# ── Containers ──────────────────────────────────────────────────────────────── +# container: "ghcr.io/karchinlab/tcrtoolkit:main" # bulk engine (default) +# sc_container: "syedsazaidi/scratch-tcr:latest" # cell-level SC processes (default) diff --git a/subworkflows/bridges/cluster_to_sc.nf b/subworkflows/bridges/cluster_to_sc.nf new file mode 100644 index 0000000..8ccc856 --- /dev/null +++ b/subworkflows/bridges/cluster_to_sc.nf @@ -0,0 +1,61 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl = 2 + +include { CLUSTER_TO_SC } from '../../modules/bridges/cluster_to_sc.nf' + +/* + * CLUSTER_TO_SC_SW (Bridge 2+) + * + * Collects per-patient GIANA / GLIPH2 and per-sample TCRdist3 outputs, then + * maps all cluster assignments back onto single cells via the Seurat object. + * + * Takes: + * seurat_rds - seurat_tcells_with_tcr from TCELL_INTEGRATION_SW + * export_cells - export_cells.tsv from TCELL_INTEGRATION_SW + * giana_clusters - channel of *_giana.txt files from PATIENT.out.giana_clusters + * gliph2_details - channel of cluster_member_details files from + * PATIENT.out.gliph2_cluster_details (may be empty) + * tcrdist_clone_dfs - channel of *_clone_df.csv from TCRDIST3_MATRIX.out.clone_df + * tcrdist_matrices - channel of distance matrix files from + * TCRDIST3_MATRIX.out.tcrdist_output (file only, meta stripped) + * + * Emits: + * enriched_seurat - Seurat RDS with giana/gliph2/tcrdist cluster columns added + * giana_export - per-cell TSV with giana_cluster column + * gliph2_export - per-cell TSV with gliph2_cluster column + * tcrdist_export - per-cell TSV with tcrdist_cluster column + */ +workflow CLUSTER_TO_SC_SW { + take: + seurat_rds + export_cells + giana_clusters + gliph2_details + tcrdist_clone_dfs + tcrdist_matrices + + main: + def nofile = file("${projectDir}/assets/NO_FILE") + + // Collect all per-patient / per-sample files into single inputs + giana_collected = giana_clusters.collect().ifEmpty([nofile]) + gliph2_collected = gliph2_details.collect().ifEmpty([nofile]) + clone_dfs_collected = tcrdist_clone_dfs.collect().ifEmpty([nofile]) + matrices_collected = tcrdist_matrices.collect().ifEmpty([nofile]) + + CLUSTER_TO_SC( + seurat_rds, + export_cells, + giana_collected, + gliph2_collected, + clone_dfs_collected, + matrices_collected, + params.tcrdist_radius ?: 24 + ) + + emit: + enriched_seurat = CLUSTER_TO_SC.out.enriched_seurat + giana_export = CLUSTER_TO_SC.out.giana_export + gliph2_export = CLUSTER_TO_SC.out.gliph2_export + tcrdist_export = CLUSTER_TO_SC.out.tcrdist_export +} diff --git a/subworkflows/bridges/sc_to_bulk.nf b/subworkflows/bridges/sc_to_bulk.nf new file mode 100644 index 0000000..55aa449 --- /dev/null +++ b/subworkflows/bridges/sc_to_bulk.nf @@ -0,0 +1,51 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl = 2 + +include { SC_TO_BULK } from '../../modules/bridges/sc_to_bulk.nf' + +/* + * SC_TO_BULK_SW + * + * Takes the per-cell export_cells.tsv from SCRATCH T-cell integration and + * produces a TCRtoolkit-compatible sample_map channel (one [meta, file] tuple + * per sample) plus a synthetic samplesheet file for report modules. + * + * The metadata columns carried over (patient, condition, timepoint, batch) + * are read from the first cell per sample in export_cells.tsv. + */ +workflow SC_TO_BULK_SW { + take: + export_cells // path: per-cell TSV from TCELL_INTEGRATION + + main: + def meta_cols = [ + params.patient_col, + params.condition_col, + params.timepoint_col, + params.batch_col + ].findAll { it }.join(',') + + // TCELL_INTEGRATION always exports a standardized 'sample' column; + // params.sample_col is the Seurat metadata name, not the export column name. + SC_TO_BULK( + export_cells, + 'sample', + meta_cols + ) + + samplesheet_utf8 = SC_TO_BULK.out.samplesheet + + // Parse synthetic samplesheet → Nextflow sample_map channel + samplesheet_utf8 + .splitCsv(header: true, sep: ',') + .map { row -> + def meta = row.findAll { k, _v -> k != 'file' } + def file_obj = file(row.file) + return [meta, file_obj] + } + .set { sample_map } + + emit: + sample_map + samplesheet_utf8 +} diff --git a/subworkflows/bridges/sc_to_cdr3.nf b/subworkflows/bridges/sc_to_cdr3.nf new file mode 100644 index 0000000..e3f82bb --- /dev/null +++ b/subworkflows/bridges/sc_to_cdr3.nf @@ -0,0 +1,39 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl = 2 + +include { SC_TO_CDR3 } from '../../modules/bridges/sc_to_cdr3.nf' + +/* + * SC_TO_CDR3_SW + * + * Wraps SC_TO_CDR3 and emits: + * sample_map — channel of [meta, file] tuples (one per sample) + * concat_cdr3 — single path to concatenated CDR3 file + * + * Used in full-SC mode as a replacement for SC_TO_BULK_SW + ANNOTATE_PROCESS. + */ +workflow SC_TO_CDR3_SW { + take: + export_cells // path: tcr_export_cells_with_embedding.tsv from TCELL_INTEGRATION + + main: + SC_TO_CDR3( export_cells ) + + // Build sample_map from unit_map.csv so meta carries patient (+ phenotype). + // patient_id lets the shared PATIENT step pool per-patient for clustering. + SC_TO_CDR3.out.unit_map + .splitCsv(header: true) + .map { row -> + def meta = [ + sample : row.sample, + patient_id: (row.patient ?: row.sample), + phenotype : (row.phenotype ?: '') + ] + [ meta, file(row.file) ] + } + .set { sample_map } + + emit: + sample_map + concat_cdr3 = SC_TO_CDR3.out.concat_cdr3 +} diff --git a/subworkflows/bridges/vdj_to_bulk.nf b/subworkflows/bridges/vdj_to_bulk.nf new file mode 100644 index 0000000..8519622 --- /dev/null +++ b/subworkflows/bridges/vdj_to_bulk.nf @@ -0,0 +1,43 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl = 2 + +include { VDJ_TO_BULK } from '../../modules/bridges/vdj_to_bulk.nf' + +/* + * VDJ_TO_BULK_SW + * + * Takes VDJ_QC's contigs_after_qc.tsv and produces a TCRtoolkit-compatible + * sample_map channel (one [meta, file] tuple per sample) plus a synthetic + * samplesheet file. + * + * Used in VDJ-only mode when no GEX Seurat object is provided and + * TCELL_INTEGRATION is skipped. + */ +workflow VDJ_TO_BULK_SW { + take: + contigs_after_qc // path: contigs_after_qc.tsv from VDJ_QC + + main: + def ss = params.sample_sheet ? file(params.sample_sheet) : file("${projectDir}/assets/NO_FILE") + VDJ_TO_BULK( + contigs_after_qc, + params.vdj_meta_sample_col ?: 'sample', + ss + ) + + samplesheet_utf8 = VDJ_TO_BULK.out.samplesheet + + // Parse synthetic samplesheet → Nextflow sample_map channel + samplesheet_utf8 + .splitCsv(header: true, sep: ',') + .map { row -> + def meta = row.findAll { k, _v -> k != 'file' } + def file_obj = file(row.file) + return [meta, file_obj] + } + .set { sample_map } + + emit: + sample_map + samplesheet_utf8 +} diff --git a/subworkflows/local/annotate.nf b/subworkflows/local/annotate.nf index d69d5ef..41aa9f7 100644 --- a/subworkflows/local/annotate.nf +++ b/subworkflows/local/annotate.nf @@ -34,8 +34,8 @@ workflow ANNOTATE { storeDir: "${params.outdir}/sample" ) - def concat_cdr3 = processed_samples - .map { _meta, file -> file } + concat_cdr3 = processed_samples + .map { _meta, f -> f } .collectFile(name: 'concat_cdr3.tsv', keepHeader: true, skip: 1) ANNOTATE_SORT_CDR3( concat_cdr3 ) @@ -75,4 +75,60 @@ workflow ANNOTATE { .collectEntries{ stats -> [(stats[0]): stats[1]] } } .first() +} + +/* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ANNOTATE_FROM_CONCAT (single-cell entrypoint — additive, not used by bulk mode) + + Variant of ANNOTATE that SKIPS ANNOTATE_PROCESS. Used by the single-cell modality + where the pseudobulk bridge (SC_TO_CDR3 / VDJ_TO_BULK) has already produced data in + the canonical clonotype schema and a pre-concatenated CDR3 table. Runs the identical + sort -> dedup -> OLGA chain and emits the same channels as ANNOTATE (minus + per_sample_stats, which ANNOTATE_PROCESS would have produced). +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +workflow ANNOTATE_FROM_CONCAT { + take: + sample_map // channel: [meta, file] already in canonical CDR3 schema + concat_cdr3 // path: pre-concatenated CDR3 table + + main: + ANNOTATE_SORT_CDR3( concat_cdr3 ) + concat_cdr3_sorted = ANNOTATE_SORT_CDR3.out.concat_cdr3_sorted + + ANNOTATE_DEDUPLICATE_CDR3_TRBV( concat_cdr3_sorted ) + + ANNOTATE_DEDUPLICATE_CDR3( + ANNOTATE_DEDUPLICATE_CDR3_TRBV.out.unique_cdr3_trbv + ) + + ANNOTATE_OLGA_CALCULATE( + ANNOTATE_DEDUPLICATE_CDR3.out.unique_cdr3 + .splitText(by: params.olga_chunk_length, file: true) + ) + + ANNOTATE_OLGA_CONCATENATE ( + ANNOTATE_OLGA_CALCULATE.out.pgen_chunk + .collectFile( + name: 'olga_pgen_body.tsv', + sort: { f -> + def m = (f.name =~ /\.(\d+)\.txt$/) + m ? m[0][1].toInteger() : 0 + } + ) + ) + + emit: + processed_samples = sample_map + concat_cdr3_sorted + cdr3_pgen = ANNOTATE_OLGA_CONCATENATE.out.cdr3_pgen + olga_stats = ANNOTATE_OLGA_CONCATENATE.out.cdr3_pgen_stats + .map { f -> + def _m = f.readLines() + .collect{ stats -> stats.split('\t') } + .collectEntries{ stats -> [(stats[0]): stats[1]] } + } + .first() } \ No newline at end of file diff --git a/subworkflows/local/compare.nf b/subworkflows/local/compare.nf index 9e2453e..0265bf6 100644 --- a/subworkflows/local/compare.nf +++ b/subworkflows/local/compare.nf @@ -33,4 +33,7 @@ workflow COMPARE { TCRSHARING_SCATTERPLOT( TCRSHARING_CALC.out.shared_cdr3 ) + + emit: + shared_cdr3 = TCRSHARING_CALC.out.shared_cdr3 } \ No newline at end of file diff --git a/subworkflows/local/patient.nf b/subworkflows/local/patient.nf index aa93022..84095aa 100644 --- a/subworkflows/local/patient.nf +++ b/subworkflows/local/patient.nf @@ -20,8 +20,12 @@ workflow PATIENT { processed_samples main: + // Grouping key is configurable for the single-cell modality (e.g. 'patient_id'). + // When params.patient_col is unset (all bulk runs), this resolves to meta.patient — + // identical to the previous behavior. + def patient_key = params.patient_col ?: 'patient' def patient_groups = processed_samples - .map { meta, file -> [ meta.patient, file ] } + .map { meta, file -> [ meta[patient_key] ?: meta.patient, file ] } .groupTuple() PATIENT_CONCATENATE ( patient_groups ) @@ -45,9 +49,36 @@ workflow PATIENT { params.threshold_vgene ) - if(params.use_gliph2) { + // Each gliph2_* emit is a list of [patient, file] pairs - kept separate per + // output type (rather than mixed together) so downstream staging can map + // each pair back to its known target leaf-name (e.g. "all_motifs.txt") + // without having to parse it back out of the patient-prefixed filename. + // + // NOTE: this Nextflow version's strict-syntax workflow output collection + // requires emit values to be direct .out expressions - referencing a + // pre-computed local `def` variable in `emit:` fails at definition time + // with "Missing workflow output parameter", even outside any conditional. + // So the params.use_gliph2 ternary has to live in the emit line itself, + // referencing GLIPH2_TURBOGLIPH.out directly. + if (params.use_gliph2) { GLIPH2_TURBOGLIPH( PATIENT_CONCATENATE.out.patient_cdr3 ) } + + emit: + // Additive outputs consumed only by the single-cell modality (CLUSTER_TO_SC). + // GIANA_CALC's second positional output is the giana.txt cluster file. + giana_clusters = GIANA_CALC.out[1] + gliph2_cluster_details = params.use_gliph2 ? GLIPH2_TURBOGLIPH.out.cluster_member_details_named : channel.empty() + + // .collect() flattens tuple(val, path) emissions by default (e.g. + // [patientA, fileA, patientB, fileB] instead of [[patientA, fileA], ...]), + // which corrupts the [patient, file] pair indexing used downstream in + // workflows/tcrtoolkit.nf - flat: false preserves the pair shape. + giana_files = GIANA_CALC.out.giana_output.collect() + gliph2_all_motifs = params.use_gliph2 ? GLIPH2_TURBOGLIPH.out.all_motifs.collect(flat: false) : channel.value([]) + gliph2_clone_network = params.use_gliph2 ? GLIPH2_TURBOGLIPH.out.clone_network.collect(flat: false) : channel.value([]) + gliph2_cluster_member_details = params.use_gliph2 ? GLIPH2_TURBOGLIPH.out.cluster_member_details.collect(flat: false) : channel.value([]) + gliph2_global_similarities = params.use_gliph2 ? GLIPH2_TURBOGLIPH.out.global_similarities.collect(flat: false) : channel.value([]) } \ No newline at end of file diff --git a/subworkflows/local/pseudobulk_qc.nf b/subworkflows/local/pseudobulk_qc.nf new file mode 100644 index 0000000..3d5a6f2 --- /dev/null +++ b/subworkflows/local/pseudobulk_qc.nf @@ -0,0 +1,93 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl = 2 + +include { + PSEUDOBULK_QC_CALC + PSEUDOBULK_QC_AGGREGATE as PSEUDOBULK_QC_SUMMARY + PSEUDOBULK_QC_AGGREGATE as PSEUDOBULK_VFAMILY_SUMMARY +} from '../../modules/local/pseudobulk_qc/main' + +/* + * PSEUDOBULK_QC_SW + * + * TCRtoolkit QC gate applied to pseudobulk-derived bulk TCR data + * (SC_TO_BULK in combined Track B, SC_TO_CDR3 in full-SC mode). + * + * For every sample it computes n_clones / n_cells and gates on: + * n_clones >= params.pseudobulk_qc_min_clones AND + * n_cells >= params.pseudobulk_qc_min_cells + * + * Behaviour on failing samples (params.pseudobulk_qc_mode): + * 'drop' (default) — skip failing samples, continue with survivors + * 'hard_stop' — abort the run if any sample fails QC + * + * Emits: + * sample_map — [meta, file] for QC-passing samples only + * concat_cdr3 — concatenated CDR3 file rebuilt from passing samples + * (consumed by ANNOTATE_FROM_CONCAT in full-SC mode) + * qc_summary — per-sample QC table (includes dropped samples) + * v_family — per-sample V gene-family usage table + */ +workflow PSEUDOBULK_QC_SW { + take: + sample_map // channel: [meta, file] pseudobulk per-sample TCR tables + + main: + def min_clones = params.pseudobulk_qc_min_clones + def min_cells = params.pseudobulk_qc_min_cells + def gate_mode = (params.pseudobulk_qc_mode ?: 'drop').toLowerCase() + + // Wrap the scalar thresholds as explicit value channels. Nextflow 24.10 does not + // reliably auto-broadcast a bare value passed alongside a queue-channel input. + PSEUDOBULK_QC_CALC( sample_map, Channel.value(min_clones), Channel.value(min_cells) ) + + branched = PSEUDOBULK_QC_CALC.out.scored.branch { meta, file, qc_pass, nc, ce -> + pass: qc_pass == 'PASS' + fail: true + } + + // Log every dropped sample + branched.fail.view { meta, file, qc_pass, nc, ce -> + "[Pseudobulk QC] DROPPED '${meta.sample}': clones=${nc} (min ${min_clones}), cells=${ce} (min ${min_cells})" + } + + // Optional hard-stop: abort the run if any sample fails QC + if (gate_mode == 'hard_stop') { + branched.fail + .map { meta, file, qc_pass, nc, ce -> "${meta.sample} (clones=${nc}, cells=${ce})" } + .collect() + .subscribe { failed -> + if (failed) { + throw new RuntimeException( + "[Pseudobulk QC] hard-stop: ${failed.size()} sample(s) failed QC -> ${failed.join('; ')}" + ) + } + } + } + + // Passing samples only -> [meta, file] + passed_map = branched.pass.map { meta, file, qc_pass, nc, ce -> [meta, file] } + + // Warn (drop mode) if nothing survived the gate + passed_map.count().subscribe { n -> + if (n == 0) { + log.warn "[Pseudobulk QC] No samples passed QC " + + "(min_clones=${min_clones}, min_cells=${min_cells}); downstream steps will be skipped." + } + } + + // Rebuild concatenated CDR3 from passing samples (for ANNOTATE_FROM_CONCAT) + concat_cdr3 = passed_map + .map { meta, file -> file } + .collectFile(name: 'concat_cdr3.tsv', keepHeader: true, skip: 1) + + // ── Reporting (non-redundant): QC summary + V gene-family usage ────────── + PSEUDOBULK_QC_SUMMARY( PSEUDOBULK_QC_CALC.out.qc_csv.collect(), 'pseudobulk_qc_summary.csv' ) + PSEUDOBULK_VFAMILY_SUMMARY( PSEUDOBULK_QC_CALC.out.v_family_csv.collect(), 'pseudobulk_v_family.csv' ) + + emit: + sample_map = passed_map + concat_cdr3 = concat_cdr3 + qc_summary = PSEUDOBULK_QC_SUMMARY.out.aggregated_csv + v_family = PSEUDOBULK_VFAMILY_SUMMARY.out.aggregated_csv +} diff --git a/subworkflows/local/sample.nf b/subworkflows/local/sample.nf index 28f7b67..f05b53c 100644 --- a/subworkflows/local/sample.nf +++ b/subworkflows/local/sample.nf @@ -50,12 +50,18 @@ workflow SAMPLE { /////// =================== PLOT SAMPLE =================== /////// - SAMPLE_PLOT ( - file(params.samplesheet), - file(params.sample_stats_template), - sample_stats_agg, - SAMPLE_CALC_PIVOT.out.v_family_wide - ) + // SAMPLE_PLOT renders metadata-stratified charts that need bulk samplesheet columns + // (patient / origin / timepoint). Single-cell mode (which supplies --sample_sheet, not + // --samplesheet) lacks those columns, so run this only in bulk mode. SC reporting is + // covered by REPERTOIRE / MASTER_SUMMARY. SAMPLE_PLOT's output feeds nothing downstream. + if (params.samplesheet) { + SAMPLE_PLOT ( + file(params.samplesheet), + file(params.sample_stats_template), + sample_stats_agg, + SAMPLE_CALC_PIVOT.out.v_family_wide + ) + } TCRDIST3_MATRIX( processed_samples, @@ -111,5 +117,22 @@ workflow SAMPLE { VDJDB_VDJMATCH (processed_samples, VDJDB_GET.out.ref_db) emit: - sample_csv = SAMPLE_CALC.out.sample_csv + sample_csv = SAMPLE_CALC.out.sample_csv + // Additive: expose tcrdist outputs so the single-cell modality can feed + // CLUSTER_TO_SC without a second TCRDIST3_MATRIX run. Bulk ignores these. + tcrdist_clone_df = TCRDIST3_MATRIX.out.clone_df + tcrdist_output = TCRDIST3_MATRIX.out.tcrdist_output + v_family = SAMPLE_CALC_PIVOT.out.v_family_wide + j_family = SAMPLE_CALC_PIVOT.out.j_family_wide + tcrdist_files = TCRDIST3_MATRIX.out.tcrdist_output.map { _sample_meta, dist -> dist } + .mix( TCRDIST3_MATRIX.out.clone_df ) + .flatten() + .collect() + olga_files = OLGA_SAMPLE_MERGE.out.olga_pgen.map { _sample_meta, pgen -> pgen } + .collect() + vdjdb_files = VDJDB_VDJMATCH.out.vdjmatch_txt + .mix( VDJDB_VDJMATCH.out.annot_summary ) + .collect() + convergence_files = CONVERGENCE.out.convergence_output.collect() + tcrpheno_files = TCRPHENO.out.tcrpheno_output.map { _sample_meta, f -> f }.collect() } \ No newline at end of file diff --git a/subworkflows/scratch/conga.nf b/subworkflows/scratch/conga.nf new file mode 100644 index 0000000..f89f036 --- /dev/null +++ b/subworkflows/scratch/conga.nf @@ -0,0 +1,27 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl = 2 + +include { CONGA } from '../../modules/scratch/CONGA/main.nf' + +workflow CONGA_SW { + take: + seurat_rds + export_cells + project_name + + main: + ch_notebook = Channel.fromPath( + "${projectDir}/modules/scratch/CONGA/CoNGA_Report.qmd", + checkIfExists: true + ) + + CONGA( seurat_rds, export_cells, ch_notebook, project_name ) + + emit: + report_html = CONGA.out.report_html + seurat_with_conga = CONGA.out.seurat_with_conga + export_cells = CONGA.out.export_cells + data = CONGA.out.data + tables = CONGA.out.tables + figures = CONGA.out.figures +} diff --git a/subworkflows/scratch/consensus_clustering.nf b/subworkflows/scratch/consensus_clustering.nf new file mode 100644 index 0000000..0f85a1c --- /dev/null +++ b/subworkflows/scratch/consensus_clustering.nf @@ -0,0 +1,37 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl = 2 + +include { CONSENSUS_CLUSTERING } from '../../modules/scratch/CONSENSUS_CLUSTERING/main.nf' + +workflow CONSENSUS_SW { + take: + seurat_rds + export_cells + gliph_export_cells + tcrdist_export_cells + giana_export_cells + project_name + + main: + ch_notebook = Channel.fromPath( + "${projectDir}/modules/scratch/CONSENSUS_CLUSTERING/Clonotype_Clustering_Consensus_Report.qmd", + checkIfExists: true + ) + + CONSENSUS_CLUSTERING( + seurat_rds, + export_cells, + gliph_export_cells, + tcrdist_export_cells, + giana_export_cells, + ch_notebook, + project_name + ) + + emit: + report_html = CONSENSUS_CLUSTERING.out.report_html + seurat_with_consensus = CONSENSUS_CLUSTERING.out.seurat_with_consensus + export_cells = CONSENSUS_CLUSTERING.out.export_cells + tables = CONSENSUS_CLUSTERING.out.tables + figures = CONSENSUS_CLUSTERING.out.figures +} diff --git a/subworkflows/scratch/master_summary.nf b/subworkflows/scratch/master_summary.nf new file mode 100644 index 0000000..7db0d02 --- /dev/null +++ b/subworkflows/scratch/master_summary.nf @@ -0,0 +1,50 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl = 2 + +include { MASTER_SUMMARY } from '../../modules/scratch/MASTER_SUMMARY/main.nf' + +workflow MASTER_SUMMARY_SW { + take: + seurat_rds + export_cells + + vdj_qc_per_sample_compact + vdj_qc_before_after_summary + vdj_qc_sample_sheet_resolved + vdj_qc_clone_rank_abundance + + vdj_qc_before_after_retention_fig + vdj_qc_pairing_bar_fig + vdj_qc_clone_rank_abundance_fig + vdj_qc_multiple_chains_fig + + barrier_done + project_name + + main: + ch_notebook = Channel.fromPath( + "${projectDir}/modules/scratch/MASTER_SUMMARY/Master_Summary_Report.qmd", + checkIfExists: true + ) + + MASTER_SUMMARY( + seurat_rds, + export_cells, + vdj_qc_per_sample_compact, + vdj_qc_before_after_summary, + vdj_qc_sample_sheet_resolved, + vdj_qc_clone_rank_abundance, + vdj_qc_before_after_retention_fig, + vdj_qc_pairing_bar_fig, + vdj_qc_clone_rank_abundance_fig, + vdj_qc_multiple_chains_fig, + ch_notebook, + barrier_done, + project_name + ) + + emit: + report_html = MASTER_SUMMARY.out.report_html + tables = MASTER_SUMMARY.out.tables + figures = MASTER_SUMMARY.out.figures +} diff --git a/subworkflows/scratch/repertoire.nf b/subworkflows/scratch/repertoire.nf new file mode 100644 index 0000000..93d31da --- /dev/null +++ b/subworkflows/scratch/repertoire.nf @@ -0,0 +1,24 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl = 2 + +include { REPERTOIRE } from '../../modules/scratch/REPERTOIRE/main.nf' + +workflow REPERTOIRE_SW { + take: + seurat_rds + export_cells + project_name + + main: + ch_notebook = Channel.fromPath( + "${projectDir}/modules/scratch/REPERTOIRE/Repertoire_Report.qmd", + checkIfExists: true + ) + + REPERTOIRE( seurat_rds, export_cells, ch_notebook, project_name ) + + emit: + report_html = REPERTOIRE.out.report_html + tables = REPERTOIRE.out.tables + figures = REPERTOIRE.out.figures +} diff --git a/subworkflows/scratch/tcell_integration.nf b/subworkflows/scratch/tcell_integration.nf new file mode 100644 index 0000000..cecd606 --- /dev/null +++ b/subworkflows/scratch/tcell_integration.nf @@ -0,0 +1,31 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl = 2 + +include { TCELL_INTEGRATION } from '../../modules/scratch/TCELL_INTEGRATION/main.nf' + +workflow TCELL_INTEGRATION_SW { + take: + contigs_after_qc + annotated_object + project_name + + main: + ch_notebook = Channel.fromPath( + "${projectDir}/modules/scratch/TCELL_INTEGRATION/TCell_Integration_Report.qmd", + checkIfExists: true + ) + + ch_tcell = TCELL_INTEGRATION( + contigs_after_qc, + annotated_object, + ch_notebook, + project_name + ) + + emit: + report_html = ch_tcell.report_html + seurat_tcells_with_tcr = ch_tcell.seurat_tcells_with_tcr + export_cells = ch_tcell.export_cells + tables = ch_tcell.tables + figures = ch_tcell.figures +} diff --git a/subworkflows/scratch/tcri.nf b/subworkflows/scratch/tcri.nf new file mode 100644 index 0000000..7719845 --- /dev/null +++ b/subworkflows/scratch/tcri.nf @@ -0,0 +1,24 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl = 2 + +include { TCRI } from '../../modules/scratch/TCRI/main.nf' + +workflow TCRI_SW { + take: + seurat_rds + export_cells + project_name + + main: + ch_notebook = Channel.fromPath( + "${projectDir}/modules/scratch/TCRI/TCRi_Report.qmd", + checkIfExists: true + ) + + ch_tcri = TCRI( ch_notebook, seurat_rds, export_cells, project_name ) + + emit: + report_html = ch_tcri.report_html + seurat_with_tcri = ch_tcri.seurat_with_tcri + export_cells = ch_tcri.export_cells +} diff --git a/subworkflows/scratch/vdj_qc.nf b/subworkflows/scratch/vdj_qc.nf new file mode 100644 index 0000000..0234db5 --- /dev/null +++ b/subworkflows/scratch/vdj_qc.nf @@ -0,0 +1,30 @@ +#!/usr/bin/env nextflow +nextflow.enable.dsl = 2 + +include { VDJ_QC } from '../../modules/scratch/VDJ_QC/main.nf' + +workflow VDJ_QC_SW { + take: + ch_sample_sheet + ch_project_name + ch_input_annotated_object + + main: + ch_notebook = Channel.fromPath( + "${projectDir}/modules/scratch/VDJ_QC/VDJ_QC_analysis.qmd", + checkIfExists: true + ) + + ch_vdj_qc = VDJ_QC( + ch_notebook, + ch_sample_sheet, + ch_input_annotated_object, + ch_project_name + ) + + emit: + report_html = ch_vdj_qc.report_html + contigs_after_qc = ch_vdj_qc.contigs_after_qc + qc_tables = ch_vdj_qc.tables + qc_figures = ch_vdj_qc.figures +} diff --git a/tests/fixtures/singlecell/cellranger/PatientA_Base/outs/clonotypes.csv b/tests/fixtures/singlecell/cellranger/PatientA_Base/outs/clonotypes.csv new file mode 100644 index 0000000..403baea --- /dev/null +++ b/tests/fixtures/singlecell/cellranger/PatientA_Base/outs/clonotypes.csv @@ -0,0 +1,6 @@ +clonotype_id,frequency,proportion,cdr3s_aa +clonotype0,3,0.375,TRA:CAVRATGGYQKVTF;TRB:CASSLGQAYEQYF +clonotype1,2,0.25,TRA:CAASASGGSYIPTF;TRB:CASSPGQGYTF +clonotype2,1,0.125,TRA:CAVSPFGNEKLTF;TRB:CASSDRGSTDTQYF +clonotype3,1,0.125,TRA:CAMSMDSNYQLIW;TRB:CASSQEGPGNTIYF +clonotype4,1,0.125,TRA:CAGPYNQGGKLIF;TRB:CASSLAPGATNEKLFF diff --git a/tests/fixtures/singlecell/cellranger/PatientA_Base/outs/filtered_contig_annotations.csv b/tests/fixtures/singlecell/cellranger/PatientA_Base/outs/filtered_contig_annotations.csv new file mode 100644 index 0000000..66dd88b --- /dev/null +++ b/tests/fixtures/singlecell/cellranger/PatientA_Base/outs/filtered_contig_annotations.csv @@ -0,0 +1,17 @@ +barcode,is_cell,contig_id,high_confidence,length,chain,v_gene,d_gene,j_gene,c_gene,full_length,productive,cdr3,cdr3_nt,reads,umis,raw_clonotype_id,raw_consensus_id +AAACCTGAG000CELL-1,True,AAACCTGAG000CELL-1_contig_1,True,550,TRB,TRBV7-2,TRBD1,TRBJ2-7,TRBC1,True,True,CASSLGQAYEQYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype0,clonotype0_consensus_1 +AAACCTGAG000CELL-1,True,AAACCTGAG000CELL-1_contig_2,True,500,TRA,TRAV3,None,TRAJ13,TRAC,True,True,CAVRATGGYQKVTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype0,clonotype0_consensus_2 +AAACCTGAG001CELL-1,True,AAACCTGAG001CELL-1_contig_1,True,550,TRB,TRBV7-2,TRBD1,TRBJ2-7,TRBC1,True,True,CASSLGQAYEQYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype0,clonotype0_consensus_1 +AAACCTGAG001CELL-1,True,AAACCTGAG001CELL-1_contig_2,True,500,TRA,TRAV3,None,TRAJ13,TRAC,True,True,CAVRATGGYQKVTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype0,clonotype0_consensus_2 +AAACCTGAG002CELL-1,True,AAACCTGAG002CELL-1_contig_1,True,550,TRB,TRBV7-2,TRBD1,TRBJ2-7,TRBC1,True,True,CASSLGQAYEQYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype0,clonotype0_consensus_1 +AAACCTGAG002CELL-1,True,AAACCTGAG002CELL-1_contig_2,True,500,TRA,TRAV3,None,TRAJ13,TRAC,True,True,CAVRATGGYQKVTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype0,clonotype0_consensus_2 +AAACCTGAG003CELL-1,True,AAACCTGAG003CELL-1_contig_1,True,550,TRB,TRBV5-1,TRBD1,TRBJ1-2,TRBC1,True,True,CASSPGQGYTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype1,clonotype1_consensus_1 +AAACCTGAG003CELL-1,True,AAACCTGAG003CELL-1_contig_2,True,500,TRA,TRAV13-1,None,TRAJ6,TRAC,True,True,CAASASGGSYIPTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype1,clonotype1_consensus_2 +AAACCTGAG004CELL-1,True,AAACCTGAG004CELL-1_contig_1,True,550,TRB,TRBV5-1,TRBD1,TRBJ1-2,TRBC1,True,True,CASSPGQGYTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype1,clonotype1_consensus_1 +AAACCTGAG004CELL-1,True,AAACCTGAG004CELL-1_contig_2,True,500,TRA,TRAV13-1,None,TRAJ6,TRAC,True,True,CAASASGGSYIPTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype1,clonotype1_consensus_2 +AAACCTGAG005CELL-1,True,AAACCTGAG005CELL-1_contig_1,True,550,TRB,TRBV6-5,TRBD1,TRBJ2-3,TRBC1,True,True,CASSDRGSTDTQYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype2,clonotype2_consensus_1 +AAACCTGAG005CELL-1,True,AAACCTGAG005CELL-1_contig_2,True,500,TRA,TRAV21,None,TRAJ48,TRAC,True,True,CAVSPFGNEKLTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype2,clonotype2_consensus_2 +AAACCTGAG006CELL-1,True,AAACCTGAG006CELL-1_contig_1,True,550,TRB,TRBV4-1,TRBD1,TRBJ1-3,TRBC1,True,True,CASSQEGPGNTIYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype3,clonotype3_consensus_1 +AAACCTGAG006CELL-1,True,AAACCTGAG006CELL-1_contig_2,True,500,TRA,TRAV12-3,None,TRAJ33,TRAC,True,True,CAMSMDSNYQLIW,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype3,clonotype3_consensus_2 +AAACCTGAG007CELL-1,True,AAACCTGAG007CELL-1_contig_1,True,550,TRB,TRBV27,TRBD1,TRBJ1-4,TRBC1,True,True,CASSLAPGATNEKLFF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype4,clonotype4_consensus_1 +AAACCTGAG007CELL-1,True,AAACCTGAG007CELL-1_contig_2,True,500,TRA,TRAV27,None,TRAJ23,TRAC,True,True,CAGPYNQGGKLIF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype4,clonotype4_consensus_2 diff --git a/tests/fixtures/singlecell/cellranger/PatientA_Post/outs/clonotypes.csv b/tests/fixtures/singlecell/cellranger/PatientA_Post/outs/clonotypes.csv new file mode 100644 index 0000000..df8a75e --- /dev/null +++ b/tests/fixtures/singlecell/cellranger/PatientA_Post/outs/clonotypes.csv @@ -0,0 +1,5 @@ +clonotype_id,frequency,proportion,cdr3s_aa +clonotype0,1,0.125,TRA:CAVRATGGYQKVTF;TRB:CASSLGQAYEQYF +clonotype1,5,0.625,TRA:CAASASGGSYIPTF;TRB:CASSPGQGYTF +clonotype5,1,0.125,TRA:CAVNAGGTSYGKLTF;TRB:CATSRDSSYEQYF +clonotype6,1,0.125,TRA:CAASGGSNYKLTF;TRB:CASRPGQGAYEQYF diff --git a/tests/fixtures/singlecell/cellranger/PatientA_Post/outs/filtered_contig_annotations.csv b/tests/fixtures/singlecell/cellranger/PatientA_Post/outs/filtered_contig_annotations.csv new file mode 100644 index 0000000..5c4b8ca --- /dev/null +++ b/tests/fixtures/singlecell/cellranger/PatientA_Post/outs/filtered_contig_annotations.csv @@ -0,0 +1,17 @@ +barcode,is_cell,contig_id,high_confidence,length,chain,v_gene,d_gene,j_gene,c_gene,full_length,productive,cdr3,cdr3_nt,reads,umis,raw_clonotype_id,raw_consensus_id +AAACCTGAG000CELL-1,True,AAACCTGAG000CELL-1_contig_1,True,550,TRB,TRBV7-2,TRBD1,TRBJ2-7,TRBC1,True,True,CASSLGQAYEQYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype0,clonotype0_consensus_1 +AAACCTGAG000CELL-1,True,AAACCTGAG000CELL-1_contig_2,True,500,TRA,TRAV3,None,TRAJ13,TRAC,True,True,CAVRATGGYQKVTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype0,clonotype0_consensus_2 +AAACCTGAG001CELL-1,True,AAACCTGAG001CELL-1_contig_1,True,550,TRB,TRBV5-1,TRBD1,TRBJ1-2,TRBC1,True,True,CASSPGQGYTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype1,clonotype1_consensus_1 +AAACCTGAG001CELL-1,True,AAACCTGAG001CELL-1_contig_2,True,500,TRA,TRAV13-1,None,TRAJ6,TRAC,True,True,CAASASGGSYIPTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype1,clonotype1_consensus_2 +AAACCTGAG002CELL-1,True,AAACCTGAG002CELL-1_contig_1,True,550,TRB,TRBV5-1,TRBD1,TRBJ1-2,TRBC1,True,True,CASSPGQGYTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype1,clonotype1_consensus_1 +AAACCTGAG002CELL-1,True,AAACCTGAG002CELL-1_contig_2,True,500,TRA,TRAV13-1,None,TRAJ6,TRAC,True,True,CAASASGGSYIPTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype1,clonotype1_consensus_2 +AAACCTGAG003CELL-1,True,AAACCTGAG003CELL-1_contig_1,True,550,TRB,TRBV5-1,TRBD1,TRBJ1-2,TRBC1,True,True,CASSPGQGYTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype1,clonotype1_consensus_1 +AAACCTGAG003CELL-1,True,AAACCTGAG003CELL-1_contig_2,True,500,TRA,TRAV13-1,None,TRAJ6,TRAC,True,True,CAASASGGSYIPTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype1,clonotype1_consensus_2 +AAACCTGAG004CELL-1,True,AAACCTGAG004CELL-1_contig_1,True,550,TRB,TRBV5-1,TRBD1,TRBJ1-2,TRBC1,True,True,CASSPGQGYTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype1,clonotype1_consensus_1 +AAACCTGAG004CELL-1,True,AAACCTGAG004CELL-1_contig_2,True,500,TRA,TRAV13-1,None,TRAJ6,TRAC,True,True,CAASASGGSYIPTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype1,clonotype1_consensus_2 +AAACCTGAG005CELL-1,True,AAACCTGAG005CELL-1_contig_1,True,550,TRB,TRBV5-1,TRBD1,TRBJ1-2,TRBC1,True,True,CASSPGQGYTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype1,clonotype1_consensus_1 +AAACCTGAG005CELL-1,True,AAACCTGAG005CELL-1_contig_2,True,500,TRA,TRAV13-1,None,TRAJ6,TRAC,True,True,CAASASGGSYIPTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype1,clonotype1_consensus_2 +AAACCTGAG006CELL-1,True,AAACCTGAG006CELL-1_contig_1,True,550,TRB,TRBV19,TRBD1,TRBJ2-7,TRBC1,True,True,CATSRDSSYEQYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype5,clonotype5_consensus_1 +AAACCTGAG006CELL-1,True,AAACCTGAG006CELL-1_contig_2,True,500,TRA,TRAV1-2,None,TRAJ52,TRAC,True,True,CAVNAGGTSYGKLTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype5,clonotype5_consensus_2 +AAACCTGAG007CELL-1,True,AAACCTGAG007CELL-1_contig_1,True,550,TRB,TRBV20-1,TRBD1,TRBJ2-7,TRBC1,True,True,CASRPGQGAYEQYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype6,clonotype6_consensus_1 +AAACCTGAG007CELL-1,True,AAACCTGAG007CELL-1_contig_2,True,500,TRA,TRAV17,None,TRAJ53,TRAC,True,True,CAASGGSNYKLTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype6,clonotype6_consensus_2 diff --git a/tests/fixtures/singlecell/cellranger/PatientB_Base/outs/clonotypes.csv b/tests/fixtures/singlecell/cellranger/PatientB_Base/outs/clonotypes.csv new file mode 100644 index 0000000..098e74c --- /dev/null +++ b/tests/fixtures/singlecell/cellranger/PatientB_Base/outs/clonotypes.csv @@ -0,0 +1,8 @@ +clonotype_id,frequency,proportion,cdr3s_aa +clonotype0,1,0.125,TRA:CAVRATGGYQKVTF;TRB:CASSLGQAYEQYF +clonotype1,1,0.125,TRA:CAASASGGSYIPTF;TRB:CASSPGQGYTF +clonotype2,2,0.25,TRA:CAVSPFGNEKLTF;TRB:CASSDRGSTDTQYF +clonotype3,1,0.125,TRA:CAMSMDSNYQLIW;TRB:CASSQEGPGNTIYF +clonotype4,1,0.125,TRA:CAGPYNQGGKLIF;TRB:CASSLAPGATNEKLFF +clonotype5,1,0.125,TRA:CAVNAGGTSYGKLTF;TRB:CATSRDSSYEQYF +clonotype6,1,0.125,TRA:CAASGGSNYKLTF;TRB:CASRPGQGAYEQYF diff --git a/tests/fixtures/singlecell/cellranger/PatientB_Base/outs/filtered_contig_annotations.csv b/tests/fixtures/singlecell/cellranger/PatientB_Base/outs/filtered_contig_annotations.csv new file mode 100644 index 0000000..6bd6c6d --- /dev/null +++ b/tests/fixtures/singlecell/cellranger/PatientB_Base/outs/filtered_contig_annotations.csv @@ -0,0 +1,17 @@ +barcode,is_cell,contig_id,high_confidence,length,chain,v_gene,d_gene,j_gene,c_gene,full_length,productive,cdr3,cdr3_nt,reads,umis,raw_clonotype_id,raw_consensus_id +AAACCTGAG000CELL-1,True,AAACCTGAG000CELL-1_contig_1,True,550,TRB,TRBV6-5,TRBD1,TRBJ2-3,TRBC1,True,True,CASSDRGSTDTQYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype2,clonotype2_consensus_1 +AAACCTGAG000CELL-1,True,AAACCTGAG000CELL-1_contig_2,True,500,TRA,TRAV21,None,TRAJ48,TRAC,True,True,CAVSPFGNEKLTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype2,clonotype2_consensus_2 +AAACCTGAG001CELL-1,True,AAACCTGAG001CELL-1_contig_1,True,550,TRB,TRBV6-5,TRBD1,TRBJ2-3,TRBC1,True,True,CASSDRGSTDTQYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype2,clonotype2_consensus_1 +AAACCTGAG001CELL-1,True,AAACCTGAG001CELL-1_contig_2,True,500,TRA,TRAV21,None,TRAJ48,TRAC,True,True,CAVSPFGNEKLTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype2,clonotype2_consensus_2 +AAACCTGAG002CELL-1,True,AAACCTGAG002CELL-1_contig_1,True,550,TRB,TRBV4-1,TRBD1,TRBJ1-3,TRBC1,True,True,CASSQEGPGNTIYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype3,clonotype3_consensus_1 +AAACCTGAG002CELL-1,True,AAACCTGAG002CELL-1_contig_2,True,500,TRA,TRAV12-3,None,TRAJ33,TRAC,True,True,CAMSMDSNYQLIW,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype3,clonotype3_consensus_2 +AAACCTGAG003CELL-1,True,AAACCTGAG003CELL-1_contig_1,True,550,TRB,TRBV27,TRBD1,TRBJ1-4,TRBC1,True,True,CASSLAPGATNEKLFF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype4,clonotype4_consensus_1 +AAACCTGAG003CELL-1,True,AAACCTGAG003CELL-1_contig_2,True,500,TRA,TRAV27,None,TRAJ23,TRAC,True,True,CAGPYNQGGKLIF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype4,clonotype4_consensus_2 +AAACCTGAG004CELL-1,True,AAACCTGAG004CELL-1_contig_1,True,550,TRB,TRBV19,TRBD1,TRBJ2-7,TRBC1,True,True,CATSRDSSYEQYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype5,clonotype5_consensus_1 +AAACCTGAG004CELL-1,True,AAACCTGAG004CELL-1_contig_2,True,500,TRA,TRAV1-2,None,TRAJ52,TRAC,True,True,CAVNAGGTSYGKLTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype5,clonotype5_consensus_2 +AAACCTGAG005CELL-1,True,AAACCTGAG005CELL-1_contig_1,True,550,TRB,TRBV20-1,TRBD1,TRBJ2-7,TRBC1,True,True,CASRPGQGAYEQYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype6,clonotype6_consensus_1 +AAACCTGAG005CELL-1,True,AAACCTGAG005CELL-1_contig_2,True,500,TRA,TRAV17,None,TRAJ53,TRAC,True,True,CAASGGSNYKLTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype6,clonotype6_consensus_2 +AAACCTGAG006CELL-1,True,AAACCTGAG006CELL-1_contig_1,True,550,TRB,TRBV7-2,TRBD1,TRBJ2-7,TRBC1,True,True,CASSLGQAYEQYF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype0,clonotype0_consensus_1 +AAACCTGAG006CELL-1,True,AAACCTGAG006CELL-1_contig_2,True,500,TRA,TRAV3,None,TRAJ13,TRAC,True,True,CAVRATGGYQKVTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype0,clonotype0_consensus_2 +AAACCTGAG007CELL-1,True,AAACCTGAG007CELL-1_contig_1,True,550,TRB,TRBV5-1,TRBD1,TRBJ1-2,TRBC1,True,True,CASSPGQGYTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1200,4,clonotype1,clonotype1_consensus_1 +AAACCTGAG007CELL-1,True,AAACCTGAG007CELL-1_contig_2,True,500,TRA,TRAV13-1,None,TRAJ6,TRAC,True,True,CAASASGGSYIPTF,TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT,1100,3,clonotype1,clonotype1_consensus_2 diff --git a/tests/fixtures/singlecell/cluster_to_sc/NO_FILE_gliph2 b/tests/fixtures/singlecell/cluster_to_sc/NO_FILE_gliph2 new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/singlecell/cluster_to_sc/NO_FILE_tcrdist_clone b/tests/fixtures/singlecell/cluster_to_sc/NO_FILE_tcrdist_clone new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/singlecell/cluster_to_sc/NO_FILE_tcrdist_matrix b/tests/fixtures/singlecell/cluster_to_sc/NO_FILE_tcrdist_matrix new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/singlecell/cluster_to_sc/PatientA_giana.txt b/tests/fixtures/singlecell/cluster_to_sc/PatientA_giana.txt new file mode 100644 index 0000000..1588127 --- /dev/null +++ b/tests/fixtures/singlecell/cluster_to_sc/PatientA_giana.txt @@ -0,0 +1,6 @@ +CDR3b cluster +CASSDQQGHFFYANRQF 1 +CASSDWTHQRNKQQRPKWLF 2 +CASSEPPHVTPSHGWHNNSGF 3 +CASSGIIATGVRWPRWQWVF 1 +CASSHFGQPTVTSEGAKRF 2 diff --git a/tests/fixtures/singlecell/cluster_to_sc/enriched_seurat.rds b/tests/fixtures/singlecell/cluster_to_sc/enriched_seurat.rds new file mode 100644 index 0000000..023f1b9 Binary files /dev/null and b/tests/fixtures/singlecell/cluster_to_sc/enriched_seurat.rds differ diff --git a/tests/fixtures/singlecell/cluster_to_sc/giana_export_cells.tsv b/tests/fixtures/singlecell/cluster_to_sc/giana_export_cells.tsv new file mode 100644 index 0000000..ba03c91 --- /dev/null +++ b/tests/fixtures/singlecell/cluster_to_sc/giana_export_cells.tsv @@ -0,0 +1,181 @@ +cell_id sample patient condition annot CTaa clone_id clone_size clone_size_bin paired_tcr has_tcr U_1 U_2 cdr3a cdr3b trav trbv traj trbj CDR3b giana_cluster +GCCAAACATCCAGCTA-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -1.76145896475969 -2.97742522822486 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +GCAGCTGCAAGCTTTG-1 PatientA_Base PatientA tumor CD8 T cell A:CAHPFDIKGDTNTHQGGQF|B:CASSLNLKDSNQRNTIKFAQF A:CAHPFDIKGDTNTHQGGQF|B:CASSLNLKDSNQRNTIKFAQF 2 [2,7] TRUE TRUE -2.18125376265703 -3.94238675700294 CAHPFDIKGDTNTHQGGQF CASSLNLKDSNQRNTIKFAQF TRAV30 TRBV30 TRAJ42 TRBJ1-5 CASSLNLKDSNQRNTIKFAQF NA +CCTACGTGGAGTCTCT-1 PatientA_Base PatientA tumor CD8 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE 12.5604359193498 -10.3216306077109 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 CASSNFQKHDILKTSGSIYF NA +GTGAAATTCAGAGATA-1 PatientA_Base PatientA tumor CD4 T cell A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF 4 [2,7] TRUE TRUE -5.28814932864366 8.6838039053811 CAARWWERHANSITAF CASSKSWHIWKTVHKYSPDAF TRAV22 TRBV29-1 TRAJ34 TRBJ1-1 CASSKSWHIWKTVHKYSPDAF NA +CGCAGCTAATGGCCCC-1 PatientA_Base PatientA tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE 12.2237125917131 -9.73065389262305 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +GCTTAACGGGTGCATC-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 12.2930637880022 -10.0137492524253 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +GGCACAGCCTCGTGCA-1 PatientA_Base PatientA tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -2.78789195578752 -2.34400571452247 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +GCCCTCACAAATTTCC-1 PatientA_Base PatientA tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -3.4977279917544 -2.03073824511634 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 CASSYTITSHWHTIEEPPDF NA +GCCATCAGTGATTCAG-1 PatientA_Base PatientA tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -6.24570092719255 9.68404184712304 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 CASSTQYRKKAWIIFNFRTYF NA +TATTGTTTCACCGCAG-1 PatientA_Base PatientA tumor CD8 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE -4.98912319701372 9.96734415425195 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 CASSNRAIAPVNLDLTHDF NA +TTACCGTGAAGTGCCG-1 PatientA_Base PatientA tumor CD4 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE 12.276452688759 -10.7411090241538 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 CASSSSRISSKTKWWF NA +CCATATACACAGGTTC-1 PatientA_Base PatientA tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -3.75678555827795 -1.94260133372413 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 CASSHVYWLQRYENAVF NA +ATGCTAGAGAAACTGC-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -5.28862843077837 7.74080025090112 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +CACCCGGGCTTGCCAA-1 PatientA_Base PatientA tumor CD4 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE 12.8339458985979 -9.62953389750587 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 CASSNRAIAPVNLDLTHDF NA +CCGGCAACGTGGCGTT-1 PatientA_Base PatientA tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -5.50058505099474 9.66362511052026 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +GATTGACAGAGCTTCT-1 PatientA_Base PatientA tumor CD8 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE 12.3304344697648 -11.0095816956626 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 CASSYTITSHWHTIEEPPDF NA +TAAGCCGCTGATGTCC-1 PatientA_Base PatientA tumor CD8 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE 11.2388531251604 -9.97715009318458 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 CASSWHSNNNFSKLQF NA +AAAAACTGAGTTAGTT-1 PatientA_Base PatientA tumor CD4 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -1.73773774665056 -2.03282369242774 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 CASSGIIATGVRWPRWQWVF PatientA_G1 +CGAGATAGTTGGATGT-1 PatientA_Base PatientA tumor CD4 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -4.15112762313543 9.95951925648583 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 CASSEPPHVTPSHGWHNNSGF PatientA_G3 +CGCACTAGAGGGAACA-1 PatientA_Base PatientA tumor CD8 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE 11.4427153154069 -9.65000642405616 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 CASSWHSNNNFSKLQF NA +GCCATAGGACCGATAC-1 PatientA_Base PatientA tumor CD8 T cell A:CAEKSPWLDQRRLWF|B:CASSVTRFSNAIYWRHSKPFF A:CAEKSPWLDQRRLWF|B:CASSVTRFSNAIYWRHSKPFF 3 [2,7] TRUE TRUE -5.47066888373552 9.06673179997338 CAEKSPWLDQRRLWF CASSVTRFSNAIYWRHSKPFF TRAV35 TRBV14 TRAJ48 TRBJ2-3 CASSVTRFSNAIYWRHSKPFF NA +AGACAGTTCAATGGCG-1 PatientA_Base PatientA tumor CD8 T cell A:CADKWSWLNKYAKDSPEF|B:CASSWLARPGNTKSPQF A:CADKWSWLNKYAKDSPEF|B:CASSWLARPGNTKSPQF 3 [2,7] TRUE TRUE -3.31344542067705 -3.67929948435889 CADKWSWLNKYAKDSPEF CASSWLARPGNTKSPQF TRAV41 TRBV10-3 TRAJ54 TRBJ2-6 CASSWLARPGNTKSPQF NA +GATGCCCGCACCTGTA-1 PatientA_Base PatientA tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 12.4322010560686 -8.88576615916358 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +TACACCTAGTATGTAA-1 PatientA_Base PatientA tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -3.34895685475527 -3.17930902110206 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 CASSDQQGHFFYANRQF PatientA_G1 +AAATTGAATACGAGAG-1 PatientA_Base PatientA tumor CD4 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -4.89543405335604 9.85885988606347 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 CASSEPPHVTPSHGWHNNSGF PatientA_G3 +GGGCACCCTTTTGCCC-1 PatientA_Base PatientA tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -5.6700201944655 7.99737345112695 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +TCCCACTGTCTATTCC-1 PatientA_Base PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -4.21085706513582 -2.57124198542701 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 CASSGIIATGVRWPRWQWVF PatientA_G1 +AGGTCGCCGAATATGT-1 PatientA_Base PatientA tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -5.82378038924394 10.0177801741494 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +CCCGACTTCGACCCGT-1 PatientA_Base PatientA tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -5.97957715552507 8.28916632069482 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 CASSDQQGHFFYANRQF PatientA_G1 +TTGGTAGGCCAGGGGC-1 PatientA_Base PatientA tumor CD8 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE -2.06546577971636 -2.79413617716895 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 CASSSSRISSKTKWWF NA +CTGGGCTAAACACGGC-1 PatientA_Base PatientA tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE 11.498861937111 -9.80322373972999 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 CASSTQYRKKAWIIFNFRTYF NA +CTTCCCGATTGCCATG-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 13.2652966066057 -10.0666447983848 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +AACTCGTTTCGCCTTA-1 PatientA_Base PatientA tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -2.21151623290239 -1.79774249659644 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +AAGTCCAACCGTAAAG-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 11.788807539528 -9.34014905558692 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +AAGCGACAGCGACCTA-1 PatientA_Base PatientA tumor CD8 T cell A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF 5 [2,7] TRUE TRUE -1.9514823870009 -2.14259089099036 CADWPVAWPSEGVSF CASSDWTHQRNKQQRPKWLF TRAV8-1 TRBV15 TRAJ33 TRBJ1-3 CASSDWTHQRNKQQRPKWLF PatientA_G2 +ATCCGAGCTTTATCTT-1 PatientA_Base PatientA tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -4.88523498337923 7.76070581807031 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +TTAAGCGGGCGCGGGG-1 PatientA_Base PatientA tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -5.0857563928908 7.47528635395898 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +ATGGAGTCTAAGGCAC-1 PatientA_Base PatientA tumor CD8 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -3.07759294074236 -2.76540673838721 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 CASSYTITSHWHTIEEPPDF NA +AGACGGATGCTCATCA-1 PatientA_Base PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -1.46512946646868 -2.91072286234962 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 CASSGIIATGVRWPRWQWVF PatientA_G1 +TATCTCGTAGCATTCA-1 PatientA_Base PatientA tumor CD8 T cell A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF 5 [2,7] TRUE TRUE -6.18157729666887 9.70356165303124 CADWPVAWPSEGVSF CASSDWTHQRNKQQRPKWLF TRAV8-1 TRBV15 TRAJ33 TRBJ1-3 CASSDWTHQRNKQQRPKWLF PatientA_G2 +TAATGTTGTGAAAAAC-1 PatientA_Base PatientA tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -2.38584289115129 -2.83788789378272 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 CASSEPPHVTPSHGWHNNSGF PatientA_G3 +GGATAGGCTGGGTCAA-1 PatientA_Base PatientA tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -2.57683393519579 -3.5795017586814 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 CASSYTITSHWHTIEEPPDF NA +CAGATATGTGTGGTAG-1 PatientA_Base PatientA tumor CD8 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE -5.96161970656572 9.42165266407861 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 CASSWHSNNNFSKLQF NA +AATTACCGGCAGCTCT-1 PatientA_Base PatientA tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -5.06553515952288 9.18473898304833 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 CASSTQYRKKAWIIFNFRTYF NA +TTTGCCGGTGGGTGCC-1 PatientA_Base PatientA tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 12.0105787797624 -8.91977037058936 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +CGGCGACCCGAAACTC-1 PatientA_Base PatientA tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -4.58725324433504 9.03379618061913 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 CASSHVYWLQRYENAVF NA +GGATATGCATGGGGGA-1 PatientA_Base PatientA tumor CD8 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE -3.93395451057909 -2.250188003646 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 CASSNRAIAPVNLDLTHDF NA +GGGCCACCCATTTATG-1 PatientA_Base PatientA tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -4.48018327515779 9.55695425404443 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 CASSEPPHVTPSHGWHNNSGF PatientA_G3 +TGCGAGCTGGAAGACG-1 PatientA_Base PatientA tumor CD4 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE -5.95634517234026 8.83379017247094 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 CASSSSRISSKTKWWF NA +GTTCAGCGGTCCCTCT-1 PatientA_Base PatientA tumor CD8 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE -2.61713597338854 -2.47319329844581 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 CASSNFQKHDILKTSGSIYF NA +TTGGATCGATTCCAAA-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -3.55778145950495 -2.44272245036231 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +TGACTAACTACGAGAC-1 PatientA_Base PatientA tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE 11.8519188447648 -10.3314925538169 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +GTGAGGCGTATAGAAA-1 PatientA_Base PatientA tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE 11.7786690278703 -9.09034360514747 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +CTTAGCGCCGTAGCAG-1 PatientA_Base PatientA tumor CD8 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE 12.7541691346818 -10.6201955185996 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 CASSYTITSHWHTIEEPPDF NA +AGGAATCAGGAGAGGC-1 PatientA_Base PatientA tumor CD8 T cell A:CARNIQNLWRQHRF|B:CASSSGAFRQKDHLKQFIGF A:CARNIQNLWRQHRF|B:CASSSGAFRQKDHLKQFIGF 3 [2,7] TRUE TRUE 12.8578392549211 -10.2796107636558 CARNIQNLWRQHRF CASSSGAFRQKDHLKQFIGF TRAV20 TRBV25-1 TRAJ22 TRBJ2-5 CASSSGAFRQKDHLKQFIGF NA +TGGTAAAACTAAGTGT-1 PatientA_Base PatientA tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE 12.440910009926 -9.47166360484229 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 CASSYTITSHWHTIEEPPDF NA +GAAGTATAAAATGCGG-1 PatientA_Base PatientA tumor CD4 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE 11.7259966417009 -9.92512429820167 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 CASSGIIATGVRWPRWQWVF PatientA_G1 +CGCAGTTTGGTAGACT-1 PatientA_Base PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -4.06385784011541 -2.1622232304679 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 CASSGIIATGVRWPRWQWVF PatientA_G1 +CATAGTATAGGACGCA-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 11.9005686326677 -9.87875474558936 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +TACTCTGAAGTTCGAA-1 PatientA_Base PatientA tumor CD4 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE -4.10451607566534 9.77192579640283 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 CASSNRAIAPVNLDLTHDF NA +CGATCAGGGAATCCCT-1 PatientA_Post PatientA tumor CD8 T cell A:CAEKSPWLDQRRLWF|B:CASSVTRFSNAIYWRHSKPFF A:CAEKSPWLDQRRLWF|B:CASSVTRFSNAIYWRHSKPFF 3 [2,7] TRUE TRUE -4.15954832893072 9.35258709324731 CAEKSPWLDQRRLWF CASSVTRFSNAIYWRHSKPFF TRAV35 TRBV14 TRAJ48 TRBJ2-3 CASSVTRFSNAIYWRHSKPFF NA +AGGCCAGCGGATATCA-1 PatientA_Post PatientA tumor CD4 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE 12.9406105561906 -10.0560647355186 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 CASSSSRISSKTKWWF NA +CAATCCCTGTGGCACG-1 PatientA_Post PatientA tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE 12.6741444154436 -9.20812429057227 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 CASSTQYRKKAWIIFNFRTYF NA +GCATGGTGGGCTCCCC-1 PatientA_Post PatientA tumor CD4 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -3.07340857546984 -1.79618323908912 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 CASSGIIATGVRWPRWQWVF PatientA_G1 +TTATCTTAGGTTTAGT-1 PatientA_Post PatientA tumor CD4 T cell A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF 4 [2,7] TRUE TRUE -2.57876143019853 -3.99043429957496 CAARWWERHANSITAF CASSKSWHIWKTVHKYSPDAF TRAV22 TRBV29-1 TRAJ34 TRBJ1-1 CASSKSWHIWKTVHKYSPDAF NA +AGCACGCTGATCCGCT-1 PatientA_Post PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -4.11630551677404 -3.32251609431373 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 CASSGIIATGVRWPRWQWVF PatientA_G1 +CGATGAGCAAAAGACC-1 PatientA_Post PatientA tumor CD8 T cell A:CADKWSWLNKYAKDSPEF|B:CASSWLARPGNTKSPQF A:CADKWSWLNKYAKDSPEF|B:CASSWLARPGNTKSPQF 3 [2,7] TRUE TRUE 12.2834621949846 -8.96522153483497 CADKWSWLNKYAKDSPEF CASSWLARPGNTKSPQF TRAV41 TRBV10-3 TRAJ54 TRBJ2-6 CASSWLARPGNTKSPQF NA +GACGCAGTCCGCGCTT-1 PatientA_Post PatientA tumor CD4 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE -1.65295514624773 -2.35097945796119 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 CASSNRAIAPVNLDLTHDF NA +CATGCAGCTAAGTTAT-1 PatientA_Post PatientA tumor CD8 T cell A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF 5 [2,7] TRUE TRUE -5.28157493632494 7.88472210301293 CAITWHWHDVSLGEIAIF CASSWAWASEYGTVLATFNYF TRAV9-2 TRBV11-2 TRAJ13 TRBJ2-7 CASSWAWASEYGTVLATFNYF NA +TATTAATCTTTCTCCA-1 PatientA_Post PatientA tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -6.20300516646562 8.48863922490014 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +GGTTGGCTACGCTACC-1 PatientA_Post PatientA tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -5.28856715720354 9.97781549824609 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +GTTCGGCAAACATATT-1 PatientA_Post PatientA tumor CD4 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE -4.16780659716783 -3.81226838694678 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 CASSNRAIAPVNLDLTHDF NA +GCTGCAAGATACTATT-1 PatientA_Post PatientA tumor CD4 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -3.72065292459188 -3.83979953394996 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 CASSHVYWLQRYENAVF NA +AGGTGGCTGGACTTGG-1 PatientA_Post PatientA tumor CD8 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -3.36463126700578 -2.23746169673072 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 CASSYTITSHWHTIEEPPDF NA +GCTAGTGGCCGACTGC-1 PatientA_Post PatientA tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -4.27865183513819 10.1674841536416 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +TAAGTATTCCCGTTTC-1 PatientA_Post PatientA tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -5.07268843215166 8.25208174122705 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 CASSHVYWLQRYENAVF NA +AGTATACTGACACGTG-1 PatientA_Post PatientA tumor CD8 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE -4.64944365781008 7.68579994572534 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 CASSNRAIAPVNLDLTHDF NA +AATCCACCAGGGAAAA-1 PatientA_Post PatientA tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -3.83156494211613 -3.13425458537208 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 CASSTQYRKKAWIIFNFRTYF NA +CCTTAGTAGCGGCCCG-1 PatientA_Post PatientA tumor CD4 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE 11.4630209489519 -10.806470047103 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 CASSNFQKHDILKTSGSIYF NA +GTATAGCCAGCATAGG-1 PatientA_Post PatientA tumor CD4 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -1.67308005850969 -3.1337877617942 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +GTAGTAAGGGAGGCCA-1 PatientA_Post PatientA tumor CD4 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -3.45695400398432 -3.60645545588599 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +TCAACGATCGTGCGGT-1 PatientA_Post PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 12.3651291413957 -10.5127784119712 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +GTGGAATCCCAGAGAG-1 PatientA_Post PatientA tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -3.80274363767563 -2.76098788844215 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 CASSHVYWLQRYENAVF NA +GGTAGGAGGCTAAAGC-1 PatientA_Post PatientA tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE 12.1282745881731 -9.55684770213233 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 CASSYTITSHWHTIEEPPDF NA +TGTTGAGGGTAGGACT-1 PatientA_Post PatientA tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE 11.9436880632097 -10.3720961915122 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +GACAGTGTGAGCGCGA-1 PatientA_Post PatientA tumor CD8 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -3.96389163073359 9.06089912785424 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 CASSTQYRKKAWIIFNFRTYF NA +AGGTCGTGTCTTCGTT-1 PatientA_Post PatientA tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -5.30336985152422 9.59342895878686 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 CASSDQQGHFFYANRQF PatientA_G1 +AGGCTGACGCACTATA-1 PatientA_Post PatientA tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 12.9455067201311 -10.590595375167 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +CCACGTCATACGCTAT-1 PatientA_Post PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -5.7104426340407 8.36062799824609 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 CASSGIIATGVRWPRWQWVF PatientA_G1 +CATGAATCTTCCTTGC-1 PatientA_Post PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -2.02833816569505 -2.77147067652808 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 CASSGIIATGVRWPRWQWVF PatientA_G1 +TGTTGGGCTAAGCGGG-1 PatientA_Post PatientA tumor CD4 T cell A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF 5 [2,7] TRUE TRUE -3.84996134203134 -2.89506114588844 CADWPVAWPSEGVSF CASSDWTHQRNKQQRPKWLF TRAV8-1 TRBV15 TRAJ33 TRBJ1-3 CASSDWTHQRNKQQRPKWLF PatientA_G2 +GATCGCTTACGAGGTT-1 PatientA_Post PatientA tumor CD4 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -6.19743785422502 9.0558742178811 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +AAATCCCGCTCTTACA-1 PatientA_Post PatientA tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -2.76107284587084 -2.91513980494605 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +GGGAAGAGAGAGTCTT-1 PatientA_Post PatientA tumor CD8 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE -4.59169754546343 10.0634840620889 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 CASSNFQKHDILKTSGSIYF NA +CCTTACTCGAGACCCA-1 PatientA_Post PatientA tumor CD8 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE -4.49012068074404 8.26831137074365 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 CASSSSRISSKTKWWF NA +TAGTACTATGATTTCG-1 PatientA_Post PatientA tumor CD8 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE -5.09293124240099 10.594387878312 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 CASSWHSNNNFSKLQF NA +GGCAAAGAAGACGGGA-1 PatientA_Post PatientA tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -5.70813080828844 7.73437105549706 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +CCACTAATATTTGACG-1 PatientA_Post PatientA tumor CD4 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -5.21682819884477 10.4104260100259 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 CASSEPPHVTPSHGWHNNSGF PatientA_G3 +GCTCCAATCTCATGAT-1 PatientA_Post PatientA tumor CD4 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -3.08637717526613 -2.68876470194923 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 CASSDQQGHFFYANRQF PatientA_G1 +CGTCCGGGGCATGCAG-1 PatientA_Post PatientA tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -2.23073515456377 -3.45884145365821 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 CASSTQYRKKAWIIFNFRTYF NA +CCCTACATCGTCCCAG-1 PatientA_Post PatientA tumor CD8 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE -4.19036150139032 -3.24174798594581 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 CASSNFQKHDILKTSGSIYF NA +CCCGGCTAGTAACGGT-1 PatientA_Post PatientA tumor CD8 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE -5.45847341578661 9.66819702519311 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 CASSWHSNNNFSKLQF NA +AGATTATCGATGTAGC-1 PatientA_Post PatientA tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -5.72081682246385 8.21453176869287 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 CASSEPPHVTPSHGWHNNSGF PatientA_G3 +GTCAAGGCCACCCATC-1 PatientA_Post PatientA tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -4.10937000971494 8.97469078434838 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 CASSDQQGHFFYANRQF PatientA_G1 +GCTTGACGTAACAGGC-1 PatientA_Post PatientA tumor CD8 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE -2.14395353358446 -3.40606797801124 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 CASSSSRISSKTKWWF NA +CTCGTTCAAAGCCGAA-1 PatientA_Post PatientA tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE 11.88905587632 -10.8512536393272 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 CASSHVYWLQRYENAVF NA +TCAACTTGGAAGTCTC-1 PatientA_Post PatientA tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE 12.65996137101 -10.8563748704063 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +ACAGGTTCATGCTGCA-1 PatientA_Post PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -2.33964416545091 -2.65408767329322 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +GGCTGGGTGCCGCTTA-1 PatientA_Post PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -4.71262052815615 9.47078501118554 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 CASSGIIATGVRWPRWQWVF PatientA_G1 +CACACGCTAAGACAAA-1 PatientA_Post PatientA tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -4.39746770423113 8.03835379017724 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +GACTTGCGCCAAAACG-1 PatientA_Post PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -5.53527781527696 8.63431583775414 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +CTGGTGCTAAAAGAGC-1 PatientA_Post PatientA tumor CD4 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE 12.9342018647844 -9.58738530741797 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 CASSEPPHVTPSHGWHNNSGF PatientA_G3 +TTGTGATGCGTACGAG-1 PatientA_Post PatientA tumor CD8 T cell A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF 4 [2,7] TRUE TRUE -3.52331942480264 -3.57436526881324 CAARWWERHANSITAF CASSKSWHIWKTVHKYSPDAF TRAV22 TRBV29-1 TRAJ34 TRBJ1-1 CASSKSWHIWKTVHKYSPDAF NA +GCCGCGCAAATTTCAG-1 PatientA_Post PatientA tumor CD4 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE 12.6249252839738 -10.3845130311118 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 CASSSSRISSKTKWWF NA +TTAGACACGATGACCA-1 PatientA_Post PatientA tumor CD4 T cell A:CAHPFDIKGDTNTHQGGQF|B:CASSLNLKDSNQRNTIKFAQF A:CAHPFDIKGDTNTHQGGQF|B:CASSLNLKDSNQRNTIKFAQF 2 [2,7] TRUE TRUE -2.7316064552611 -2.7719131814109 CAHPFDIKGDTNTHQGGQF CASSLNLKDSNQRNTIKFAQF TRAV30 TRBV30 TRAJ42 TRBJ1-5 CASSLNLKDSNQRNTIKFAQF NA +CGAAATACCGGGGAGG-1 PatientA_Post PatientA tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -3.07679906647859 -3.27355683909522 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 CASSYTITSHWHTIEEPPDF NA +TTTAGCTTGTTTGCTT-1 PatientA_Post PatientA tumor CD4 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -5.53946647208391 10.3174074782266 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 CASSDQQGHFFYANRQF PatientA_G1 +AATCCCGCCAAGTAAA-1 PatientA_Post PatientA tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE 12.7385593934709 -9.22988809214698 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 CASSEPPHVTPSHGWHNNSGF PatientA_G3 +TAGCCCAGTCGGACGC-1 PatientA_Post PatientA tumor CD4 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -4.63243058484255 10.2407320631875 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 CASSHVYWLQRYENAVF NA +GGCTTCCGCTAGATAA-1 PatientA_Post PatientA tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -5.42474958460985 8.4782146109475 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 CASSEPPHVTPSHGWHNNSGF PatientA_G3 +TGGATGGATACTATAT-1 PatientB_Base PatientB tumor CD4 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE 12.2451511903459 -9.50545610056983 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +TTCCAAAACAGCACTG-1 PatientB_Base PatientB tumor CD8 T cell A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF 5 [2,7] TRUE TRUE 11.7509161515886 -10.4877101288901 CAITWHWHDVSLGEIAIF CASSWAWASEYGTVLATFNYF TRAV9-2 TRBV11-2 TRAJ13 TRBJ2-7 CASSWAWASEYGTVLATFNYF NA +TATTTGACGCCGCTAT-1 PatientB_Base PatientB tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -6.31513056319414 9.45800625218286 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +GGCCCTCGTGGGTGAC-1 PatientB_Base PatientB tumor CD8 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -2.09143254321276 -3.17100442515479 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 CASSYTITSHWHTIEEPPDF NA +ATGCTGGGTTAGCTAG-1 PatientB_Base PatientB tumor CD4 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -2.83433780234514 -2.06479943858253 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +CCGGGCAACAACTCTT-1 PatientB_Base PatientB tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -4.97110912364183 10.2590340269936 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +AGCGTTGAGTCGGGCT-1 PatientB_Base PatientB tumor CD4 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE -4.04740010362325 -2.79340613947974 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 CASSWHSNNNFSKLQF NA +CAGAGTTGCACCCGTT-1 PatientB_Base PatientB tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -3.96363799806891 8.58459125889672 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +GGCTGGGTTAAATATA-1 PatientB_Base PatientB tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -3.83207696836649 -2.40093864070045 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 CASSEPPHVTPSHGWHNNSGF PatientA_G3 +TCACACGGCAAGCGAT-1 PatientB_Base PatientB tumor CD8 T cell A:CADKWSWLNKYAKDSPEF|B:CASSWLARPGNTKSPQF A:CADKWSWLNKYAKDSPEF|B:CASSWLARPGNTKSPQF 3 [2,7] TRUE TRUE -4.34838336866556 -2.86247361765968 CADKWSWLNKYAKDSPEF CASSWLARPGNTKSPQF TRAV41 TRBV10-3 TRAJ54 TRBJ2-6 CASSWLARPGNTKSPQF NA +GCGTCATGTAATAGGT-1 PatientB_Base PatientB tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -2.42668292086778 -2.1113587723838 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +GCGGAACTCCAGATTG-1 PatientB_Base PatientB tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 11.8874718232805 -9.2385274277793 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +GCGATGTGCAATTACT-1 PatientB_Base PatientB tumor CD8 T cell A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF 5 [2,7] TRUE TRUE -3.31398591559587 -1.63692177401649 CADWPVAWPSEGVSF CASSDWTHQRNKQQRPKWLF TRAV8-1 TRBV15 TRAJ33 TRBJ1-3 CASSDWTHQRNKQQRPKWLF PatientA_G2 +GTGCGACTTGTCATAT-1 PatientB_Base PatientB tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -3.78946954172312 -3.43110431300269 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 CASSTQYRKKAWIIFNFRTYF NA +CTACAGGGGACTCCAG-1 PatientB_Base PatientB tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -4.97457835238634 8.94325243367089 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +AATATCACGTACTTCC-1 PatientB_Base PatientB tumor CD8 T cell A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF 4 [2,7] TRUE TRUE -5.29444512885271 8.17678152455224 CAARWWERHANSITAF CASSKSWHIWKTVHKYSPDAF TRAV22 TRBV29-1 TRAJ34 TRBJ1-1 CASSKSWHIWKTVHKYSPDAF NA +CAATACCCAATATCTT-1 PatientB_Base PatientB tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -4.6122883872336 8.80930935276879 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +GCAACCAAGCGTTTAA-1 PatientB_Base PatientB tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -6.07700666945635 8.38826786412133 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 CASSDQQGHFFYANRQF PatientA_G1 +TCACCTGTTAAATTGT-1 PatientB_Base PatientB tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -2.28883752387224 -3.36419452296363 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +GTAGAGTAGTAAGCAG-1 PatientB_Base PatientB tumor CD8 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -5.68017754119097 9.82572447193994 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 CASSYTITSHWHTIEEPPDF NA +ATTACTGCGGAGAGAG-1 PatientB_Base PatientB tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -5.80609247248827 9.98402010334863 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 CASSEPPHVTPSHGWHNNSGF PatientA_G3 +AGCGCGAATCTATACT-1 PatientB_Base PatientB tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -4.7034856991118 9.58462749852075 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 CASSGIIATGVRWPRWQWVF PatientA_G1 +ACGCTCCGTCTGGTTA-1 PatientB_Base PatientB tumor CD4 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -2.6494325117415 -2.23418820963966 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 CASSDQQGHFFYANRQF PatientA_G1 +CCGTTGCCTGGTAAGA-1 PatientB_Base PatientB tumor CD8 T cell A:CARNIQNLWRQHRF|B:CASSSGAFRQKDHLKQFIGF A:CARNIQNLWRQHRF|B:CASSSGAFRQKDHLKQFIGF 3 [2,7] TRUE TRUE 13.0261913819963 -10.3450032578574 CARNIQNLWRQHRF CASSSGAFRQKDHLKQFIGF TRAV20 TRBV25-1 TRAJ22 TRBJ2-5 CASSSGAFRQKDHLKQFIGF NA +ATGACTACGGCCTAGA-1 PatientB_Base PatientB tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 12.0490738435441 -9.91464723216163 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +ATGGGTCCACACCCTG-1 PatientB_Base PatientB tumor CD8 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE -5.4522604660338 8.77096258534326 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 CASSWHSNNNFSKLQF NA +CCTCCTAAAATTGCAC-1 PatientB_Base PatientB tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -3.51801160138307 -2.25329173670875 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 CASSYTITSHWHTIEEPPDF NA +TGAGCAGCAGAGCAAC-1 PatientB_Base PatientB tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -4.84831628363787 8.57664715184106 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +GAGTGACAAAGCTGGG-1 PatientB_Base PatientB tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -4.33460509937464 8.31950699223413 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +AGCCCCCAATAGAAAG-1 PatientB_Base PatientB tumor CD8 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE -5.69160816233812 9.23854767216577 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 CASSNFQKHDILKTSGSIYF NA +TACTGGATTTCTATGC-1 PatientB_Base PatientB tumor CD4 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE -4.28916895549951 -3.21787942515479 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 CASSNFQKHDILKTSGSIYF NA +CACATGGGACCGGCCA-1 PatientB_Base PatientB tumor CD8 T cell A:CARNIQNLWRQHRF|B:CASSSGAFRQKDHLKQFIGF A:CARNIQNLWRQHRF|B:CASSSGAFRQKDHLKQFIGF 3 [2,7] TRUE TRUE -2.79154834311662 -2.42739738093482 CARNIQNLWRQHRF CASSSGAFRQKDHLKQFIGF TRAV20 TRBV25-1 TRAJ22 TRBJ2-5 CASSSGAFRQKDHLKQFIGF NA +ATACATCATTCTGGAA-1 PatientB_Base PatientB tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -5.03040811579882 9.47834001912011 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 CASSDQQGHFFYANRQF PatientA_G1 +TGTTCCAATGTTGCTT-1 PatientB_Base PatientB tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -2.02686962645708 -2.26249946223365 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +CGGGATCATCTATCTT-1 PatientB_Base PatientB tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -2.88358232539354 -3.52014220820533 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +GGGCGGATAACATACG-1 PatientB_Base PatientB tumor CD8 T cell A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF 5 [2,7] TRUE TRUE -3.05576333563982 -3.87040055857764 CAITWHWHDVSLGEIAIF CASSWAWASEYGTVLATFNYF TRAV9-2 TRBV11-2 TRAJ13 TRBJ2-7 CASSWAWASEYGTVLATFNYF NA +GGTACGTGCCTTGGCC-1 PatientB_Base PatientB tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE 11.7021004243547 -10.4350491868125 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 CASSDQQGHFFYANRQF PatientA_G1 +GTCCTGCTCACGTGCT-1 PatientB_Base PatientB tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -4.83402654689012 7.95667492283715 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 CASSTQYRKKAWIIFNFRTYF NA +CGACGCTTACCGACAT-1 PatientB_Base PatientB tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -3.43695718210398 -3.09748519526588 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +AGCTAAGACGTGCGCT-1 PatientB_Base PatientB tumor CD4 T cell A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF 5 [2,7] TRUE TRUE -5.75576231043993 9.00335632695092 CAITWHWHDVSLGEIAIF CASSWAWASEYGTVLATFNYF TRAV9-2 TRBV11-2 TRAJ13 TRBJ2-7 CASSWAWASEYGTVLATFNYF NA +GAGGTTACTACGTCTG-1 PatientB_Base PatientB tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -1.96885320704637 -3.59549153910743 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +AAATTCCCCAGCGTTA-1 PatientB_Base PatientB tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -5.23778185408769 9.05536590947045 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 CASSGIIATGVRWPRWQWVF PatientA_G1 +ACAATTAAGACCGTTA-1 PatientB_Base PatientB tumor CD4 T cell A:CAEKSPWLDQRRLWF|B:CASSVTRFSNAIYWRHSKPFF A:CAEKSPWLDQRRLWF|B:CASSVTRFSNAIYWRHSKPFF 3 [2,7] TRUE TRUE 12.1942001863176 -10.8652335511314 CAEKSPWLDQRRLWF CASSVTRFSNAIYWRHSKPFF TRAV35 TRBV14 TRAJ48 TRBJ2-3 CASSVTRFSNAIYWRHSKPFF NA +TGCCACTAGTGTAGCT-1 PatientB_Base PatientB tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE 12.6667896791154 -9.63749803172217 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 CASSHVYWLQRYENAVF NA +TACTGATAATGACAGG-1 PatientB_Base PatientB tumor CD8 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -4.36529335539995 8.57565342320336 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 CASSTQYRKKAWIIFNFRTYF NA +TACTAAAAAGTGCAAT-1 PatientB_Base PatientB tumor CD4 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE -4.45807799857317 8.61310039891137 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 CASSSSRISSKTKWWF NA +ACCATCACGATCTATC-1 PatientB_Base PatientB tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -4.83792379658876 8.94277416600122 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +ATAGGTGTCATTGTAA-1 PatientB_Base PatientB tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE 11.6085888429338 -10.6409551011191 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 CASSHFGQPTVTSEGAKRF PatientA_G2 +AATCACGTCTATAAGA-1 PatientB_Base PatientB tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -4.79757872384249 9.36735378636254 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 CASSGIIATGVRWPRWQWVF PatientA_G1 +CCTTTTCTGCTCTTGT-1 PatientB_Base PatientB tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 11.6682125611955 -10.1317845688926 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +ACGGGTGCGATTAATA-1 PatientB_Base PatientB tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -3.85481858413873 -3.63432086573707 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +CGTCTTCGGCTTAAAA-1 PatientB_Base PatientB tumor CD4 T cell A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF 5 [2,7] TRUE TRUE -2.62648853820024 -2.81721795664893 CAITWHWHDVSLGEIAIF CASSWAWASEYGTVLATFNYF TRAV9-2 TRBV11-2 TRAJ13 TRBJ2-7 CASSWAWASEYGTVLATFNYF NA +CCGCATCACAGCGTTG-1 PatientB_Base PatientB tumor CD4 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -1.84084758322893 -2.81585086451636 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +TTCTCCAAGATACAGA-1 PatientB_Base PatientB tumor CD8 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -4.20097571771799 8.84655176533593 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 CASSTQYRKKAWIIFNFRTYF NA +AGGTACCCTTCACTAT-1 PatientB_Base PatientB tumor CD4 T cell A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF 5 [2,7] TRUE TRUE 11.9962574525529 -9.24195493327247 CADWPVAWPSEGVSF CASSDWTHQRNKQQRPKWLF TRAV8-1 TRBV15 TRAJ33 TRBJ1-3 CASSDWTHQRNKQQRPKWLF PatientA_G2 +TAATATAGAGTCTGAT-1 PatientB_Base PatientB tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -4.46694669287859 9.10492597950829 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 CASSHVYWLQRYENAVF NA +TCCCAAGGTAATTAAA-1 PatientB_Base PatientB tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE 12.4444195314104 -9.563251625167 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA +CACTTCTGAGACTTGT-1 PatientB_Base PatientB tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -1.89947518866716 -2.51306737528907 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 CASSTAKPHWFDPTIF NA +AGACAATGCAGTGCCT-1 PatientB_Base PatientB tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -4.78898880046068 10.4260042799844 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 CASSYTITSHWHTIEEPPDF NA +CACCTAGCATCGGTTG-1 PatientB_Base PatientB tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -2.76885411303697 -3.69044173823463 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 CASSSNKETAWDPVKQLQRGF NA diff --git a/tests/fixtures/singlecell/cluster_to_sc/seurat_tcells_with_TCR.rds b/tests/fixtures/singlecell/cluster_to_sc/seurat_tcells_with_TCR.rds new file mode 100644 index 0000000..1de0c2d Binary files /dev/null and b/tests/fixtures/singlecell/cluster_to_sc/seurat_tcells_with_TCR.rds differ diff --git a/tests/fixtures/singlecell/cluster_to_sc/tcr_export_cells_with_embedding.tsv b/tests/fixtures/singlecell/cluster_to_sc/tcr_export_cells_with_embedding.tsv new file mode 100644 index 0000000..cf03396 --- /dev/null +++ b/tests/fixtures/singlecell/cluster_to_sc/tcr_export_cells_with_embedding.tsv @@ -0,0 +1,181 @@ +cell_id sample patient condition annot CTaa clone_id clone_size clone_size_bin paired_tcr has_tcr U_1 U_2 cdr3a cdr3b trav trbv traj trbj +GCCAAACATCCAGCTA-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -1.76145896475969 -2.97742522822486 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +GCAGCTGCAAGCTTTG-1 PatientA_Base PatientA tumor CD8 T cell A:CAHPFDIKGDTNTHQGGQF|B:CASSLNLKDSNQRNTIKFAQF A:CAHPFDIKGDTNTHQGGQF|B:CASSLNLKDSNQRNTIKFAQF 2 [2,7] TRUE TRUE -2.18125376265703 -3.94238675700294 CAHPFDIKGDTNTHQGGQF CASSLNLKDSNQRNTIKFAQF TRAV30 TRBV30 TRAJ42 TRBJ1-5 +CCTACGTGGAGTCTCT-1 PatientA_Base PatientA tumor CD8 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE 12.5604359193498 -10.3216306077109 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 +GTGAAATTCAGAGATA-1 PatientA_Base PatientA tumor CD4 T cell A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF 4 [2,7] TRUE TRUE -5.28814932864366 8.6838039053811 CAARWWERHANSITAF CASSKSWHIWKTVHKYSPDAF TRAV22 TRBV29-1 TRAJ34 TRBJ1-1 +CGCAGCTAATGGCCCC-1 PatientA_Base PatientA tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE 12.2237125917131 -9.73065389262305 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +GCTTAACGGGTGCATC-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 12.2930637880022 -10.0137492524253 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +GGCACAGCCTCGTGCA-1 PatientA_Base PatientA tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -2.78789195578752 -2.34400571452247 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +GCCCTCACAAATTTCC-1 PatientA_Base PatientA tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -3.4977279917544 -2.03073824511634 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 +GCCATCAGTGATTCAG-1 PatientA_Base PatientA tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -6.24570092719255 9.68404184712304 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 +TATTGTTTCACCGCAG-1 PatientA_Base PatientA tumor CD8 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE -4.98912319701372 9.96734415425195 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 +TTACCGTGAAGTGCCG-1 PatientA_Base PatientA tumor CD4 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE 12.276452688759 -10.7411090241538 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 +CCATATACACAGGTTC-1 PatientA_Base PatientA tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -3.75678555827795 -1.94260133372413 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 +ATGCTAGAGAAACTGC-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -5.28862843077837 7.74080025090112 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +CACCCGGGCTTGCCAA-1 PatientA_Base PatientA tumor CD4 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE 12.8339458985979 -9.62953389750587 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 +CCGGCAACGTGGCGTT-1 PatientA_Base PatientA tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -5.50058505099474 9.66362511052026 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +GATTGACAGAGCTTCT-1 PatientA_Base PatientA tumor CD8 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE 12.3304344697648 -11.0095816956626 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 +TAAGCCGCTGATGTCC-1 PatientA_Base PatientA tumor CD8 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE 11.2388531251604 -9.97715009318458 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 +AAAAACTGAGTTAGTT-1 PatientA_Base PatientA tumor CD4 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -1.73773774665056 -2.03282369242774 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 +CGAGATAGTTGGATGT-1 PatientA_Base PatientA tumor CD4 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -4.15112762313543 9.95951925648583 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 +CGCACTAGAGGGAACA-1 PatientA_Base PatientA tumor CD8 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE 11.4427153154069 -9.65000642405616 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 +GCCATAGGACCGATAC-1 PatientA_Base PatientA tumor CD8 T cell A:CAEKSPWLDQRRLWF|B:CASSVTRFSNAIYWRHSKPFF A:CAEKSPWLDQRRLWF|B:CASSVTRFSNAIYWRHSKPFF 3 [2,7] TRUE TRUE -5.47066888373552 9.06673179997338 CAEKSPWLDQRRLWF CASSVTRFSNAIYWRHSKPFF TRAV35 TRBV14 TRAJ48 TRBJ2-3 +AGACAGTTCAATGGCG-1 PatientA_Base PatientA tumor CD8 T cell A:CADKWSWLNKYAKDSPEF|B:CASSWLARPGNTKSPQF A:CADKWSWLNKYAKDSPEF|B:CASSWLARPGNTKSPQF 3 [2,7] TRUE TRUE -3.31344542067705 -3.67929948435889 CADKWSWLNKYAKDSPEF CASSWLARPGNTKSPQF TRAV41 TRBV10-3 TRAJ54 TRBJ2-6 +GATGCCCGCACCTGTA-1 PatientA_Base PatientA tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 12.4322010560686 -8.88576615916358 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +TACACCTAGTATGTAA-1 PatientA_Base PatientA tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -3.34895685475527 -3.17930902110206 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 +AAATTGAATACGAGAG-1 PatientA_Base PatientA tumor CD4 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -4.89543405335604 9.85885988606347 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 +GGGCACCCTTTTGCCC-1 PatientA_Base PatientA tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -5.6700201944655 7.99737345112695 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +TCCCACTGTCTATTCC-1 PatientA_Base PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -4.21085706513582 -2.57124198542701 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 +AGGTCGCCGAATATGT-1 PatientA_Base PatientA tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -5.82378038924394 10.0177801741494 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +CCCGACTTCGACCCGT-1 PatientA_Base PatientA tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -5.97957715552507 8.28916632069482 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 +TTGGTAGGCCAGGGGC-1 PatientA_Base PatientA tumor CD8 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE -2.06546577971636 -2.79413617716895 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 +CTGGGCTAAACACGGC-1 PatientA_Base PatientA tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE 11.498861937111 -9.80322373972999 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 +CTTCCCGATTGCCATG-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 13.2652966066057 -10.0666447983848 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +AACTCGTTTCGCCTTA-1 PatientA_Base PatientA tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -2.21151623290239 -1.79774249659644 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +AAGTCCAACCGTAAAG-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 11.788807539528 -9.34014905558692 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +AAGCGACAGCGACCTA-1 PatientA_Base PatientA tumor CD8 T cell A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF 5 [2,7] TRUE TRUE -1.9514823870009 -2.14259089099036 CADWPVAWPSEGVSF CASSDWTHQRNKQQRPKWLF TRAV8-1 TRBV15 TRAJ33 TRBJ1-3 +ATCCGAGCTTTATCTT-1 PatientA_Base PatientA tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -4.88523498337923 7.76070581807031 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +TTAAGCGGGCGCGGGG-1 PatientA_Base PatientA tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -5.0857563928908 7.47528635395898 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +ATGGAGTCTAAGGCAC-1 PatientA_Base PatientA tumor CD8 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -3.07759294074236 -2.76540673838721 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 +AGACGGATGCTCATCA-1 PatientA_Base PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -1.46512946646868 -2.91072286234962 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 +TATCTCGTAGCATTCA-1 PatientA_Base PatientA tumor CD8 T cell A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF 5 [2,7] TRUE TRUE -6.18157729666887 9.70356165303124 CADWPVAWPSEGVSF CASSDWTHQRNKQQRPKWLF TRAV8-1 TRBV15 TRAJ33 TRBJ1-3 +TAATGTTGTGAAAAAC-1 PatientA_Base PatientA tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -2.38584289115129 -2.83788789378272 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 +GGATAGGCTGGGTCAA-1 PatientA_Base PatientA tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -2.57683393519579 -3.5795017586814 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 +CAGATATGTGTGGTAG-1 PatientA_Base PatientA tumor CD8 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE -5.96161970656572 9.42165266407861 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 +AATTACCGGCAGCTCT-1 PatientA_Base PatientA tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -5.06553515952288 9.18473898304833 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 +TTTGCCGGTGGGTGCC-1 PatientA_Base PatientA tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 12.0105787797624 -8.91977037058936 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +CGGCGACCCGAAACTC-1 PatientA_Base PatientA tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -4.58725324433504 9.03379618061913 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 +GGATATGCATGGGGGA-1 PatientA_Base PatientA tumor CD8 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE -3.93395451057909 -2.250188003646 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 +GGGCCACCCATTTATG-1 PatientA_Base PatientA tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -4.48018327515779 9.55695425404443 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 +TGCGAGCTGGAAGACG-1 PatientA_Base PatientA tumor CD4 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE -5.95634517234026 8.83379017247094 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 +GTTCAGCGGTCCCTCT-1 PatientA_Base PatientA tumor CD8 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE -2.61713597338854 -2.47319329844581 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 +TTGGATCGATTCCAAA-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -3.55778145950495 -2.44272245036231 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +TGACTAACTACGAGAC-1 PatientA_Base PatientA tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE 11.8519188447648 -10.3314925538169 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +GTGAGGCGTATAGAAA-1 PatientA_Base PatientA tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE 11.7786690278703 -9.09034360514747 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +CTTAGCGCCGTAGCAG-1 PatientA_Base PatientA tumor CD8 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE 12.7541691346818 -10.6201955185996 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 +AGGAATCAGGAGAGGC-1 PatientA_Base PatientA tumor CD8 T cell A:CARNIQNLWRQHRF|B:CASSSGAFRQKDHLKQFIGF A:CARNIQNLWRQHRF|B:CASSSGAFRQKDHLKQFIGF 3 [2,7] TRUE TRUE 12.8578392549211 -10.2796107636558 CARNIQNLWRQHRF CASSSGAFRQKDHLKQFIGF TRAV20 TRBV25-1 TRAJ22 TRBJ2-5 +TGGTAAAACTAAGTGT-1 PatientA_Base PatientA tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE 12.440910009926 -9.47166360484229 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 +GAAGTATAAAATGCGG-1 PatientA_Base PatientA tumor CD4 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE 11.7259966417009 -9.92512429820167 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 +CGCAGTTTGGTAGACT-1 PatientA_Base PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -4.06385784011541 -2.1622232304679 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 +CATAGTATAGGACGCA-1 PatientA_Base PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 11.9005686326677 -9.87875474558936 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +TACTCTGAAGTTCGAA-1 PatientA_Base PatientA tumor CD4 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE -4.10451607566534 9.77192579640283 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 +CGATCAGGGAATCCCT-1 PatientA_Post PatientA tumor CD8 T cell A:CAEKSPWLDQRRLWF|B:CASSVTRFSNAIYWRHSKPFF A:CAEKSPWLDQRRLWF|B:CASSVTRFSNAIYWRHSKPFF 3 [2,7] TRUE TRUE -4.15954832893072 9.35258709324731 CAEKSPWLDQRRLWF CASSVTRFSNAIYWRHSKPFF TRAV35 TRBV14 TRAJ48 TRBJ2-3 +AGGCCAGCGGATATCA-1 PatientA_Post PatientA tumor CD4 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE 12.9406105561906 -10.0560647355186 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 +CAATCCCTGTGGCACG-1 PatientA_Post PatientA tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE 12.6741444154436 -9.20812429057227 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 +GCATGGTGGGCTCCCC-1 PatientA_Post PatientA tumor CD4 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -3.07340857546984 -1.79618323908912 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 +TTATCTTAGGTTTAGT-1 PatientA_Post PatientA tumor CD4 T cell A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF 4 [2,7] TRUE TRUE -2.57876143019853 -3.99043429957496 CAARWWERHANSITAF CASSKSWHIWKTVHKYSPDAF TRAV22 TRBV29-1 TRAJ34 TRBJ1-1 +AGCACGCTGATCCGCT-1 PatientA_Post PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -4.11630551677404 -3.32251609431373 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 +CGATGAGCAAAAGACC-1 PatientA_Post PatientA tumor CD8 T cell A:CADKWSWLNKYAKDSPEF|B:CASSWLARPGNTKSPQF A:CADKWSWLNKYAKDSPEF|B:CASSWLARPGNTKSPQF 3 [2,7] TRUE TRUE 12.2834621949846 -8.96522153483497 CADKWSWLNKYAKDSPEF CASSWLARPGNTKSPQF TRAV41 TRBV10-3 TRAJ54 TRBJ2-6 +GACGCAGTCCGCGCTT-1 PatientA_Post PatientA tumor CD4 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE -1.65295514624773 -2.35097945796119 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 +CATGCAGCTAAGTTAT-1 PatientA_Post PatientA tumor CD8 T cell A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF 5 [2,7] TRUE TRUE -5.28157493632494 7.88472210301293 CAITWHWHDVSLGEIAIF CASSWAWASEYGTVLATFNYF TRAV9-2 TRBV11-2 TRAJ13 TRBJ2-7 +TATTAATCTTTCTCCA-1 PatientA_Post PatientA tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -6.20300516646562 8.48863922490014 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +GGTTGGCTACGCTACC-1 PatientA_Post PatientA tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -5.28856715720354 9.97781549824609 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +GTTCGGCAAACATATT-1 PatientA_Post PatientA tumor CD4 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE -4.16780659716783 -3.81226838694678 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 +GCTGCAAGATACTATT-1 PatientA_Post PatientA tumor CD4 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -3.72065292459188 -3.83979953394996 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 +AGGTGGCTGGACTTGG-1 PatientA_Post PatientA tumor CD8 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -3.36463126700578 -2.23746169673072 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 +GCTAGTGGCCGACTGC-1 PatientA_Post PatientA tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -4.27865183513819 10.1674841536416 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +TAAGTATTCCCGTTTC-1 PatientA_Post PatientA tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -5.07268843215166 8.25208174122705 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 +AGTATACTGACACGTG-1 PatientA_Post PatientA tumor CD8 T cell A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF A:CAYHWEEFNGAFPQF|B:CASSNRAIAPVNLDLTHDF 7 [2,7] TRUE TRUE -4.64944365781008 7.68579994572534 CAYHWEEFNGAFPQF CASSNRAIAPVNLDLTHDF TRAV5 TRBV24-1 TRAJ40 TRBJ2-1 +AATCCACCAGGGAAAA-1 PatientA_Post PatientA tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -3.83156494211613 -3.13425458537208 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 +CCTTAGTAGCGGCCCG-1 PatientA_Post PatientA tumor CD4 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE 11.4630209489519 -10.806470047103 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 +GTATAGCCAGCATAGG-1 PatientA_Post PatientA tumor CD4 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -1.67308005850969 -3.1337877617942 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +GTAGTAAGGGAGGCCA-1 PatientA_Post PatientA tumor CD4 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -3.45695400398432 -3.60645545588599 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +TCAACGATCGTGCGGT-1 PatientA_Post PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 12.3651291413957 -10.5127784119712 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +GTGGAATCCCAGAGAG-1 PatientA_Post PatientA tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -3.80274363767563 -2.76098788844215 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 +GGTAGGAGGCTAAAGC-1 PatientA_Post PatientA tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE 12.1282745881731 -9.55684770213233 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 +TGTTGAGGGTAGGACT-1 PatientA_Post PatientA tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE 11.9436880632097 -10.3720961915122 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +GACAGTGTGAGCGCGA-1 PatientA_Post PatientA tumor CD8 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -3.96389163073359 9.06089912785424 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 +AGGTCGTGTCTTCGTT-1 PatientA_Post PatientA tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -5.30336985152422 9.59342895878686 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 +AGGCTGACGCACTATA-1 PatientA_Post PatientA tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 12.9455067201311 -10.590595375167 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +CCACGTCATACGCTAT-1 PatientA_Post PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -5.7104426340407 8.36062799824609 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 +CATGAATCTTCCTTGC-1 PatientA_Post PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -2.02833816569505 -2.77147067652808 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 +TGTTGGGCTAAGCGGG-1 PatientA_Post PatientA tumor CD4 T cell A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF 5 [2,7] TRUE TRUE -3.84996134203134 -2.89506114588844 CADWPVAWPSEGVSF CASSDWTHQRNKQQRPKWLF TRAV8-1 TRBV15 TRAJ33 TRBJ1-3 +GATCGCTTACGAGGTT-1 PatientA_Post PatientA tumor CD4 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -6.19743785422502 9.0558742178811 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +AAATCCCGCTCTTACA-1 PatientA_Post PatientA tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -2.76107284587084 -2.91513980494605 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +GGGAAGAGAGAGTCTT-1 PatientA_Post PatientA tumor CD8 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE -4.59169754546343 10.0634840620889 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 +CCTTACTCGAGACCCA-1 PatientA_Post PatientA tumor CD8 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE -4.49012068074404 8.26831137074365 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 +TAGTACTATGATTTCG-1 PatientA_Post PatientA tumor CD8 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE -5.09293124240099 10.594387878312 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 +GGCAAAGAAGACGGGA-1 PatientA_Post PatientA tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -5.70813080828844 7.73437105549706 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +CCACTAATATTTGACG-1 PatientA_Post PatientA tumor CD4 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -5.21682819884477 10.4104260100259 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 +GCTCCAATCTCATGAT-1 PatientA_Post PatientA tumor CD4 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -3.08637717526613 -2.68876470194923 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 +CGTCCGGGGCATGCAG-1 PatientA_Post PatientA tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -2.23073515456377 -3.45884145365821 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 +CCCTACATCGTCCCAG-1 PatientA_Post PatientA tumor CD8 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE -4.19036150139032 -3.24174798594581 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 +CCCGGCTAGTAACGGT-1 PatientA_Post PatientA tumor CD8 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE -5.45847341578661 9.66819702519311 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 +AGATTATCGATGTAGC-1 PatientA_Post PatientA tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -5.72081682246385 8.21453176869287 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 +GTCAAGGCCACCCATC-1 PatientA_Post PatientA tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -4.10937000971494 8.97469078434838 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 +GCTTGACGTAACAGGC-1 PatientA_Post PatientA tumor CD8 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE -2.14395353358446 -3.40606797801124 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 +CTCGTTCAAAGCCGAA-1 PatientA_Post PatientA tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE 11.88905587632 -10.8512536393272 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 +TCAACTTGGAAGTCTC-1 PatientA_Post PatientA tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE 12.65996137101 -10.8563748704063 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +ACAGGTTCATGCTGCA-1 PatientA_Post PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -2.33964416545091 -2.65408767329322 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +GGCTGGGTGCCGCTTA-1 PatientA_Post PatientA tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -4.71262052815615 9.47078501118554 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 +CACACGCTAAGACAAA-1 PatientA_Post PatientA tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -4.39746770423113 8.03835379017724 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +GACTTGCGCCAAAACG-1 PatientA_Post PatientA tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -5.53527781527696 8.63431583775414 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +CTGGTGCTAAAAGAGC-1 PatientA_Post PatientA tumor CD4 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE 12.9342018647844 -9.58738530741797 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 +TTGTGATGCGTACGAG-1 PatientA_Post PatientA tumor CD8 T cell A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF 4 [2,7] TRUE TRUE -3.52331942480264 -3.57436526881324 CAARWWERHANSITAF CASSKSWHIWKTVHKYSPDAF TRAV22 TRBV29-1 TRAJ34 TRBJ1-1 +GCCGCGCAAATTTCAG-1 PatientA_Post PatientA tumor CD4 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE 12.6249252839738 -10.3845130311118 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 +TTAGACACGATGACCA-1 PatientA_Post PatientA tumor CD4 T cell A:CAHPFDIKGDTNTHQGGQF|B:CASSLNLKDSNQRNTIKFAQF A:CAHPFDIKGDTNTHQGGQF|B:CASSLNLKDSNQRNTIKFAQF 2 [2,7] TRUE TRUE -2.7316064552611 -2.7719131814109 CAHPFDIKGDTNTHQGGQF CASSLNLKDSNQRNTIKFAQF TRAV30 TRBV30 TRAJ42 TRBJ1-5 +CGAAATACCGGGGAGG-1 PatientA_Post PatientA tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -3.07679906647859 -3.27355683909522 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 +TTTAGCTTGTTTGCTT-1 PatientA_Post PatientA tumor CD4 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -5.53946647208391 10.3174074782266 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 +AATCCCGCCAAGTAAA-1 PatientA_Post PatientA tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE 12.7385593934709 -9.22988809214698 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 +TAGCCCAGTCGGACGC-1 PatientA_Post PatientA tumor CD4 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -4.63243058484255 10.2407320631875 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 +GGCTTCCGCTAGATAA-1 PatientA_Post PatientA tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -5.42474958460985 8.4782146109475 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 +TGGATGGATACTATAT-1 PatientB_Base PatientB tumor CD4 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE 12.2451511903459 -9.50545610056983 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +TTCCAAAACAGCACTG-1 PatientB_Base PatientB tumor CD8 T cell A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF 5 [2,7] TRUE TRUE 11.7509161515886 -10.4877101288901 CAITWHWHDVSLGEIAIF CASSWAWASEYGTVLATFNYF TRAV9-2 TRBV11-2 TRAJ13 TRBJ2-7 +TATTTGACGCCGCTAT-1 PatientB_Base PatientB tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -6.31513056319414 9.45800625218286 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +GGCCCTCGTGGGTGAC-1 PatientB_Base PatientB tumor CD8 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -2.09143254321276 -3.17100442515479 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 +ATGCTGGGTTAGCTAG-1 PatientB_Base PatientB tumor CD4 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -2.83433780234514 -2.06479943858253 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +CCGGGCAACAACTCTT-1 PatientB_Base PatientB tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -4.97110912364183 10.2590340269936 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +AGCGTTGAGTCGGGCT-1 PatientB_Base PatientB tumor CD4 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE -4.04740010362325 -2.79340613947974 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 +CAGAGTTGCACCCGTT-1 PatientB_Base PatientB tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -3.96363799806891 8.58459125889672 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +GGCTGGGTTAAATATA-1 PatientB_Base PatientB tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -3.83207696836649 -2.40093864070045 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 +TCACACGGCAAGCGAT-1 PatientB_Base PatientB tumor CD8 T cell A:CADKWSWLNKYAKDSPEF|B:CASSWLARPGNTKSPQF A:CADKWSWLNKYAKDSPEF|B:CASSWLARPGNTKSPQF 3 [2,7] TRUE TRUE -4.34838336866556 -2.86247361765968 CADKWSWLNKYAKDSPEF CASSWLARPGNTKSPQF TRAV41 TRBV10-3 TRAJ54 TRBJ2-6 +GCGTCATGTAATAGGT-1 PatientB_Base PatientB tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -2.42668292086778 -2.1113587723838 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +GCGGAACTCCAGATTG-1 PatientB_Base PatientB tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 11.8874718232805 -9.2385274277793 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +GCGATGTGCAATTACT-1 PatientB_Base PatientB tumor CD8 T cell A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF 5 [2,7] TRUE TRUE -3.31398591559587 -1.63692177401649 CADWPVAWPSEGVSF CASSDWTHQRNKQQRPKWLF TRAV8-1 TRBV15 TRAJ33 TRBJ1-3 +GTGCGACTTGTCATAT-1 PatientB_Base PatientB tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -3.78946954172312 -3.43110431300269 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 +CTACAGGGGACTCCAG-1 PatientB_Base PatientB tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -4.97457835238634 8.94325243367089 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +AATATCACGTACTTCC-1 PatientB_Base PatientB tumor CD8 T cell A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF A:CAARWWERHANSITAF|B:CASSKSWHIWKTVHKYSPDAF 4 [2,7] TRUE TRUE -5.29444512885271 8.17678152455224 CAARWWERHANSITAF CASSKSWHIWKTVHKYSPDAF TRAV22 TRBV29-1 TRAJ34 TRBJ1-1 +CAATACCCAATATCTT-1 PatientB_Base PatientB tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -4.6122883872336 8.80930935276879 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +GCAACCAAGCGTTTAA-1 PatientB_Base PatientB tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -6.07700666945635 8.38826786412133 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 +TCACCTGTTAAATTGT-1 PatientB_Base PatientB tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -2.28883752387224 -3.36419452296363 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +GTAGAGTAGTAAGCAG-1 PatientB_Base PatientB tumor CD8 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -5.68017754119097 9.82572447193994 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 +ATTACTGCGGAGAGAG-1 PatientB_Base PatientB tumor CD8 T cell A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF A:CANARVKVFWFEPNKYLYF|B:CASSEPPHVTPSHGWHNNSGF 11 (10,13] TRUE TRUE -5.80609247248827 9.98402010334863 CANARVKVFWFEPNKYLYF CASSEPPHVTPSHGWHNNSGF TRAV21 TRBV6-5 TRAJ48 TRBJ2-3 +AGCGCGAATCTATACT-1 PatientB_Base PatientB tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -4.7034856991118 9.58462749852075 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 +ACGCTCCGTCTGGTTA-1 PatientB_Base PatientB tumor CD4 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -2.6494325117415 -2.23418820963966 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 +CCGTTGCCTGGTAAGA-1 PatientB_Base PatientB tumor CD8 T cell A:CARNIQNLWRQHRF|B:CASSSGAFRQKDHLKQFIGF A:CARNIQNLWRQHRF|B:CASSSGAFRQKDHLKQFIGF 3 [2,7] TRUE TRUE 13.0261913819963 -10.3450032578574 CARNIQNLWRQHRF CASSSGAFRQKDHLKQFIGF TRAV20 TRBV25-1 TRAJ22 TRBJ2-5 +ATGACTACGGCCTAGA-1 PatientB_Base PatientB tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 12.0490738435441 -9.91464723216163 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +ATGGGTCCACACCCTG-1 PatientB_Base PatientB tumor CD8 T cell A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF A:CADRKEGVYEGQYRVDF|B:CASSWHSNNNFSKLQF 7 [2,7] TRUE TRUE -5.4522604660338 8.77096258534326 CADRKEGVYEGQYRVDF CASSWHSNNNFSKLQF TRAV19 TRBV6-1 TRAJ34 TRBJ1-1 +CCTCCTAAAATTGCAC-1 PatientB_Base PatientB tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -3.51801160138307 -2.25329173670875 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 +TGAGCAGCAGAGCAAC-1 PatientB_Base PatientB tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -4.84831628363787 8.57664715184106 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +GAGTGACAAAGCTGGG-1 PatientB_Base PatientB tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -4.33460509937464 8.31950699223413 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +AGCCCCCAATAGAAAG-1 PatientB_Base PatientB tumor CD8 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE -5.69160816233812 9.23854767216577 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 +TACTGGATTTCTATGC-1 PatientB_Base PatientB tumor CD4 T cell A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF A:CAQRKIAAIRQTSQGLF|B:CASSNFQKHDILKTSGSIYF 7 [2,7] TRUE TRUE -4.28916895549951 -3.21787942515479 CAQRKIAAIRQTSQGLF CASSNFQKHDILKTSGSIYF TRAV24 TRBV18 TRAJ23 TRBJ1-4 +CACATGGGACCGGCCA-1 PatientB_Base PatientB tumor CD8 T cell A:CARNIQNLWRQHRF|B:CASSSGAFRQKDHLKQFIGF A:CARNIQNLWRQHRF|B:CASSSGAFRQKDHLKQFIGF 3 [2,7] TRUE TRUE -2.79154834311662 -2.42739738093482 CARNIQNLWRQHRF CASSSGAFRQKDHLKQFIGF TRAV20 TRBV25-1 TRAJ22 TRBJ2-5 +ATACATCATTCTGGAA-1 PatientB_Base PatientB tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE -5.03040811579882 9.47834001912011 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 +TGTTCCAATGTTGCTT-1 PatientB_Base PatientB tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -2.02686962645708 -2.26249946223365 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +CGGGATCATCTATCTT-1 PatientB_Base PatientB tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -2.88358232539354 -3.52014220820533 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +GGGCGGATAACATACG-1 PatientB_Base PatientB tumor CD8 T cell A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF 5 [2,7] TRUE TRUE -3.05576333563982 -3.87040055857764 CAITWHWHDVSLGEIAIF CASSWAWASEYGTVLATFNYF TRAV9-2 TRBV11-2 TRAJ13 TRBJ2-7 +GGTACGTGCCTTGGCC-1 PatientB_Base PatientB tumor CD8 T cell A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF A:CAHSTKLGHHHFAEESRF|B:CASSDQQGHFFYANRQF 10 (7,10] TRUE TRUE 11.7021004243547 -10.4350491868125 CAHSTKLGHHHFAEESRF CASSDQQGHFFYANRQF TRAV14/DV4 TRBV12-3 TRAJ6 TRBJ1-2 +GTCCTGCTCACGTGCT-1 PatientB_Base PatientB tumor CD4 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -4.83402654689012 7.95667492283715 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 +CGACGCTTACCGACAT-1 PatientB_Base PatientB tumor CD8 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE -3.43695718210398 -3.09748519526588 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +AGCTAAGACGTGCGCT-1 PatientB_Base PatientB tumor CD4 T cell A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF 5 [2,7] TRUE TRUE -5.75576231043993 9.00335632695092 CAITWHWHDVSLGEIAIF CASSWAWASEYGTVLATFNYF TRAV9-2 TRBV11-2 TRAJ13 TRBJ2-7 +GAGGTTACTACGTCTG-1 PatientB_Base PatientB tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -1.96885320704637 -3.59549153910743 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +AAATTCCCCAGCGTTA-1 PatientB_Base PatientB tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -5.23778185408769 9.05536590947045 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 +ACAATTAAGACCGTTA-1 PatientB_Base PatientB tumor CD4 T cell A:CAEKSPWLDQRRLWF|B:CASSVTRFSNAIYWRHSKPFF A:CAEKSPWLDQRRLWF|B:CASSVTRFSNAIYWRHSKPFF 3 [2,7] TRUE TRUE 12.1942001863176 -10.8652335511314 CAEKSPWLDQRRLWF CASSVTRFSNAIYWRHSKPFF TRAV35 TRBV14 TRAJ48 TRBJ2-3 +TGCCACTAGTGTAGCT-1 PatientB_Base PatientB tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE 12.6667896791154 -9.63749803172217 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 +TACTGATAATGACAGG-1 PatientB_Base PatientB tumor CD8 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -4.36529335539995 8.57565342320336 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 +TACTAAAAAGTGCAAT-1 PatientB_Base PatientB tumor CD4 T cell A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF A:CADTHDWGYEHKFLF|B:CASSSSRISSKTKWWF 8 (7,10] TRUE TRUE -4.45807799857317 8.61310039891137 CADTHDWGYEHKFLF CASSSSRISSKTKWWF TRAV38-1 TRBV9 TRAJ42 TRBJ1-5 +ACCATCACGATCTATC-1 PatientB_Base PatientB tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -4.83792379658876 8.94277416600122 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +ATAGGTGTCATTGTAA-1 PatientB_Base PatientB tumor CD4 T cell A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF A:CALKIRSYIFWNKVF|B:CASSHFGQPTVTSEGAKRF 19 (13,19] TRUE TRUE 11.6085888429338 -10.6409551011191 CALKIRSYIFWNKVF CASSHFGQPTVTSEGAKRF TRAV13-1 TRBV5-1 TRAJ6 TRBJ1-2 +AATCACGTCTATAAGA-1 PatientB_Base PatientB tumor CD8 T cell A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF A:CAYKSEAVDRRVTSFF|B:CASSGIIATGVRWPRWQWVF 13 (10,13] TRUE TRUE -4.79757872384249 9.36735378636254 CAYKSEAVDRRVTSFF CASSGIIATGVRWPRWQWVF TRAV26-1 TRBV28 TRAJ54 TRBJ2-6 +CCTTTTCTGCTCTTGT-1 PatientB_Base PatientB tumor CD4 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE 11.6682125611955 -10.1317845688926 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +ACGGGTGCGATTAATA-1 PatientB_Base PatientB tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -3.85481858413873 -3.63432086573707 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +CGTCTTCGGCTTAAAA-1 PatientB_Base PatientB tumor CD4 T cell A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF A:CAITWHWHDVSLGEIAIF|B:CASSWAWASEYGTVLATFNYF 5 [2,7] TRUE TRUE -2.62648853820024 -2.81721795664893 CAITWHWHDVSLGEIAIF CASSWAWASEYGTVLATFNYF TRAV9-2 TRBV11-2 TRAJ13 TRBJ2-7 +CCGCATCACAGCGTTG-1 PatientB_Base PatientB tumor CD4 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -1.84084758322893 -2.81585086451636 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +TTCTCCAAGATACAGA-1 PatientB_Base PatientB tumor CD8 T cell A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF A:CAVNNETQKRAFPF|B:CASSTQYRKKAWIIFNFRTYF 11 (10,13] TRUE TRUE -4.20097571771799 8.84655176533593 CAVNNETQKRAFPF CASSTQYRKKAWIIFNFRTYF TRAV17 TRBV20-1 TRAJ22 TRBJ2-5 +AGGTACCCTTCACTAT-1 PatientB_Base PatientB tumor CD4 T cell A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF A:CADWPVAWPSEGVSF|B:CASSDWTHQRNKQQRPKWLF 5 [2,7] TRUE TRUE 11.9962574525529 -9.24195493327247 CADWPVAWPSEGVSF CASSDWTHQRNKQQRPKWLF TRAV8-1 TRBV15 TRAJ33 TRBJ1-3 +TAATATAGAGTCTGAT-1 PatientB_Base PatientB tumor CD8 T cell A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF A:CAEVISNVGVYYVVEF|B:CASSHVYWLQRYENAVF 9 (7,10] TRUE TRUE -4.46694669287859 9.10492597950829 CAEVISNVGVYYVVEF CASSHVYWLQRYENAVF TRAV1-2 TRBV19 TRAJ40 TRBJ2-1 +TCCCAAGGTAATTAAA-1 PatientB_Base PatientB tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE 12.4444195314104 -9.563251625167 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 +CACTTCTGAGACTTGT-1 PatientB_Base PatientB tumor CD8 T cell A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF A:CAIIGHKAETNIPQEF|B:CASSTAKPHWFDPTIF 23 (19,23] TRUE TRUE -1.89947518866716 -2.51306737528907 CAIIGHKAETNIPQEF CASSTAKPHWFDPTIF TRAV3 TRBV7-2 TRAJ13 TRBJ2-7 +AGACAATGCAGTGCCT-1 PatientB_Base PatientB tumor CD4 T cell A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF A:CASDRRANKQLVHFAF|B:CASSYTITSHWHTIEEPPDF 13 (10,13] TRUE TRUE -4.78898880046068 10.4260042799844 CASDRRANKQLVHFAF CASSYTITSHWHTIEEPPDF TRAV27 TRBV27 TRAJ23 TRBJ1-4 +CACCTAGCATCGGTTG-1 PatientB_Base PatientB tumor CD8 T cell A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF A:CAGDGKKDVKIRVGKF|B:CASSSNKETAWDPVKQLQRGF 17 (13,19] TRUE TRUE -2.76885411303697 -3.69044173823463 CAGDGKKDVKIRVGKF CASSSNKETAWDPVKQLQRGF TRAV12-3 TRBV4-1 TRAJ33 TRBJ1-3 diff --git a/tests/fixtures/singlecell/gex/contigs_after_qc.tsv b/tests/fixtures/singlecell/gex/contigs_after_qc.tsv new file mode 100644 index 0000000..0014a16 --- /dev/null +++ b/tests/fixtures/singlecell/gex/contigs_after_qc.tsv @@ -0,0 +1,361 @@ +sample barcode is_cell contig_id high_confidence length chain cdr3 cdr3_nt v_gene j_gene d_gene c_gene full_length productive reads umis raw_clonotype_id raw_consensus_id +PatientA_Base GCCAAACATCCAGCTA-1 True GCCAAACATCCAGCTA-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Base GCCAAACATCCAGCTA-1 True GCCAAACATCCAGCTA-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Base GCAGCTGCAAGCTTTG-1 True GCAGCTGCAAGCTTTG-1_contig_1 True 550 TRB CASSLNLKDSNQRNTIKFAQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV30 TRBJ1-5 TRBD1 TRBC1 True True 1200 4 clonotype19 clonotype19_consensus_1 +PatientA_Base GCAGCTGCAAGCTTTG-1 True GCAGCTGCAAGCTTTG-1_contig_2 True 500 TRA CAHPFDIKGDTNTHQGGQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV30 TRAJ42 None TRAC True True 1100 3 clonotype19 clonotype19_consensus_2 +PatientA_Base CCTACGTGGAGTCTCT-1 True CCTACGTGGAGTCTCT-1_contig_1 True 550 TRB CASSNFQKHDILKTSGSIYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV18 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype15 clonotype15_consensus_1 +PatientA_Base CCTACGTGGAGTCTCT-1 True CCTACGTGGAGTCTCT-1_contig_2 True 500 TRA CAQRKIAAIRQTSQGLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV24 TRAJ23 None TRAC True True 1100 3 clonotype15 clonotype15_consensus_2 +PatientA_Base GTGAAATTCAGAGATA-1 True GTGAAATTCAGAGATA-1_contig_1 True 550 TRB CASSKSWHIWKTVHKYSPDAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV29-1 TRBJ1-1 TRBD1 TRBC1 True True 1200 4 clonotype18 clonotype18_consensus_1 +PatientA_Base GTGAAATTCAGAGATA-1 True GTGAAATTCAGAGATA-1_contig_2 True 500 TRA CAARWWERHANSITAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV22 TRAJ34 None TRAC True True 1100 3 clonotype18 clonotype18_consensus_2 +PatientA_Base CGCAGCTAATGGCCCC-1 True CGCAGCTAATGGCCCC-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientA_Base CGCAGCTAATGGCCCC-1 True CGCAGCTAATGGCCCC-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientA_Base GCTTAACGGGTGCATC-1 True GCTTAACGGGTGCATC-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Base GCTTAACGGGTGCATC-1 True GCTTAACGGGTGCATC-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Base GGCACAGCCTCGTGCA-1 True GGCACAGCCTCGTGCA-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientA_Base GGCACAGCCTCGTGCA-1 True GGCACAGCCTCGTGCA-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientA_Base GCCCTCACAAATTTCC-1 True GCCCTCACAAATTTCC-1_contig_1 True 550 TRB CASSYTITSHWHTIEEPPDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV27 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype5 clonotype5_consensus_1 +PatientA_Base GCCCTCACAAATTTCC-1 True GCCCTCACAAATTTCC-1_contig_2 True 500 TRA CASDRRANKQLVHFAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV27 TRAJ23 None TRAC True True 1100 3 clonotype5 clonotype5_consensus_2 +PatientA_Base GCCATCAGTGATTCAG-1 True GCCATCAGTGATTCAG-1_contig_1 True 550 TRB CASSTQYRKKAWIIFNFRTYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV20-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype7 clonotype7_consensus_1 +PatientA_Base GCCATCAGTGATTCAG-1 True GCCATCAGTGATTCAG-1_contig_2 True 500 TRA CAVNNETQKRAFPF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV17 TRAJ22 None TRAC True True 1100 3 clonotype7 clonotype7_consensus_2 +PatientA_Base TATTGTTTCACCGCAG-1 True TATTGTTTCACCGCAG-1_contig_1 True 550 TRB CASSNRAIAPVNLDLTHDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV24-1 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype16 clonotype16_consensus_1 +PatientA_Base TATTGTTTCACCGCAG-1 True TATTGTTTCACCGCAG-1_contig_2 True 500 TRA CAYHWEEFNGAFPQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV5 TRAJ40 None TRAC True True 1100 3 clonotype16 clonotype16_consensus_2 +PatientA_Base TTACCGTGAAGTGCCG-1 True TTACCGTGAAGTGCCG-1_contig_1 True 550 TRB CASSSSRISSKTKWWF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV9 TRBJ1-5 TRBD1 TRBC1 True True 1200 4 clonotype9 clonotype9_consensus_1 +PatientA_Base TTACCGTGAAGTGCCG-1 True TTACCGTGAAGTGCCG-1_contig_2 True 500 TRA CADTHDWGYEHKFLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV38-1 TRAJ42 None TRAC True True 1100 3 clonotype9 clonotype9_consensus_2 +PatientA_Base CCATATACACAGGTTC-1 True CCATATACACAGGTTC-1_contig_1 True 550 TRB CASSHVYWLQRYENAVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV19 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype6 clonotype6_consensus_1 +PatientA_Base CCATATACACAGGTTC-1 True CCATATACACAGGTTC-1_contig_2 True 500 TRA CAEVISNVGVYYVVEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV1-2 TRAJ40 None TRAC True True 1100 3 clonotype6 clonotype6_consensus_2 +PatientA_Base ATGCTAGAGAAACTGC-1 True ATGCTAGAGAAACTGC-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Base ATGCTAGAGAAACTGC-1 True ATGCTAGAGAAACTGC-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Base CACCCGGGCTTGCCAA-1 True CACCCGGGCTTGCCAA-1_contig_1 True 550 TRB CASSNRAIAPVNLDLTHDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV24-1 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype16 clonotype16_consensus_1 +PatientA_Base CACCCGGGCTTGCCAA-1 True CACCCGGGCTTGCCAA-1_contig_2 True 500 TRA CAYHWEEFNGAFPQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV5 TRAJ40 None TRAC True True 1100 3 clonotype16 clonotype16_consensus_2 +PatientA_Base CCGGCAACGTGGCGTT-1 True CCGGCAACGTGGCGTT-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientA_Base CCGGCAACGTGGCGTT-1 True CCGGCAACGTGGCGTT-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientA_Base GATTGACAGAGCTTCT-1 True GATTGACAGAGCTTCT-1_contig_1 True 550 TRB CASSYTITSHWHTIEEPPDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV27 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype5 clonotype5_consensus_1 +PatientA_Base GATTGACAGAGCTTCT-1 True GATTGACAGAGCTTCT-1_contig_2 True 500 TRA CASDRRANKQLVHFAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV27 TRAJ23 None TRAC True True 1100 3 clonotype5 clonotype5_consensus_2 +PatientA_Base TAAGCCGCTGATGTCC-1 True TAAGCCGCTGATGTCC-1_contig_1 True 550 TRB CASSWHSNNNFSKLQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-1 TRBJ1-1 TRBD1 TRBC1 True True 1200 4 clonotype8 clonotype8_consensus_1 +PatientA_Base TAAGCCGCTGATGTCC-1 True TAAGCCGCTGATGTCC-1_contig_2 True 500 TRA CADRKEGVYEGQYRVDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV19 TRAJ34 None TRAC True True 1100 3 clonotype8 clonotype8_consensus_2 +PatientA_Base AAAAACTGAGTTAGTT-1 True AAAAACTGAGTTAGTT-1_contig_1 True 550 TRB CASSGIIATGVRWPRWQWVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV28 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype10 clonotype10_consensus_1 +PatientA_Base AAAAACTGAGTTAGTT-1 True AAAAACTGAGTTAGTT-1_contig_2 True 500 TRA CAYKSEAVDRRVTSFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV26-1 TRAJ54 None TRAC True True 1100 3 clonotype10 clonotype10_consensus_2 +PatientA_Base CGAGATAGTTGGATGT-1 True CGAGATAGTTGGATGT-1_contig_1 True 550 TRB CASSEPPHVTPSHGWHNNSGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-5 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype3 clonotype3_consensus_1 +PatientA_Base CGAGATAGTTGGATGT-1 True CGAGATAGTTGGATGT-1_contig_2 True 500 TRA CANARVKVFWFEPNKYLYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV21 TRAJ48 None TRAC True True 1100 3 clonotype3 clonotype3_consensus_2 +PatientA_Base CGCACTAGAGGGAACA-1 True CGCACTAGAGGGAACA-1_contig_1 True 550 TRB CASSWHSNNNFSKLQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-1 TRBJ1-1 TRBD1 TRBC1 True True 1200 4 clonotype8 clonotype8_consensus_1 +PatientA_Base CGCACTAGAGGGAACA-1 True CGCACTAGAGGGAACA-1_contig_2 True 500 TRA CADRKEGVYEGQYRVDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV19 TRAJ34 None TRAC True True 1100 3 clonotype8 clonotype8_consensus_2 +PatientA_Base GCCATAGGACCGATAC-1 True GCCATAGGACCGATAC-1_contig_1 True 550 TRB CASSVTRFSNAIYWRHSKPFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV14 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype13 clonotype13_consensus_1 +PatientA_Base GCCATAGGACCGATAC-1 True GCCATAGGACCGATAC-1_contig_2 True 500 TRA CAEKSPWLDQRRLWF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV35 TRAJ48 None TRAC True True 1100 3 clonotype13 clonotype13_consensus_2 +PatientA_Base AGACAGTTCAATGGCG-1 True AGACAGTTCAATGGCG-1_contig_1 True 550 TRB CASSWLARPGNTKSPQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV10-3 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype20 clonotype20_consensus_1 +PatientA_Base AGACAGTTCAATGGCG-1 True AGACAGTTCAATGGCG-1_contig_2 True 500 TRA CADKWSWLNKYAKDSPEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV41 TRAJ54 None TRAC True True 1100 3 clonotype20 clonotype20_consensus_2 +PatientA_Base GATGCCCGCACCTGTA-1 True GATGCCCGCACCTGTA-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Base GATGCCCGCACCTGTA-1 True GATGCCCGCACCTGTA-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Base TACACCTAGTATGTAA-1 True TACACCTAGTATGTAA-1_contig_1 True 550 TRB CASSDQQGHFFYANRQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV12-3 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype12 clonotype12_consensus_1 +PatientA_Base TACACCTAGTATGTAA-1 True TACACCTAGTATGTAA-1_contig_2 True 500 TRA CAHSTKLGHHHFAEESRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV14/DV4 TRAJ6 None TRAC True True 1100 3 clonotype12 clonotype12_consensus_2 +PatientA_Base AAATTGAATACGAGAG-1 True AAATTGAATACGAGAG-1_contig_1 True 550 TRB CASSEPPHVTPSHGWHNNSGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-5 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype3 clonotype3_consensus_1 +PatientA_Base AAATTGAATACGAGAG-1 True AAATTGAATACGAGAG-1_contig_2 True 500 TRA CANARVKVFWFEPNKYLYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV21 TRAJ48 None TRAC True True 1100 3 clonotype3 clonotype3_consensus_2 +PatientA_Base GGGCACCCTTTTGCCC-1 True GGGCACCCTTTTGCCC-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientA_Base GGGCACCCTTTTGCCC-1 True GGGCACCCTTTTGCCC-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientA_Base TCCCACTGTCTATTCC-1 True TCCCACTGTCTATTCC-1_contig_1 True 550 TRB CASSGIIATGVRWPRWQWVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV28 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype10 clonotype10_consensus_1 +PatientA_Base TCCCACTGTCTATTCC-1 True TCCCACTGTCTATTCC-1_contig_2 True 500 TRA CAYKSEAVDRRVTSFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV26-1 TRAJ54 None TRAC True True 1100 3 clonotype10 clonotype10_consensus_2 +PatientA_Base AGGTCGCCGAATATGT-1 True AGGTCGCCGAATATGT-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientA_Base AGGTCGCCGAATATGT-1 True AGGTCGCCGAATATGT-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientA_Base CCCGACTTCGACCCGT-1 True CCCGACTTCGACCCGT-1_contig_1 True 550 TRB CASSDQQGHFFYANRQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV12-3 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype12 clonotype12_consensus_1 +PatientA_Base CCCGACTTCGACCCGT-1 True CCCGACTTCGACCCGT-1_contig_2 True 500 TRA CAHSTKLGHHHFAEESRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV14/DV4 TRAJ6 None TRAC True True 1100 3 clonotype12 clonotype12_consensus_2 +PatientA_Base TTGGTAGGCCAGGGGC-1 True TTGGTAGGCCAGGGGC-1_contig_1 True 550 TRB CASSSSRISSKTKWWF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV9 TRBJ1-5 TRBD1 TRBC1 True True 1200 4 clonotype9 clonotype9_consensus_1 +PatientA_Base TTGGTAGGCCAGGGGC-1 True TTGGTAGGCCAGGGGC-1_contig_2 True 500 TRA CADTHDWGYEHKFLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV38-1 TRAJ42 None TRAC True True 1100 3 clonotype9 clonotype9_consensus_2 +PatientA_Base CTGGGCTAAACACGGC-1 True CTGGGCTAAACACGGC-1_contig_1 True 550 TRB CASSTQYRKKAWIIFNFRTYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV20-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype7 clonotype7_consensus_1 +PatientA_Base CTGGGCTAAACACGGC-1 True CTGGGCTAAACACGGC-1_contig_2 True 500 TRA CAVNNETQKRAFPF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV17 TRAJ22 None TRAC True True 1100 3 clonotype7 clonotype7_consensus_2 +PatientA_Base CTTCCCGATTGCCATG-1 True CTTCCCGATTGCCATG-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Base CTTCCCGATTGCCATG-1 True CTTCCCGATTGCCATG-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Base AACTCGTTTCGCCTTA-1 True AACTCGTTTCGCCTTA-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientA_Base AACTCGTTTCGCCTTA-1 True AACTCGTTTCGCCTTA-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientA_Base AAGTCCAACCGTAAAG-1 True AAGTCCAACCGTAAAG-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Base AAGTCCAACCGTAAAG-1 True AAGTCCAACCGTAAAG-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Base AAGCGACAGCGACCTA-1 True AAGCGACAGCGACCTA-1_contig_1 True 550 TRB CASSDWTHQRNKQQRPKWLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV15 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype14 clonotype14_consensus_1 +PatientA_Base AAGCGACAGCGACCTA-1 True AAGCGACAGCGACCTA-1_contig_2 True 500 TRA CADWPVAWPSEGVSF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV8-1 TRAJ33 None TRAC True True 1100 3 clonotype14 clonotype14_consensus_2 +PatientA_Base ATCCGAGCTTTATCTT-1 True ATCCGAGCTTTATCTT-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientA_Base ATCCGAGCTTTATCTT-1 True ATCCGAGCTTTATCTT-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientA_Base TTAAGCGGGCGCGGGG-1 True TTAAGCGGGCGCGGGG-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Base TTAAGCGGGCGCGGGG-1 True TTAAGCGGGCGCGGGG-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Base ATGGAGTCTAAGGCAC-1 True ATGGAGTCTAAGGCAC-1_contig_1 True 550 TRB CASSYTITSHWHTIEEPPDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV27 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype5 clonotype5_consensus_1 +PatientA_Base ATGGAGTCTAAGGCAC-1 True ATGGAGTCTAAGGCAC-1_contig_2 True 500 TRA CASDRRANKQLVHFAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV27 TRAJ23 None TRAC True True 1100 3 clonotype5 clonotype5_consensus_2 +PatientA_Base AGACGGATGCTCATCA-1 True AGACGGATGCTCATCA-1_contig_1 True 550 TRB CASSGIIATGVRWPRWQWVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV28 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype10 clonotype10_consensus_1 +PatientA_Base AGACGGATGCTCATCA-1 True AGACGGATGCTCATCA-1_contig_2 True 500 TRA CAYKSEAVDRRVTSFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV26-1 TRAJ54 None TRAC True True 1100 3 clonotype10 clonotype10_consensus_2 +PatientA_Base TATCTCGTAGCATTCA-1 True TATCTCGTAGCATTCA-1_contig_1 True 550 TRB CASSDWTHQRNKQQRPKWLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV15 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype14 clonotype14_consensus_1 +PatientA_Base TATCTCGTAGCATTCA-1 True TATCTCGTAGCATTCA-1_contig_2 True 500 TRA CADWPVAWPSEGVSF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV8-1 TRAJ33 None TRAC True True 1100 3 clonotype14 clonotype14_consensus_2 +PatientA_Base TAATGTTGTGAAAAAC-1 True TAATGTTGTGAAAAAC-1_contig_1 True 550 TRB CASSEPPHVTPSHGWHNNSGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-5 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype3 clonotype3_consensus_1 +PatientA_Base TAATGTTGTGAAAAAC-1 True TAATGTTGTGAAAAAC-1_contig_2 True 500 TRA CANARVKVFWFEPNKYLYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV21 TRAJ48 None TRAC True True 1100 3 clonotype3 clonotype3_consensus_2 +PatientA_Base GGATAGGCTGGGTCAA-1 True GGATAGGCTGGGTCAA-1_contig_1 True 550 TRB CASSYTITSHWHTIEEPPDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV27 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype5 clonotype5_consensus_1 +PatientA_Base GGATAGGCTGGGTCAA-1 True GGATAGGCTGGGTCAA-1_contig_2 True 500 TRA CASDRRANKQLVHFAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV27 TRAJ23 None TRAC True True 1100 3 clonotype5 clonotype5_consensus_2 +PatientA_Base CAGATATGTGTGGTAG-1 True CAGATATGTGTGGTAG-1_contig_1 True 550 TRB CASSWHSNNNFSKLQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-1 TRBJ1-1 TRBD1 TRBC1 True True 1200 4 clonotype8 clonotype8_consensus_1 +PatientA_Base CAGATATGTGTGGTAG-1 True CAGATATGTGTGGTAG-1_contig_2 True 500 TRA CADRKEGVYEGQYRVDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV19 TRAJ34 None TRAC True True 1100 3 clonotype8 clonotype8_consensus_2 +PatientA_Base AATTACCGGCAGCTCT-1 True AATTACCGGCAGCTCT-1_contig_1 True 550 TRB CASSTQYRKKAWIIFNFRTYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV20-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype7 clonotype7_consensus_1 +PatientA_Base AATTACCGGCAGCTCT-1 True AATTACCGGCAGCTCT-1_contig_2 True 500 TRA CAVNNETQKRAFPF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV17 TRAJ22 None TRAC True True 1100 3 clonotype7 clonotype7_consensus_2 +PatientA_Base TTTGCCGGTGGGTGCC-1 True TTTGCCGGTGGGTGCC-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Base TTTGCCGGTGGGTGCC-1 True TTTGCCGGTGGGTGCC-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Base CGGCGACCCGAAACTC-1 True CGGCGACCCGAAACTC-1_contig_1 True 550 TRB CASSHVYWLQRYENAVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV19 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype6 clonotype6_consensus_1 +PatientA_Base CGGCGACCCGAAACTC-1 True CGGCGACCCGAAACTC-1_contig_2 True 500 TRA CAEVISNVGVYYVVEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV1-2 TRAJ40 None TRAC True True 1100 3 clonotype6 clonotype6_consensus_2 +PatientA_Base GGATATGCATGGGGGA-1 True GGATATGCATGGGGGA-1_contig_1 True 550 TRB CASSNRAIAPVNLDLTHDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV24-1 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype16 clonotype16_consensus_1 +PatientA_Base GGATATGCATGGGGGA-1 True GGATATGCATGGGGGA-1_contig_2 True 500 TRA CAYHWEEFNGAFPQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV5 TRAJ40 None TRAC True True 1100 3 clonotype16 clonotype16_consensus_2 +PatientA_Base GGGCCACCCATTTATG-1 True GGGCCACCCATTTATG-1_contig_1 True 550 TRB CASSEPPHVTPSHGWHNNSGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-5 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype3 clonotype3_consensus_1 +PatientA_Base GGGCCACCCATTTATG-1 True GGGCCACCCATTTATG-1_contig_2 True 500 TRA CANARVKVFWFEPNKYLYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV21 TRAJ48 None TRAC True True 1100 3 clonotype3 clonotype3_consensus_2 +PatientA_Base TGCGAGCTGGAAGACG-1 True TGCGAGCTGGAAGACG-1_contig_1 True 550 TRB CASSSSRISSKTKWWF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV9 TRBJ1-5 TRBD1 TRBC1 True True 1200 4 clonotype9 clonotype9_consensus_1 +PatientA_Base TGCGAGCTGGAAGACG-1 True TGCGAGCTGGAAGACG-1_contig_2 True 500 TRA CADTHDWGYEHKFLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV38-1 TRAJ42 None TRAC True True 1100 3 clonotype9 clonotype9_consensus_2 +PatientA_Base GTTCAGCGGTCCCTCT-1 True GTTCAGCGGTCCCTCT-1_contig_1 True 550 TRB CASSNFQKHDILKTSGSIYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV18 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype15 clonotype15_consensus_1 +PatientA_Base GTTCAGCGGTCCCTCT-1 True GTTCAGCGGTCCCTCT-1_contig_2 True 500 TRA CAQRKIAAIRQTSQGLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV24 TRAJ23 None TRAC True True 1100 3 clonotype15 clonotype15_consensus_2 +PatientA_Base TTGGATCGATTCCAAA-1 True TTGGATCGATTCCAAA-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Base TTGGATCGATTCCAAA-1 True TTGGATCGATTCCAAA-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Base TGACTAACTACGAGAC-1 True TGACTAACTACGAGAC-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientA_Base TGACTAACTACGAGAC-1 True TGACTAACTACGAGAC-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientA_Base GTGAGGCGTATAGAAA-1 True GTGAGGCGTATAGAAA-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientA_Base GTGAGGCGTATAGAAA-1 True GTGAGGCGTATAGAAA-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientA_Base CTTAGCGCCGTAGCAG-1 True CTTAGCGCCGTAGCAG-1_contig_1 True 550 TRB CASSYTITSHWHTIEEPPDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV27 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype5 clonotype5_consensus_1 +PatientA_Base CTTAGCGCCGTAGCAG-1 True CTTAGCGCCGTAGCAG-1_contig_2 True 500 TRA CASDRRANKQLVHFAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV27 TRAJ23 None TRAC True True 1100 3 clonotype5 clonotype5_consensus_2 +PatientA_Base AGGAATCAGGAGAGGC-1 True AGGAATCAGGAGAGGC-1_contig_1 True 550 TRB CASSSGAFRQKDHLKQFIGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV25-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype17 clonotype17_consensus_1 +PatientA_Base AGGAATCAGGAGAGGC-1 True AGGAATCAGGAGAGGC-1_contig_2 True 500 TRA CARNIQNLWRQHRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV20 TRAJ22 None TRAC True True 1100 3 clonotype17 clonotype17_consensus_2 +PatientA_Base TGGTAAAACTAAGTGT-1 True TGGTAAAACTAAGTGT-1_contig_1 True 550 TRB CASSYTITSHWHTIEEPPDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV27 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype5 clonotype5_consensus_1 +PatientA_Base TGGTAAAACTAAGTGT-1 True TGGTAAAACTAAGTGT-1_contig_2 True 500 TRA CASDRRANKQLVHFAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV27 TRAJ23 None TRAC True True 1100 3 clonotype5 clonotype5_consensus_2 +PatientA_Base GAAGTATAAAATGCGG-1 True GAAGTATAAAATGCGG-1_contig_1 True 550 TRB CASSGIIATGVRWPRWQWVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV28 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype10 clonotype10_consensus_1 +PatientA_Base GAAGTATAAAATGCGG-1 True GAAGTATAAAATGCGG-1_contig_2 True 500 TRA CAYKSEAVDRRVTSFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV26-1 TRAJ54 None TRAC True True 1100 3 clonotype10 clonotype10_consensus_2 +PatientA_Base CGCAGTTTGGTAGACT-1 True CGCAGTTTGGTAGACT-1_contig_1 True 550 TRB CASSGIIATGVRWPRWQWVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV28 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype10 clonotype10_consensus_1 +PatientA_Base CGCAGTTTGGTAGACT-1 True CGCAGTTTGGTAGACT-1_contig_2 True 500 TRA CAYKSEAVDRRVTSFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV26-1 TRAJ54 None TRAC True True 1100 3 clonotype10 clonotype10_consensus_2 +PatientA_Base CATAGTATAGGACGCA-1 True CATAGTATAGGACGCA-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Base CATAGTATAGGACGCA-1 True CATAGTATAGGACGCA-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Base TACTCTGAAGTTCGAA-1 True TACTCTGAAGTTCGAA-1_contig_1 True 550 TRB CASSNRAIAPVNLDLTHDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV24-1 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype16 clonotype16_consensus_1 +PatientA_Base TACTCTGAAGTTCGAA-1 True TACTCTGAAGTTCGAA-1_contig_2 True 500 TRA CAYHWEEFNGAFPQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV5 TRAJ40 None TRAC True True 1100 3 clonotype16 clonotype16_consensus_2 +PatientA_Post CGATCAGGGAATCCCT-1 True CGATCAGGGAATCCCT-1_contig_1 True 550 TRB CASSVTRFSNAIYWRHSKPFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV14 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype13 clonotype13_consensus_1 +PatientA_Post CGATCAGGGAATCCCT-1 True CGATCAGGGAATCCCT-1_contig_2 True 500 TRA CAEKSPWLDQRRLWF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV35 TRAJ48 None TRAC True True 1100 3 clonotype13 clonotype13_consensus_2 +PatientA_Post AGGCCAGCGGATATCA-1 True AGGCCAGCGGATATCA-1_contig_1 True 550 TRB CASSSSRISSKTKWWF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV9 TRBJ1-5 TRBD1 TRBC1 True True 1200 4 clonotype9 clonotype9_consensus_1 +PatientA_Post AGGCCAGCGGATATCA-1 True AGGCCAGCGGATATCA-1_contig_2 True 500 TRA CADTHDWGYEHKFLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV38-1 TRAJ42 None TRAC True True 1100 3 clonotype9 clonotype9_consensus_2 +PatientA_Post CAATCCCTGTGGCACG-1 True CAATCCCTGTGGCACG-1_contig_1 True 550 TRB CASSTQYRKKAWIIFNFRTYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV20-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype7 clonotype7_consensus_1 +PatientA_Post CAATCCCTGTGGCACG-1 True CAATCCCTGTGGCACG-1_contig_2 True 500 TRA CAVNNETQKRAFPF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV17 TRAJ22 None TRAC True True 1100 3 clonotype7 clonotype7_consensus_2 +PatientA_Post GCATGGTGGGCTCCCC-1 True GCATGGTGGGCTCCCC-1_contig_1 True 550 TRB CASSGIIATGVRWPRWQWVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV28 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype10 clonotype10_consensus_1 +PatientA_Post GCATGGTGGGCTCCCC-1 True GCATGGTGGGCTCCCC-1_contig_2 True 500 TRA CAYKSEAVDRRVTSFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV26-1 TRAJ54 None TRAC True True 1100 3 clonotype10 clonotype10_consensus_2 +PatientA_Post TTATCTTAGGTTTAGT-1 True TTATCTTAGGTTTAGT-1_contig_1 True 550 TRB CASSKSWHIWKTVHKYSPDAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV29-1 TRBJ1-1 TRBD1 TRBC1 True True 1200 4 clonotype18 clonotype18_consensus_1 +PatientA_Post TTATCTTAGGTTTAGT-1 True TTATCTTAGGTTTAGT-1_contig_2 True 500 TRA CAARWWERHANSITAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV22 TRAJ34 None TRAC True True 1100 3 clonotype18 clonotype18_consensus_2 +PatientA_Post AGCACGCTGATCCGCT-1 True AGCACGCTGATCCGCT-1_contig_1 True 550 TRB CASSGIIATGVRWPRWQWVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV28 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype10 clonotype10_consensus_1 +PatientA_Post AGCACGCTGATCCGCT-1 True AGCACGCTGATCCGCT-1_contig_2 True 500 TRA CAYKSEAVDRRVTSFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV26-1 TRAJ54 None TRAC True True 1100 3 clonotype10 clonotype10_consensus_2 +PatientA_Post CGATGAGCAAAAGACC-1 True CGATGAGCAAAAGACC-1_contig_1 True 550 TRB CASSWLARPGNTKSPQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV10-3 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype20 clonotype20_consensus_1 +PatientA_Post CGATGAGCAAAAGACC-1 True CGATGAGCAAAAGACC-1_contig_2 True 500 TRA CADKWSWLNKYAKDSPEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV41 TRAJ54 None TRAC True True 1100 3 clonotype20 clonotype20_consensus_2 +PatientA_Post GACGCAGTCCGCGCTT-1 True GACGCAGTCCGCGCTT-1_contig_1 True 550 TRB CASSNRAIAPVNLDLTHDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV24-1 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype16 clonotype16_consensus_1 +PatientA_Post GACGCAGTCCGCGCTT-1 True GACGCAGTCCGCGCTT-1_contig_2 True 500 TRA CAYHWEEFNGAFPQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV5 TRAJ40 None TRAC True True 1100 3 clonotype16 clonotype16_consensus_2 +PatientA_Post CATGCAGCTAAGTTAT-1 True CATGCAGCTAAGTTAT-1_contig_1 True 550 TRB CASSWAWASEYGTVLATFNYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV11-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype11 clonotype11_consensus_1 +PatientA_Post CATGCAGCTAAGTTAT-1 True CATGCAGCTAAGTTAT-1_contig_2 True 500 TRA CAITWHWHDVSLGEIAIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV9-2 TRAJ13 None TRAC True True 1100 3 clonotype11 clonotype11_consensus_2 +PatientA_Post TATTAATCTTTCTCCA-1 True TATTAATCTTTCTCCA-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientA_Post TATTAATCTTTCTCCA-1 True TATTAATCTTTCTCCA-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientA_Post GGTTGGCTACGCTACC-1 True GGTTGGCTACGCTACC-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientA_Post GGTTGGCTACGCTACC-1 True GGTTGGCTACGCTACC-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientA_Post GTTCGGCAAACATATT-1 True GTTCGGCAAACATATT-1_contig_1 True 550 TRB CASSNRAIAPVNLDLTHDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV24-1 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype16 clonotype16_consensus_1 +PatientA_Post GTTCGGCAAACATATT-1 True GTTCGGCAAACATATT-1_contig_2 True 500 TRA CAYHWEEFNGAFPQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV5 TRAJ40 None TRAC True True 1100 3 clonotype16 clonotype16_consensus_2 +PatientA_Post GCTGCAAGATACTATT-1 True GCTGCAAGATACTATT-1_contig_1 True 550 TRB CASSHVYWLQRYENAVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV19 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype6 clonotype6_consensus_1 +PatientA_Post GCTGCAAGATACTATT-1 True GCTGCAAGATACTATT-1_contig_2 True 500 TRA CAEVISNVGVYYVVEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV1-2 TRAJ40 None TRAC True True 1100 3 clonotype6 clonotype6_consensus_2 +PatientA_Post AGGTGGCTGGACTTGG-1 True AGGTGGCTGGACTTGG-1_contig_1 True 550 TRB CASSYTITSHWHTIEEPPDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV27 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype5 clonotype5_consensus_1 +PatientA_Post AGGTGGCTGGACTTGG-1 True AGGTGGCTGGACTTGG-1_contig_2 True 500 TRA CASDRRANKQLVHFAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV27 TRAJ23 None TRAC True True 1100 3 clonotype5 clonotype5_consensus_2 +PatientA_Post GCTAGTGGCCGACTGC-1 True GCTAGTGGCCGACTGC-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientA_Post GCTAGTGGCCGACTGC-1 True GCTAGTGGCCGACTGC-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientA_Post TAAGTATTCCCGTTTC-1 True TAAGTATTCCCGTTTC-1_contig_1 True 550 TRB CASSHVYWLQRYENAVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV19 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype6 clonotype6_consensus_1 +PatientA_Post TAAGTATTCCCGTTTC-1 True TAAGTATTCCCGTTTC-1_contig_2 True 500 TRA CAEVISNVGVYYVVEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV1-2 TRAJ40 None TRAC True True 1100 3 clonotype6 clonotype6_consensus_2 +PatientA_Post AGTATACTGACACGTG-1 True AGTATACTGACACGTG-1_contig_1 True 550 TRB CASSNRAIAPVNLDLTHDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV24-1 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype16 clonotype16_consensus_1 +PatientA_Post AGTATACTGACACGTG-1 True AGTATACTGACACGTG-1_contig_2 True 500 TRA CAYHWEEFNGAFPQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV5 TRAJ40 None TRAC True True 1100 3 clonotype16 clonotype16_consensus_2 +PatientA_Post AATCCACCAGGGAAAA-1 True AATCCACCAGGGAAAA-1_contig_1 True 550 TRB CASSTQYRKKAWIIFNFRTYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV20-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype7 clonotype7_consensus_1 +PatientA_Post AATCCACCAGGGAAAA-1 True AATCCACCAGGGAAAA-1_contig_2 True 500 TRA CAVNNETQKRAFPF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV17 TRAJ22 None TRAC True True 1100 3 clonotype7 clonotype7_consensus_2 +PatientA_Post CCTTAGTAGCGGCCCG-1 True CCTTAGTAGCGGCCCG-1_contig_1 True 550 TRB CASSNFQKHDILKTSGSIYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV18 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype15 clonotype15_consensus_1 +PatientA_Post CCTTAGTAGCGGCCCG-1 True CCTTAGTAGCGGCCCG-1_contig_2 True 500 TRA CAQRKIAAIRQTSQGLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV24 TRAJ23 None TRAC True True 1100 3 clonotype15 clonotype15_consensus_2 +PatientA_Post GTATAGCCAGCATAGG-1 True GTATAGCCAGCATAGG-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientA_Post GTATAGCCAGCATAGG-1 True GTATAGCCAGCATAGG-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientA_Post GTAGTAAGGGAGGCCA-1 True GTAGTAAGGGAGGCCA-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientA_Post GTAGTAAGGGAGGCCA-1 True GTAGTAAGGGAGGCCA-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientA_Post TCAACGATCGTGCGGT-1 True TCAACGATCGTGCGGT-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Post TCAACGATCGTGCGGT-1 True TCAACGATCGTGCGGT-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Post GTGGAATCCCAGAGAG-1 True GTGGAATCCCAGAGAG-1_contig_1 True 550 TRB CASSHVYWLQRYENAVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV19 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype6 clonotype6_consensus_1 +PatientA_Post GTGGAATCCCAGAGAG-1 True GTGGAATCCCAGAGAG-1_contig_2 True 500 TRA CAEVISNVGVYYVVEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV1-2 TRAJ40 None TRAC True True 1100 3 clonotype6 clonotype6_consensus_2 +PatientA_Post GGTAGGAGGCTAAAGC-1 True GGTAGGAGGCTAAAGC-1_contig_1 True 550 TRB CASSYTITSHWHTIEEPPDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV27 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype5 clonotype5_consensus_1 +PatientA_Post GGTAGGAGGCTAAAGC-1 True GGTAGGAGGCTAAAGC-1_contig_2 True 500 TRA CASDRRANKQLVHFAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV27 TRAJ23 None TRAC True True 1100 3 clonotype5 clonotype5_consensus_2 +PatientA_Post TGTTGAGGGTAGGACT-1 True TGTTGAGGGTAGGACT-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientA_Post TGTTGAGGGTAGGACT-1 True TGTTGAGGGTAGGACT-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientA_Post GACAGTGTGAGCGCGA-1 True GACAGTGTGAGCGCGA-1_contig_1 True 550 TRB CASSTQYRKKAWIIFNFRTYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV20-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype7 clonotype7_consensus_1 +PatientA_Post GACAGTGTGAGCGCGA-1 True GACAGTGTGAGCGCGA-1_contig_2 True 500 TRA CAVNNETQKRAFPF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV17 TRAJ22 None TRAC True True 1100 3 clonotype7 clonotype7_consensus_2 +PatientA_Post AGGTCGTGTCTTCGTT-1 True AGGTCGTGTCTTCGTT-1_contig_1 True 550 TRB CASSDQQGHFFYANRQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV12-3 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype12 clonotype12_consensus_1 +PatientA_Post AGGTCGTGTCTTCGTT-1 True AGGTCGTGTCTTCGTT-1_contig_2 True 500 TRA CAHSTKLGHHHFAEESRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV14/DV4 TRAJ6 None TRAC True True 1100 3 clonotype12 clonotype12_consensus_2 +PatientA_Post AGGCTGACGCACTATA-1 True AGGCTGACGCACTATA-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Post AGGCTGACGCACTATA-1 True AGGCTGACGCACTATA-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Post CCACGTCATACGCTAT-1 True CCACGTCATACGCTAT-1_contig_1 True 550 TRB CASSGIIATGVRWPRWQWVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV28 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype10 clonotype10_consensus_1 +PatientA_Post CCACGTCATACGCTAT-1 True CCACGTCATACGCTAT-1_contig_2 True 500 TRA CAYKSEAVDRRVTSFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV26-1 TRAJ54 None TRAC True True 1100 3 clonotype10 clonotype10_consensus_2 +PatientA_Post CATGAATCTTCCTTGC-1 True CATGAATCTTCCTTGC-1_contig_1 True 550 TRB CASSGIIATGVRWPRWQWVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV28 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype10 clonotype10_consensus_1 +PatientA_Post CATGAATCTTCCTTGC-1 True CATGAATCTTCCTTGC-1_contig_2 True 500 TRA CAYKSEAVDRRVTSFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV26-1 TRAJ54 None TRAC True True 1100 3 clonotype10 clonotype10_consensus_2 +PatientA_Post TGTTGGGCTAAGCGGG-1 True TGTTGGGCTAAGCGGG-1_contig_1 True 550 TRB CASSDWTHQRNKQQRPKWLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV15 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype14 clonotype14_consensus_1 +PatientA_Post TGTTGGGCTAAGCGGG-1 True TGTTGGGCTAAGCGGG-1_contig_2 True 500 TRA CADWPVAWPSEGVSF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV8-1 TRAJ33 None TRAC True True 1100 3 clonotype14 clonotype14_consensus_2 +PatientA_Post GATCGCTTACGAGGTT-1 True GATCGCTTACGAGGTT-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientA_Post GATCGCTTACGAGGTT-1 True GATCGCTTACGAGGTT-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientA_Post AAATCCCGCTCTTACA-1 True AAATCCCGCTCTTACA-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientA_Post AAATCCCGCTCTTACA-1 True AAATCCCGCTCTTACA-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientA_Post GGGAAGAGAGAGTCTT-1 True GGGAAGAGAGAGTCTT-1_contig_1 True 550 TRB CASSNFQKHDILKTSGSIYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV18 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype15 clonotype15_consensus_1 +PatientA_Post GGGAAGAGAGAGTCTT-1 True GGGAAGAGAGAGTCTT-1_contig_2 True 500 TRA CAQRKIAAIRQTSQGLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV24 TRAJ23 None TRAC True True 1100 3 clonotype15 clonotype15_consensus_2 +PatientA_Post CCTTACTCGAGACCCA-1 True CCTTACTCGAGACCCA-1_contig_1 True 550 TRB CASSSSRISSKTKWWF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV9 TRBJ1-5 TRBD1 TRBC1 True True 1200 4 clonotype9 clonotype9_consensus_1 +PatientA_Post CCTTACTCGAGACCCA-1 True CCTTACTCGAGACCCA-1_contig_2 True 500 TRA CADTHDWGYEHKFLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV38-1 TRAJ42 None TRAC True True 1100 3 clonotype9 clonotype9_consensus_2 +PatientA_Post TAGTACTATGATTTCG-1 True TAGTACTATGATTTCG-1_contig_1 True 550 TRB CASSWHSNNNFSKLQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-1 TRBJ1-1 TRBD1 TRBC1 True True 1200 4 clonotype8 clonotype8_consensus_1 +PatientA_Post TAGTACTATGATTTCG-1 True TAGTACTATGATTTCG-1_contig_2 True 500 TRA CADRKEGVYEGQYRVDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV19 TRAJ34 None TRAC True True 1100 3 clonotype8 clonotype8_consensus_2 +PatientA_Post GGCAAAGAAGACGGGA-1 True GGCAAAGAAGACGGGA-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientA_Post GGCAAAGAAGACGGGA-1 True GGCAAAGAAGACGGGA-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientA_Post CCACTAATATTTGACG-1 True CCACTAATATTTGACG-1_contig_1 True 550 TRB CASSEPPHVTPSHGWHNNSGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-5 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype3 clonotype3_consensus_1 +PatientA_Post CCACTAATATTTGACG-1 True CCACTAATATTTGACG-1_contig_2 True 500 TRA CANARVKVFWFEPNKYLYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV21 TRAJ48 None TRAC True True 1100 3 clonotype3 clonotype3_consensus_2 +PatientA_Post GCTCCAATCTCATGAT-1 True GCTCCAATCTCATGAT-1_contig_1 True 550 TRB CASSDQQGHFFYANRQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV12-3 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype12 clonotype12_consensus_1 +PatientA_Post GCTCCAATCTCATGAT-1 True GCTCCAATCTCATGAT-1_contig_2 True 500 TRA CAHSTKLGHHHFAEESRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV14/DV4 TRAJ6 None TRAC True True 1100 3 clonotype12 clonotype12_consensus_2 +PatientA_Post CGTCCGGGGCATGCAG-1 True CGTCCGGGGCATGCAG-1_contig_1 True 550 TRB CASSTQYRKKAWIIFNFRTYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV20-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype7 clonotype7_consensus_1 +PatientA_Post CGTCCGGGGCATGCAG-1 True CGTCCGGGGCATGCAG-1_contig_2 True 500 TRA CAVNNETQKRAFPF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV17 TRAJ22 None TRAC True True 1100 3 clonotype7 clonotype7_consensus_2 +PatientA_Post CCCTACATCGTCCCAG-1 True CCCTACATCGTCCCAG-1_contig_1 True 550 TRB CASSNFQKHDILKTSGSIYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV18 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype15 clonotype15_consensus_1 +PatientA_Post CCCTACATCGTCCCAG-1 True CCCTACATCGTCCCAG-1_contig_2 True 500 TRA CAQRKIAAIRQTSQGLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV24 TRAJ23 None TRAC True True 1100 3 clonotype15 clonotype15_consensus_2 +PatientA_Post CCCGGCTAGTAACGGT-1 True CCCGGCTAGTAACGGT-1_contig_1 True 550 TRB CASSWHSNNNFSKLQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-1 TRBJ1-1 TRBD1 TRBC1 True True 1200 4 clonotype8 clonotype8_consensus_1 +PatientA_Post CCCGGCTAGTAACGGT-1 True CCCGGCTAGTAACGGT-1_contig_2 True 500 TRA CADRKEGVYEGQYRVDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV19 TRAJ34 None TRAC True True 1100 3 clonotype8 clonotype8_consensus_2 +PatientA_Post AGATTATCGATGTAGC-1 True AGATTATCGATGTAGC-1_contig_1 True 550 TRB CASSEPPHVTPSHGWHNNSGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-5 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype3 clonotype3_consensus_1 +PatientA_Post AGATTATCGATGTAGC-1 True AGATTATCGATGTAGC-1_contig_2 True 500 TRA CANARVKVFWFEPNKYLYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV21 TRAJ48 None TRAC True True 1100 3 clonotype3 clonotype3_consensus_2 +PatientA_Post GTCAAGGCCACCCATC-1 True GTCAAGGCCACCCATC-1_contig_1 True 550 TRB CASSDQQGHFFYANRQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV12-3 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype12 clonotype12_consensus_1 +PatientA_Post GTCAAGGCCACCCATC-1 True GTCAAGGCCACCCATC-1_contig_2 True 500 TRA CAHSTKLGHHHFAEESRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV14/DV4 TRAJ6 None TRAC True True 1100 3 clonotype12 clonotype12_consensus_2 +PatientA_Post GCTTGACGTAACAGGC-1 True GCTTGACGTAACAGGC-1_contig_1 True 550 TRB CASSSSRISSKTKWWF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV9 TRBJ1-5 TRBD1 TRBC1 True True 1200 4 clonotype9 clonotype9_consensus_1 +PatientA_Post GCTTGACGTAACAGGC-1 True GCTTGACGTAACAGGC-1_contig_2 True 500 TRA CADTHDWGYEHKFLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV38-1 TRAJ42 None TRAC True True 1100 3 clonotype9 clonotype9_consensus_2 +PatientA_Post CTCGTTCAAAGCCGAA-1 True CTCGTTCAAAGCCGAA-1_contig_1 True 550 TRB CASSHVYWLQRYENAVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV19 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype6 clonotype6_consensus_1 +PatientA_Post CTCGTTCAAAGCCGAA-1 True CTCGTTCAAAGCCGAA-1_contig_2 True 500 TRA CAEVISNVGVYYVVEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV1-2 TRAJ40 None TRAC True True 1100 3 clonotype6 clonotype6_consensus_2 +PatientA_Post TCAACTTGGAAGTCTC-1 True TCAACTTGGAAGTCTC-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientA_Post TCAACTTGGAAGTCTC-1 True TCAACTTGGAAGTCTC-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientA_Post ACAGGTTCATGCTGCA-1 True ACAGGTTCATGCTGCA-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Post ACAGGTTCATGCTGCA-1 True ACAGGTTCATGCTGCA-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Post GGCTGGGTGCCGCTTA-1 True GGCTGGGTGCCGCTTA-1_contig_1 True 550 TRB CASSGIIATGVRWPRWQWVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV28 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype10 clonotype10_consensus_1 +PatientA_Post GGCTGGGTGCCGCTTA-1 True GGCTGGGTGCCGCTTA-1_contig_2 True 500 TRA CAYKSEAVDRRVTSFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV26-1 TRAJ54 None TRAC True True 1100 3 clonotype10 clonotype10_consensus_2 +PatientA_Post CACACGCTAAGACAAA-1 True CACACGCTAAGACAAA-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientA_Post CACACGCTAAGACAAA-1 True CACACGCTAAGACAAA-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientA_Post GACTTGCGCCAAAACG-1 True GACTTGCGCCAAAACG-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientA_Post GACTTGCGCCAAAACG-1 True GACTTGCGCCAAAACG-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientA_Post CTGGTGCTAAAAGAGC-1 True CTGGTGCTAAAAGAGC-1_contig_1 True 550 TRB CASSEPPHVTPSHGWHNNSGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-5 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype3 clonotype3_consensus_1 +PatientA_Post CTGGTGCTAAAAGAGC-1 True CTGGTGCTAAAAGAGC-1_contig_2 True 500 TRA CANARVKVFWFEPNKYLYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV21 TRAJ48 None TRAC True True 1100 3 clonotype3 clonotype3_consensus_2 +PatientA_Post TTGTGATGCGTACGAG-1 True TTGTGATGCGTACGAG-1_contig_1 True 550 TRB CASSKSWHIWKTVHKYSPDAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV29-1 TRBJ1-1 TRBD1 TRBC1 True True 1200 4 clonotype18 clonotype18_consensus_1 +PatientA_Post TTGTGATGCGTACGAG-1 True TTGTGATGCGTACGAG-1_contig_2 True 500 TRA CAARWWERHANSITAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV22 TRAJ34 None TRAC True True 1100 3 clonotype18 clonotype18_consensus_2 +PatientA_Post GCCGCGCAAATTTCAG-1 True GCCGCGCAAATTTCAG-1_contig_1 True 550 TRB CASSSSRISSKTKWWF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV9 TRBJ1-5 TRBD1 TRBC1 True True 1200 4 clonotype9 clonotype9_consensus_1 +PatientA_Post GCCGCGCAAATTTCAG-1 True GCCGCGCAAATTTCAG-1_contig_2 True 500 TRA CADTHDWGYEHKFLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV38-1 TRAJ42 None TRAC True True 1100 3 clonotype9 clonotype9_consensus_2 +PatientA_Post TTAGACACGATGACCA-1 True TTAGACACGATGACCA-1_contig_1 True 550 TRB CASSLNLKDSNQRNTIKFAQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV30 TRBJ1-5 TRBD1 TRBC1 True True 1200 4 clonotype19 clonotype19_consensus_1 +PatientA_Post TTAGACACGATGACCA-1 True TTAGACACGATGACCA-1_contig_2 True 500 TRA CAHPFDIKGDTNTHQGGQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV30 TRAJ42 None TRAC True True 1100 3 clonotype19 clonotype19_consensus_2 +PatientA_Post CGAAATACCGGGGAGG-1 True CGAAATACCGGGGAGG-1_contig_1 True 550 TRB CASSYTITSHWHTIEEPPDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV27 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype5 clonotype5_consensus_1 +PatientA_Post CGAAATACCGGGGAGG-1 True CGAAATACCGGGGAGG-1_contig_2 True 500 TRA CASDRRANKQLVHFAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV27 TRAJ23 None TRAC True True 1100 3 clonotype5 clonotype5_consensus_2 +PatientA_Post TTTAGCTTGTTTGCTT-1 True TTTAGCTTGTTTGCTT-1_contig_1 True 550 TRB CASSDQQGHFFYANRQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV12-3 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype12 clonotype12_consensus_1 +PatientA_Post TTTAGCTTGTTTGCTT-1 True TTTAGCTTGTTTGCTT-1_contig_2 True 500 TRA CAHSTKLGHHHFAEESRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV14/DV4 TRAJ6 None TRAC True True 1100 3 clonotype12 clonotype12_consensus_2 +PatientA_Post AATCCCGCCAAGTAAA-1 True AATCCCGCCAAGTAAA-1_contig_1 True 550 TRB CASSEPPHVTPSHGWHNNSGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-5 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype3 clonotype3_consensus_1 +PatientA_Post AATCCCGCCAAGTAAA-1 True AATCCCGCCAAGTAAA-1_contig_2 True 500 TRA CANARVKVFWFEPNKYLYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV21 TRAJ48 None TRAC True True 1100 3 clonotype3 clonotype3_consensus_2 +PatientA_Post TAGCCCAGTCGGACGC-1 True TAGCCCAGTCGGACGC-1_contig_1 True 550 TRB CASSHVYWLQRYENAVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV19 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype6 clonotype6_consensus_1 +PatientA_Post TAGCCCAGTCGGACGC-1 True TAGCCCAGTCGGACGC-1_contig_2 True 500 TRA CAEVISNVGVYYVVEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV1-2 TRAJ40 None TRAC True True 1100 3 clonotype6 clonotype6_consensus_2 +PatientA_Post GGCTTCCGCTAGATAA-1 True GGCTTCCGCTAGATAA-1_contig_1 True 550 TRB CASSEPPHVTPSHGWHNNSGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-5 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype3 clonotype3_consensus_1 +PatientA_Post GGCTTCCGCTAGATAA-1 True GGCTTCCGCTAGATAA-1_contig_2 True 500 TRA CANARVKVFWFEPNKYLYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV21 TRAJ48 None TRAC True True 1100 3 clonotype3 clonotype3_consensus_2 +PatientB_Base TGGATGGATACTATAT-1 True TGGATGGATACTATAT-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientB_Base TGGATGGATACTATAT-1 True TGGATGGATACTATAT-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientB_Base TTCCAAAACAGCACTG-1 True TTCCAAAACAGCACTG-1_contig_1 True 550 TRB CASSWAWASEYGTVLATFNYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV11-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype11 clonotype11_consensus_1 +PatientB_Base TTCCAAAACAGCACTG-1 True TTCCAAAACAGCACTG-1_contig_2 True 500 TRA CAITWHWHDVSLGEIAIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV9-2 TRAJ13 None TRAC True True 1100 3 clonotype11 clonotype11_consensus_2 +PatientB_Base TATTTGACGCCGCTAT-1 True TATTTGACGCCGCTAT-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientB_Base TATTTGACGCCGCTAT-1 True TATTTGACGCCGCTAT-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientB_Base GGCCCTCGTGGGTGAC-1 True GGCCCTCGTGGGTGAC-1_contig_1 True 550 TRB CASSYTITSHWHTIEEPPDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV27 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype5 clonotype5_consensus_1 +PatientB_Base GGCCCTCGTGGGTGAC-1 True GGCCCTCGTGGGTGAC-1_contig_2 True 500 TRA CASDRRANKQLVHFAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV27 TRAJ23 None TRAC True True 1100 3 clonotype5 clonotype5_consensus_2 +PatientB_Base ATGCTGGGTTAGCTAG-1 True ATGCTGGGTTAGCTAG-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientB_Base ATGCTGGGTTAGCTAG-1 True ATGCTGGGTTAGCTAG-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientB_Base CCGGGCAACAACTCTT-1 True CCGGGCAACAACTCTT-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientB_Base CCGGGCAACAACTCTT-1 True CCGGGCAACAACTCTT-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientB_Base AGCGTTGAGTCGGGCT-1 True AGCGTTGAGTCGGGCT-1_contig_1 True 550 TRB CASSWHSNNNFSKLQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-1 TRBJ1-1 TRBD1 TRBC1 True True 1200 4 clonotype8 clonotype8_consensus_1 +PatientB_Base AGCGTTGAGTCGGGCT-1 True AGCGTTGAGTCGGGCT-1_contig_2 True 500 TRA CADRKEGVYEGQYRVDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV19 TRAJ34 None TRAC True True 1100 3 clonotype8 clonotype8_consensus_2 +PatientB_Base CAGAGTTGCACCCGTT-1 True CAGAGTTGCACCCGTT-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientB_Base CAGAGTTGCACCCGTT-1 True CAGAGTTGCACCCGTT-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientB_Base GGCTGGGTTAAATATA-1 True GGCTGGGTTAAATATA-1_contig_1 True 550 TRB CASSEPPHVTPSHGWHNNSGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-5 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype3 clonotype3_consensus_1 +PatientB_Base GGCTGGGTTAAATATA-1 True GGCTGGGTTAAATATA-1_contig_2 True 500 TRA CANARVKVFWFEPNKYLYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV21 TRAJ48 None TRAC True True 1100 3 clonotype3 clonotype3_consensus_2 +PatientB_Base TCACACGGCAAGCGAT-1 True TCACACGGCAAGCGAT-1_contig_1 True 550 TRB CASSWLARPGNTKSPQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV10-3 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype20 clonotype20_consensus_1 +PatientB_Base TCACACGGCAAGCGAT-1 True TCACACGGCAAGCGAT-1_contig_2 True 500 TRA CADKWSWLNKYAKDSPEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV41 TRAJ54 None TRAC True True 1100 3 clonotype20 clonotype20_consensus_2 +PatientB_Base GCGTCATGTAATAGGT-1 True GCGTCATGTAATAGGT-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientB_Base GCGTCATGTAATAGGT-1 True GCGTCATGTAATAGGT-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientB_Base GCGGAACTCCAGATTG-1 True GCGGAACTCCAGATTG-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientB_Base GCGGAACTCCAGATTG-1 True GCGGAACTCCAGATTG-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientB_Base GCGATGTGCAATTACT-1 True GCGATGTGCAATTACT-1_contig_1 True 550 TRB CASSDWTHQRNKQQRPKWLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV15 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype14 clonotype14_consensus_1 +PatientB_Base GCGATGTGCAATTACT-1 True GCGATGTGCAATTACT-1_contig_2 True 500 TRA CADWPVAWPSEGVSF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV8-1 TRAJ33 None TRAC True True 1100 3 clonotype14 clonotype14_consensus_2 +PatientB_Base GTGCGACTTGTCATAT-1 True GTGCGACTTGTCATAT-1_contig_1 True 550 TRB CASSTQYRKKAWIIFNFRTYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV20-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype7 clonotype7_consensus_1 +PatientB_Base GTGCGACTTGTCATAT-1 True GTGCGACTTGTCATAT-1_contig_2 True 500 TRA CAVNNETQKRAFPF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV17 TRAJ22 None TRAC True True 1100 3 clonotype7 clonotype7_consensus_2 +PatientB_Base CTACAGGGGACTCCAG-1 True CTACAGGGGACTCCAG-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientB_Base CTACAGGGGACTCCAG-1 True CTACAGGGGACTCCAG-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientB_Base AATATCACGTACTTCC-1 True AATATCACGTACTTCC-1_contig_1 True 550 TRB CASSKSWHIWKTVHKYSPDAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV29-1 TRBJ1-1 TRBD1 TRBC1 True True 1200 4 clonotype18 clonotype18_consensus_1 +PatientB_Base AATATCACGTACTTCC-1 True AATATCACGTACTTCC-1_contig_2 True 500 TRA CAARWWERHANSITAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV22 TRAJ34 None TRAC True True 1100 3 clonotype18 clonotype18_consensus_2 +PatientB_Base CAATACCCAATATCTT-1 True CAATACCCAATATCTT-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientB_Base CAATACCCAATATCTT-1 True CAATACCCAATATCTT-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientB_Base GCAACCAAGCGTTTAA-1 True GCAACCAAGCGTTTAA-1_contig_1 True 550 TRB CASSDQQGHFFYANRQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV12-3 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype12 clonotype12_consensus_1 +PatientB_Base GCAACCAAGCGTTTAA-1 True GCAACCAAGCGTTTAA-1_contig_2 True 500 TRA CAHSTKLGHHHFAEESRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV14/DV4 TRAJ6 None TRAC True True 1100 3 clonotype12 clonotype12_consensus_2 +PatientB_Base TCACCTGTTAAATTGT-1 True TCACCTGTTAAATTGT-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientB_Base TCACCTGTTAAATTGT-1 True TCACCTGTTAAATTGT-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientB_Base GTAGAGTAGTAAGCAG-1 True GTAGAGTAGTAAGCAG-1_contig_1 True 550 TRB CASSYTITSHWHTIEEPPDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV27 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype5 clonotype5_consensus_1 +PatientB_Base GTAGAGTAGTAAGCAG-1 True GTAGAGTAGTAAGCAG-1_contig_2 True 500 TRA CASDRRANKQLVHFAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV27 TRAJ23 None TRAC True True 1100 3 clonotype5 clonotype5_consensus_2 +PatientB_Base ATTACTGCGGAGAGAG-1 True ATTACTGCGGAGAGAG-1_contig_1 True 550 TRB CASSEPPHVTPSHGWHNNSGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-5 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype3 clonotype3_consensus_1 +PatientB_Base ATTACTGCGGAGAGAG-1 True ATTACTGCGGAGAGAG-1_contig_2 True 500 TRA CANARVKVFWFEPNKYLYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV21 TRAJ48 None TRAC True True 1100 3 clonotype3 clonotype3_consensus_2 +PatientB_Base AGCGCGAATCTATACT-1 True AGCGCGAATCTATACT-1_contig_1 True 550 TRB CASSGIIATGVRWPRWQWVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV28 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype10 clonotype10_consensus_1 +PatientB_Base AGCGCGAATCTATACT-1 True AGCGCGAATCTATACT-1_contig_2 True 500 TRA CAYKSEAVDRRVTSFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV26-1 TRAJ54 None TRAC True True 1100 3 clonotype10 clonotype10_consensus_2 +PatientB_Base ACGCTCCGTCTGGTTA-1 True ACGCTCCGTCTGGTTA-1_contig_1 True 550 TRB CASSDQQGHFFYANRQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV12-3 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype12 clonotype12_consensus_1 +PatientB_Base ACGCTCCGTCTGGTTA-1 True ACGCTCCGTCTGGTTA-1_contig_2 True 500 TRA CAHSTKLGHHHFAEESRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV14/DV4 TRAJ6 None TRAC True True 1100 3 clonotype12 clonotype12_consensus_2 +PatientB_Base CCGTTGCCTGGTAAGA-1 True CCGTTGCCTGGTAAGA-1_contig_1 True 550 TRB CASSSGAFRQKDHLKQFIGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV25-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype17 clonotype17_consensus_1 +PatientB_Base CCGTTGCCTGGTAAGA-1 True CCGTTGCCTGGTAAGA-1_contig_2 True 500 TRA CARNIQNLWRQHRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV20 TRAJ22 None TRAC True True 1100 3 clonotype17 clonotype17_consensus_2 +PatientB_Base ATGACTACGGCCTAGA-1 True ATGACTACGGCCTAGA-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientB_Base ATGACTACGGCCTAGA-1 True ATGACTACGGCCTAGA-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientB_Base ATGGGTCCACACCCTG-1 True ATGGGTCCACACCCTG-1_contig_1 True 550 TRB CASSWHSNNNFSKLQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV6-1 TRBJ1-1 TRBD1 TRBC1 True True 1200 4 clonotype8 clonotype8_consensus_1 +PatientB_Base ATGGGTCCACACCCTG-1 True ATGGGTCCACACCCTG-1_contig_2 True 500 TRA CADRKEGVYEGQYRVDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV19 TRAJ34 None TRAC True True 1100 3 clonotype8 clonotype8_consensus_2 +PatientB_Base CCTCCTAAAATTGCAC-1 True CCTCCTAAAATTGCAC-1_contig_1 True 550 TRB CASSYTITSHWHTIEEPPDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV27 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype5 clonotype5_consensus_1 +PatientB_Base CCTCCTAAAATTGCAC-1 True CCTCCTAAAATTGCAC-1_contig_2 True 500 TRA CASDRRANKQLVHFAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV27 TRAJ23 None TRAC True True 1100 3 clonotype5 clonotype5_consensus_2 +PatientB_Base TGAGCAGCAGAGCAAC-1 True TGAGCAGCAGAGCAAC-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientB_Base TGAGCAGCAGAGCAAC-1 True TGAGCAGCAGAGCAAC-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientB_Base GAGTGACAAAGCTGGG-1 True GAGTGACAAAGCTGGG-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientB_Base GAGTGACAAAGCTGGG-1 True GAGTGACAAAGCTGGG-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientB_Base AGCCCCCAATAGAAAG-1 True AGCCCCCAATAGAAAG-1_contig_1 True 550 TRB CASSNFQKHDILKTSGSIYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV18 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype15 clonotype15_consensus_1 +PatientB_Base AGCCCCCAATAGAAAG-1 True AGCCCCCAATAGAAAG-1_contig_2 True 500 TRA CAQRKIAAIRQTSQGLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV24 TRAJ23 None TRAC True True 1100 3 clonotype15 clonotype15_consensus_2 +PatientB_Base TACTGGATTTCTATGC-1 True TACTGGATTTCTATGC-1_contig_1 True 550 TRB CASSNFQKHDILKTSGSIYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV18 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype15 clonotype15_consensus_1 +PatientB_Base TACTGGATTTCTATGC-1 True TACTGGATTTCTATGC-1_contig_2 True 500 TRA CAQRKIAAIRQTSQGLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV24 TRAJ23 None TRAC True True 1100 3 clonotype15 clonotype15_consensus_2 +PatientB_Base CACATGGGACCGGCCA-1 True CACATGGGACCGGCCA-1_contig_1 True 550 TRB CASSSGAFRQKDHLKQFIGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV25-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype17 clonotype17_consensus_1 +PatientB_Base CACATGGGACCGGCCA-1 True CACATGGGACCGGCCA-1_contig_2 True 500 TRA CARNIQNLWRQHRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV20 TRAJ22 None TRAC True True 1100 3 clonotype17 clonotype17_consensus_2 +PatientB_Base ATACATCATTCTGGAA-1 True ATACATCATTCTGGAA-1_contig_1 True 550 TRB CASSDQQGHFFYANRQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV12-3 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype12 clonotype12_consensus_1 +PatientB_Base ATACATCATTCTGGAA-1 True ATACATCATTCTGGAA-1_contig_2 True 500 TRA CAHSTKLGHHHFAEESRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV14/DV4 TRAJ6 None TRAC True True 1100 3 clonotype12 clonotype12_consensus_2 +PatientB_Base TGTTCCAATGTTGCTT-1 True TGTTCCAATGTTGCTT-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientB_Base TGTTCCAATGTTGCTT-1 True TGTTCCAATGTTGCTT-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientB_Base CGGGATCATCTATCTT-1 True CGGGATCATCTATCTT-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientB_Base CGGGATCATCTATCTT-1 True CGGGATCATCTATCTT-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientB_Base GGGCGGATAACATACG-1 True GGGCGGATAACATACG-1_contig_1 True 550 TRB CASSWAWASEYGTVLATFNYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV11-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype11 clonotype11_consensus_1 +PatientB_Base GGGCGGATAACATACG-1 True GGGCGGATAACATACG-1_contig_2 True 500 TRA CAITWHWHDVSLGEIAIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV9-2 TRAJ13 None TRAC True True 1100 3 clonotype11 clonotype11_consensus_2 +PatientB_Base GGTACGTGCCTTGGCC-1 True GGTACGTGCCTTGGCC-1_contig_1 True 550 TRB CASSDQQGHFFYANRQF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV12-3 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype12 clonotype12_consensus_1 +PatientB_Base GGTACGTGCCTTGGCC-1 True GGTACGTGCCTTGGCC-1_contig_2 True 500 TRA CAHSTKLGHHHFAEESRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV14/DV4 TRAJ6 None TRAC True True 1100 3 clonotype12 clonotype12_consensus_2 +PatientB_Base GTCCTGCTCACGTGCT-1 True GTCCTGCTCACGTGCT-1_contig_1 True 550 TRB CASSTQYRKKAWIIFNFRTYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV20-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype7 clonotype7_consensus_1 +PatientB_Base GTCCTGCTCACGTGCT-1 True GTCCTGCTCACGTGCT-1_contig_2 True 500 TRA CAVNNETQKRAFPF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV17 TRAJ22 None TRAC True True 1100 3 clonotype7 clonotype7_consensus_2 +PatientB_Base CGACGCTTACCGACAT-1 True CGACGCTTACCGACAT-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientB_Base CGACGCTTACCGACAT-1 True CGACGCTTACCGACAT-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientB_Base AGCTAAGACGTGCGCT-1 True AGCTAAGACGTGCGCT-1_contig_1 True 550 TRB CASSWAWASEYGTVLATFNYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV11-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype11 clonotype11_consensus_1 +PatientB_Base AGCTAAGACGTGCGCT-1 True AGCTAAGACGTGCGCT-1_contig_2 True 500 TRA CAITWHWHDVSLGEIAIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV9-2 TRAJ13 None TRAC True True 1100 3 clonotype11 clonotype11_consensus_2 +PatientB_Base GAGGTTACTACGTCTG-1 True GAGGTTACTACGTCTG-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientB_Base GAGGTTACTACGTCTG-1 True GAGGTTACTACGTCTG-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientB_Base AAATTCCCCAGCGTTA-1 True AAATTCCCCAGCGTTA-1_contig_1 True 550 TRB CASSGIIATGVRWPRWQWVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV28 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype10 clonotype10_consensus_1 +PatientB_Base AAATTCCCCAGCGTTA-1 True AAATTCCCCAGCGTTA-1_contig_2 True 500 TRA CAYKSEAVDRRVTSFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV26-1 TRAJ54 None TRAC True True 1100 3 clonotype10 clonotype10_consensus_2 +PatientB_Base ACAATTAAGACCGTTA-1 True ACAATTAAGACCGTTA-1_contig_1 True 550 TRB CASSVTRFSNAIYWRHSKPFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV14 TRBJ2-3 TRBD1 TRBC1 True True 1200 4 clonotype13 clonotype13_consensus_1 +PatientB_Base ACAATTAAGACCGTTA-1 True ACAATTAAGACCGTTA-1_contig_2 True 500 TRA CAEKSPWLDQRRLWF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV35 TRAJ48 None TRAC True True 1100 3 clonotype13 clonotype13_consensus_2 +PatientB_Base TGCCACTAGTGTAGCT-1 True TGCCACTAGTGTAGCT-1_contig_1 True 550 TRB CASSHVYWLQRYENAVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV19 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype6 clonotype6_consensus_1 +PatientB_Base TGCCACTAGTGTAGCT-1 True TGCCACTAGTGTAGCT-1_contig_2 True 500 TRA CAEVISNVGVYYVVEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV1-2 TRAJ40 None TRAC True True 1100 3 clonotype6 clonotype6_consensus_2 +PatientB_Base TACTGATAATGACAGG-1 True TACTGATAATGACAGG-1_contig_1 True 550 TRB CASSTQYRKKAWIIFNFRTYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV20-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype7 clonotype7_consensus_1 +PatientB_Base TACTGATAATGACAGG-1 True TACTGATAATGACAGG-1_contig_2 True 500 TRA CAVNNETQKRAFPF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV17 TRAJ22 None TRAC True True 1100 3 clonotype7 clonotype7_consensus_2 +PatientB_Base TACTAAAAAGTGCAAT-1 True TACTAAAAAGTGCAAT-1_contig_1 True 550 TRB CASSSSRISSKTKWWF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV9 TRBJ1-5 TRBD1 TRBC1 True True 1200 4 clonotype9 clonotype9_consensus_1 +PatientB_Base TACTAAAAAGTGCAAT-1 True TACTAAAAAGTGCAAT-1_contig_2 True 500 TRA CADTHDWGYEHKFLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV38-1 TRAJ42 None TRAC True True 1100 3 clonotype9 clonotype9_consensus_2 +PatientB_Base ACCATCACGATCTATC-1 True ACCATCACGATCTATC-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientB_Base ACCATCACGATCTATC-1 True ACCATCACGATCTATC-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientB_Base ATAGGTGTCATTGTAA-1 True ATAGGTGTCATTGTAA-1_contig_1 True 550 TRB CASSHFGQPTVTSEGAKRF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV5-1 TRBJ1-2 TRBD1 TRBC1 True True 1200 4 clonotype2 clonotype2_consensus_1 +PatientB_Base ATAGGTGTCATTGTAA-1 True ATAGGTGTCATTGTAA-1_contig_2 True 500 TRA CALKIRSYIFWNKVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV13-1 TRAJ6 None TRAC True True 1100 3 clonotype2 clonotype2_consensus_2 +PatientB_Base AATCACGTCTATAAGA-1 True AATCACGTCTATAAGA-1_contig_1 True 550 TRB CASSGIIATGVRWPRWQWVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV28 TRBJ2-6 TRBD1 TRBC1 True True 1200 4 clonotype10 clonotype10_consensus_1 +PatientB_Base AATCACGTCTATAAGA-1 True AATCACGTCTATAAGA-1_contig_2 True 500 TRA CAYKSEAVDRRVTSFF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV26-1 TRAJ54 None TRAC True True 1100 3 clonotype10 clonotype10_consensus_2 +PatientB_Base CCTTTTCTGCTCTTGT-1 True CCTTTTCTGCTCTTGT-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientB_Base CCTTTTCTGCTCTTGT-1 True CCTTTTCTGCTCTTGT-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientB_Base ACGGGTGCGATTAATA-1 True ACGGGTGCGATTAATA-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientB_Base ACGGGTGCGATTAATA-1 True ACGGGTGCGATTAATA-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientB_Base CGTCTTCGGCTTAAAA-1 True CGTCTTCGGCTTAAAA-1_contig_1 True 550 TRB CASSWAWASEYGTVLATFNYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV11-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype11 clonotype11_consensus_1 +PatientB_Base CGTCTTCGGCTTAAAA-1 True CGTCTTCGGCTTAAAA-1_contig_2 True 500 TRA CAITWHWHDVSLGEIAIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV9-2 TRAJ13 None TRAC True True 1100 3 clonotype11 clonotype11_consensus_2 +PatientB_Base CCGCATCACAGCGTTG-1 True CCGCATCACAGCGTTG-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientB_Base CCGCATCACAGCGTTG-1 True CCGCATCACAGCGTTG-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientB_Base TTCTCCAAGATACAGA-1 True TTCTCCAAGATACAGA-1_contig_1 True 550 TRB CASSTQYRKKAWIIFNFRTYF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV20-1 TRBJ2-5 TRBD1 TRBC1 True True 1200 4 clonotype7 clonotype7_consensus_1 +PatientB_Base TTCTCCAAGATACAGA-1 True TTCTCCAAGATACAGA-1_contig_2 True 500 TRA CAVNNETQKRAFPF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV17 TRAJ22 None TRAC True True 1100 3 clonotype7 clonotype7_consensus_2 +PatientB_Base AGGTACCCTTCACTAT-1 True AGGTACCCTTCACTAT-1_contig_1 True 550 TRB CASSDWTHQRNKQQRPKWLF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV15 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype14 clonotype14_consensus_1 +PatientB_Base AGGTACCCTTCACTAT-1 True AGGTACCCTTCACTAT-1_contig_2 True 500 TRA CADWPVAWPSEGVSF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV8-1 TRAJ33 None TRAC True True 1100 3 clonotype14 clonotype14_consensus_2 +PatientB_Base TAATATAGAGTCTGAT-1 True TAATATAGAGTCTGAT-1_contig_1 True 550 TRB CASSHVYWLQRYENAVF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV19 TRBJ2-1 TRBD1 TRBC1 True True 1200 4 clonotype6 clonotype6_consensus_1 +PatientB_Base TAATATAGAGTCTGAT-1 True TAATATAGAGTCTGAT-1_contig_2 True 500 TRA CAEVISNVGVYYVVEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV1-2 TRAJ40 None TRAC True True 1100 3 clonotype6 clonotype6_consensus_2 +PatientB_Base TCCCAAGGTAATTAAA-1 True TCCCAAGGTAATTAAA-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientB_Base TCCCAAGGTAATTAAA-1 True TCCCAAGGTAATTAAA-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 +PatientB_Base CACTTCTGAGACTTGT-1 True CACTTCTGAGACTTGT-1_contig_1 True 550 TRB CASSTAKPHWFDPTIF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV7-2 TRBJ2-7 TRBD1 TRBC1 True True 1200 4 clonotype1 clonotype1_consensus_1 +PatientB_Base CACTTCTGAGACTTGT-1 True CACTTCTGAGACTTGT-1_contig_2 True 500 TRA CAIIGHKAETNIPQEF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV3 TRAJ13 None TRAC True True 1100 3 clonotype1 clonotype1_consensus_2 +PatientB_Base AGACAATGCAGTGCCT-1 True AGACAATGCAGTGCCT-1_contig_1 True 550 TRB CASSYTITSHWHTIEEPPDF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV27 TRBJ1-4 TRBD1 TRBC1 True True 1200 4 clonotype5 clonotype5_consensus_1 +PatientB_Base AGACAATGCAGTGCCT-1 True AGACAATGCAGTGCCT-1_contig_2 True 500 TRA CASDRRANKQLVHFAF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV27 TRAJ23 None TRAC True True 1100 3 clonotype5 clonotype5_consensus_2 +PatientB_Base CACCTAGCATCGGTTG-1 True CACCTAGCATCGGTTG-1_contig_1 True 550 TRB CASSSNKETAWDPVKQLQRGF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRBV4-1 TRBJ1-3 TRBD1 TRBC1 True True 1200 4 clonotype4 clonotype4_consensus_1 +PatientB_Base CACCTAGCATCGGTTG-1 True CACCTAGCATCGGTTG-1_contig_2 True 500 TRA CAGDGKKDVKIRVGKF TGTGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCGCCTTT TRAV12-3 TRAJ33 None TRAC True True 1100 3 clonotype4 clonotype4_consensus_2 diff --git a/tests/fixtures/singlecell/gex/generate_fixture.R b/tests/fixtures/singlecell/gex/generate_fixture.R new file mode 100644 index 0000000..2d67368 --- /dev/null +++ b/tests/fixtures/singlecell/gex/generate_fixture.R @@ -0,0 +1,187 @@ +#!/usr/bin/env Rscript +# Generates a small but real, paired GEX + VDJ single-cell fixture for testing +# TCELL_INTEGRATION (and, downstream, CONGA / CONSENSUS_CLUSTERING / CLUSTER_TO_SC): +# - seurat_annotated.rds : a real Seurat object (RNA counts + PCA/UMAP + metadata) +# - contigs_after_qc.tsv : matching VDJ contigs (same barcodes/samples), in the +# schema TCELL_INTEGRATION reads directly (sample, barcode, chain, cdr3, +# v_gene, j_gene, raw_clonotype_id). +# +# Barcodes are shared between the two files so Seurat/scRepertoire's barcode +# harmonization can match them. Not biologically meaningful data - just +# numerically valid input real Seurat/scRepertoire code can run against. +# +# IMPORTANT: barcodes must be real Cell-Ranger-style 16nt-ACGT + "-1" strings, +# not human-readable IDs - scRepertoire::combineTCR() silently drops any cell +# whose barcode isn't in that format (confirmed by direct testing: a 4-row +# input with "PatientA_Base_CELL001-1"-style barcodes produced 0 output rows +# per sample; the same input with "AAACCTGAGAAACCAT-1"-style barcodes worked). +# combineTCR() also prepends "{sample}_" to its own internal barcode column +# regardless of the `ID` argument - Seurat's raw colnames should NOT have that +# prefix baked in, since TCELL_INTEGRATION's harmonize_barcodes() step exists +# specifically to reconcile that "{sample}_{barcode}" vs "{barcode}" mismatch. + +suppressPackageStartupMessages({ + library(Seurat) + library(Matrix) +}) + +set.seed(42) + +out_dir <- dirname(sub("--file=", "", grep("--file=", commandArgs(), value = TRUE))) +if (length(out_dir) == 0 || out_dir == "") out_dir <- "." + +samples <- c("PatientA_Base", "PatientA_Post", "PatientB_Base") +patient_of <- c(PatientA_Base = "PatientA", PatientA_Post = "PatientA", PatientB_Base = "PatientB") +timepoint_of <- c(PatientA_Base = "Base", PatientA_Post = "Post", PatientB_Base = "Base") +cells_per_sample <- 60 + +# CoNGA refuses to run below a minimum clonotype count ("ERROR too few +# clonotypes", confirmed via its own real log output) - it needs enough +# distinct clones to find meaningful GEX-TCR statistical correlations, not +# just enough cells. 20 distinct clones comfortably clears that. +n_clones <- 20 +trbv_pool <- c("TRBV7-2","TRBV5-1","TRBV6-5","TRBV4-1","TRBV27","TRBV19","TRBV20-1", + "TRBV6-1","TRBV9","TRBV28","TRBV11-2","TRBV12-3","TRBV14","TRBV15", + "TRBV18","TRBV24-1","TRBV25-1","TRBV29-1","TRBV30","TRBV10-3") +trbj_pool <- c("TRBJ2-7","TRBJ1-2","TRBJ2-3","TRBJ1-3","TRBJ1-4","TRBJ2-1","TRBJ2-5", + "TRBJ1-1","TRBJ1-5","TRBJ2-6") +trav_pool <- c("TRAV3","TRAV13-1","TRAV21","TRAV12-3","TRAV27","TRAV1-2","TRAV17", + "TRAV19","TRAV38-1","TRAV26-1","TRAV9-2","TRAV14/DV4","TRAV35", + "TRAV8-1","TRAV24","TRAV5","TRAV20","TRAV22","TRAV30","TRAV41") +traj_pool <- c("TRAJ13","TRAJ6","TRAJ48","TRAJ33","TRAJ23","TRAJ40","TRAJ22", + "TRAJ34","TRAJ42","TRAJ54") + +aa <- c("A","S","G","P","T","N","D","E","Q","K","R","L","V","I","F","Y","W","H") +random_cdr3 <- function(prefix, min_len = 11, max_len = 16) { + mid <- paste(sample(aa, sample(min_len:max_len, 1), replace = TRUE), collapse = "") + paste0(prefix, mid, "F") +} + +beta_clones <- lapply(seq_len(n_clones), function(i) { + c(random_cdr3("CASS"), trbv_pool[[((i - 1) %% length(trbv_pool)) + 1]], trbj_pool[[((i - 1) %% length(trbj_pool)) + 1]]) +}) +alpha_clones <- lapply(seq_len(n_clones), function(i) { + c(random_cdr3("CA"), trav_pool[[((i - 1) %% length(trav_pool)) + 1]], traj_pool[[((i - 1) %% length(traj_pool)) + 1]]) +}) +stopifnot(length(unique(sapply(beta_clones, `[[`, 1))) == n_clones) + +random_nt_barcode <- function() paste0(paste(sample(c("A","C","G","T"), 16, replace = TRUE), collapse = ""), "-1") + +all_barcodes <- c() +all_meta <- data.frame() +contig_rows <- list() + +for (s in samples) { + # Weight so a handful of clones are visibly expanded, not perfectly uniform. + clone_prob <- rev(seq_len(n_clones)) + clone_idx <- sample(seq_len(n_clones), cells_per_sample, replace = TRUE, + prob = clone_prob / sum(clone_prob)) + barcodes <- character(0) + while (length(barcodes) < cells_per_sample) { + cand <- random_nt_barcode() + if (!(cand %in% all_barcodes) && !(cand %in% barcodes)) barcodes <- c(barcodes, cand) + } + all_barcodes <- c(all_barcodes, barcodes) + + # CONGA (unlike TCELL_INTEGRATION) has no fallback when no cell-type + # annotation column is present - it hard-requires one of + # predicted_labels/celltype/Annotation and errors otherwise + # ("Could not resolve annotation label column."), since correlating GEX + # cell state with TCR clusters is the whole point of the tool. Real usage + # is expected to supply an already-annotated GEX object; synthesize a + # plausible-enough placeholder here so the module can actually run. + celltype <- sample(c("CD8 T cell", "CD4 T cell"), cells_per_sample, replace = TRUE, prob = c(0.6, 0.4)) + + all_meta <- rbind(all_meta, data.frame( + barcode = barcodes, + orig.ident = s, + patient_id = patient_of[[s]], + condition = "tumor", + timepoint = timepoint_of[[s]], + celltype = celltype, + stringsAsFactors = FALSE + )) + + for (i in seq_len(cells_per_sample)) { + ci <- clone_idx[i] + clonotype_id <- paste0("clonotype", ci) + beta_nt <- paste0("TGT", paste(rep("GCC", nchar(beta_clones[[ci]][1]) - 2), collapse = ""), "TTT") + alpha_nt <- paste0("TGT", paste(rep("GCC", nchar(alpha_clones[[ci]][1]) - 2), collapse = ""), "TTT") + contig_rows[[length(contig_rows) + 1]] <- data.frame( + sample = s, barcode = barcodes[i], is_cell = "True", + contig_id = paste0(barcodes[i], "_contig_1"), high_confidence = "True", + length = 550, chain = "TRB", + cdr3 = beta_clones[[ci]][1], cdr3_nt = beta_nt, + v_gene = beta_clones[[ci]][2], j_gene = beta_clones[[ci]][3], + d_gene = "TRBD1", c_gene = "TRBC1", full_length = "True", + productive = "True", reads = 1200, umis = 4, + raw_clonotype_id = clonotype_id, + raw_consensus_id = paste0(clonotype_id, "_consensus_1"), stringsAsFactors = FALSE + ) + contig_rows[[length(contig_rows) + 1]] <- data.frame( + sample = s, barcode = barcodes[i], is_cell = "True", + contig_id = paste0(barcodes[i], "_contig_2"), high_confidence = "True", + length = 500, chain = "TRA", + cdr3 = alpha_clones[[ci]][1], cdr3_nt = alpha_nt, + v_gene = alpha_clones[[ci]][2], j_gene = alpha_clones[[ci]][3], + d_gene = "None", c_gene = "TRAC", full_length = "True", + productive = "True", reads = 1100, umis = 3, + raw_clonotype_id = clonotype_id, + raw_consensus_id = paste0(clonotype_id, "_consensus_2"), stringsAsFactors = FALSE + ) + } +} + +contigs_after_qc <- do.call(rbind, contig_rows) +write.table(contigs_after_qc, file.path(out_dir, "contigs_after_qc.tsv"), + sep = "\t", quote = FALSE, row.names = FALSE) +cat(sprintf("wrote contigs_after_qc.tsv: %d rows across %d samples\n", + nrow(contigs_after_qc), length(samples))) + +# ---- synthetic GEX counts: n_genes x n_cells, with a STRONG per-state mean +# shift on distinct marker-gene sets, then a real Seurat pipeline +# (Normalize/ScaleData/PCA/UMAP). CoNGA's own downstream DEG/dotplot step +# (find_gex_cluster_degs -> sc.pl.dotplot) needs louvain GEX clustering to +# find real, separable clusters with real differentially-expressed genes - +# a small number of states (independent of TCR clone count) with big, +# non-overlapping marker sets gives it that; too many/weak states (the +# earlier version scaled state count with n_clones, diluting the signal) +# left too few genes anywhere near significant, and CoNGA's own dotplot call +# crashed on the resulting near-empty gene list (matplotlib +# "left cannot be >= right" from a degenerate GridSpec). +n_genes <- 500 +gene_names <- sprintf("Gene%04d", seq_len(n_genes)) +n_cells <- length(all_barcodes) +n_gex_states <- 3 +markers_per_state <- 100 # broad enough that >=50 genes clear scanpy's HVG + # selection - CoNGA's internal PCA hard-requires + # at least 50 variable genes (n_components=50); + # too few genes at a very strong fold-change + # over-concentrates variance into too narrow a set. + +base_rate <- rgamma(n_genes, shape = 2, rate = 1) +state_vec <- sample(seq_len(n_gex_states), n_cells, replace = TRUE) +state_markers <- split(sample(seq_len(n_genes), n_gex_states * markers_per_state), + rep(seq_len(n_gex_states), each = markers_per_state)) + +counts <- matrix(0L, nrow = n_genes, ncol = n_cells, dimnames = list(gene_names, all_barcodes)) +for (cell in seq_len(n_cells)) { + lambda <- base_rate + lambda[state_markers[[state_vec[cell]]]] <- lambda[state_markers[[state_vec[cell]]]] * 6 + counts[, cell] <- rpois(n_genes, lambda * 5) +} +counts <- as(counts, "CsparseMatrix") + +rownames(all_meta) <- all_meta$barcode +seu <- CreateSeuratObject(counts = counts, meta.data = all_meta[all_barcodes, , drop = FALSE]) + +seu <- NormalizeData(seu, verbose = FALSE) +seu <- FindVariableFeatures(seu, nfeatures = min(200, n_genes), verbose = FALSE) +seu <- ScaleData(seu, verbose = FALSE) +seu <- RunPCA(seu, npcs = 20, verbose = FALSE) +seu <- FindNeighbors(seu, dims = 1:20, verbose = FALSE) +seu <- RunUMAP(seu, dims = 1:20, verbose = FALSE) + +saveRDS(seu, file.path(out_dir, "seurat_annotated.rds")) +cat(sprintf("wrote seurat_annotated.rds: %d genes x %d cells, samples: %s\n", + nrow(seu), ncol(seu), paste(samples, collapse = ", "))) diff --git a/tests/fixtures/singlecell/gex/seurat_annotated.rds b/tests/fixtures/singlecell/gex/seurat_annotated.rds new file mode 100644 index 0000000..b1dcfca Binary files /dev/null and b/tests/fixtures/singlecell/gex/seurat_annotated.rds differ diff --git a/tests/main.nf.test b/tests/main.nf.test index 92bb2b9..85acbe3 100644 --- a/tests/main.nf.test +++ b/tests/main.nf.test @@ -23,4 +23,45 @@ nextflow_pipeline { } } + + test("Single-cell VDJ-only minimal example") { + + tag "singlecell" + tag "vdj-only" + + when { + params { + // sample_sheet's path column must be an absolute path resolvable inside + // the container (see tests/nextflow.config's VDJ_QC containerOptions), so + // it's generated here with the real projectDir baked in rather than + // committed as a static fixture file. + def cellrangerDir = "${projectDir}/tests/fixtures/singlecell/cellranger" + def sheet = new File("${launchDir}/vdj_only_sample_sheet.csv") + sheet.text = "sample,path,patient_id,condition,timepoint\n" + + "PatientA_Base,${cellrangerDir}/PatientA_Base,PatientA,tumor,Base\n" + + "PatientA_Post,${cellrangerDir}/PatientA_Post,PatientA,tumor,Post\n" + + "PatientB_Base,${cellrangerDir}/PatientB_Base,PatientB,tumor,Base\n" + + mode = "singlecell" + // Not actually dereferenced downstream (VDJ_QC reads per-sample paths + // from sample_sheet instead) but validated non-null - see workflows/singlecell.nf. + input_vdj_contigs = cellrangerDir + sample_sheet = sheet.path + outdir = "out-singlecell-vdjonly-test" + project_name = "singlecell_vdjonly_test" + // Tiny fixture (8 cells/sample) - default pseudobulk QC gate + // (25 clones / 50 cells) would drop every sample outright. + pseudobulk_qc_min_clones = 1 + pseudobulk_qc_min_cells = 1 + max_memory = "8.GB" + max_cpus = 4 + container = System.getenv("CONTAINER") ?: "ghcr.io/karchinlab/tcrtoolkit:main" + } + } + + then { + assert workflow.success + } + + } } diff --git a/tests/modules/bridges/cluster_to_sc.nf.test b/tests/modules/bridges/cluster_to_sc.nf.test new file mode 100644 index 0000000..af535e5 --- /dev/null +++ b/tests/modules/bridges/cluster_to_sc.nf.test @@ -0,0 +1,49 @@ +nextflow_process { + + name "Test CLUSTER_TO_SC" + script "modules/bridges/cluster_to_sc.nf" + process "CLUSTER_TO_SC" + + test("Should map real GIANA clusters onto real TCELL_INTEGRATION cells") { + + tag "singlecell" + + when { + params { + container = System.getenv("CONTAINER") ?: "ghcr.io/karchinlab/tcrtoolkit:main" + } + + process { + """ + input[0] = file("${projectDir}/tests/fixtures/singlecell/cluster_to_sc/seurat_tcells_with_TCR.rds") + input[1] = file("${projectDir}/tests/fixtures/singlecell/cluster_to_sc/tcr_export_cells_with_embedding.tsv") + input[2] = file("${projectDir}/tests/fixtures/singlecell/cluster_to_sc/PatientA_giana.txt") + input[3] = file("${projectDir}/tests/fixtures/singlecell/cluster_to_sc/NO_FILE_gliph2") + input[4] = file("${projectDir}/tests/fixtures/singlecell/cluster_to_sc/NO_FILE_tcrdist_clone") + input[5] = file("${projectDir}/tests/fixtures/singlecell/cluster_to_sc/NO_FILE_tcrdist_matrix") + input[6] = 24 + """ + } + } + + then { + assert process.success + with(process.out.enriched_seurat) { + assert path(get(0)).exists() + } + with(process.out.giana_export) { + def export_file = path(get(0)) + assert export_file.exists() + def lines = export_file.readLines() + // PatientA_giana.txt lists real CDR3b clones from the fixture, so + // at least some cells should get a real giana_cluster assignment + // (not just an empty column) - confirms the join actually matched. + def header = lines[0].split("\t") + def clusterIdx = header.findIndexOf { it == "giana_cluster" } + assert clusterIdx >= 0 + def assigned = lines[1..-1].count { it.split("\t", -1)[clusterIdx] != "" } + assert assigned > 0 + } + } + } +} diff --git a/tests/modules/local/report/render_notebook.nf.test b/tests/modules/local/report/render_notebook.nf.test index eba24f6..8f7290a 100644 --- a/tests/modules/local/report/render_notebook.nf.test +++ b/tests/modules/local/report/render_notebook.nf.test @@ -21,7 +21,8 @@ nextflow_process { """ input[0] = [ file("${projectDir}/tests/fixtures/test.qmd"), - file("${projectDir}/tests/fixtures") + file("${projectDir}/tests/fixtures"), + [] ] input[1] = "TCRtoolkit" input[2] = "nextflow run main.nf" @@ -39,4 +40,88 @@ nextflow_process { } } + test("Should stage files into a nested project_dir layout") { + // Verifies the staged_layout mechanism used by notebooks that read from a + // project_dir// tree (e.g. template_discovery_brief.qmd) + // instead of bare relative filenames. + + tag "notebook" + tag "staged_layout" + + when { + params { + samplesheet = "${projectDir}/tests/fixtures/valid_samplesheet.csv" + container = System.getenv("CONTAINER") ?: "ghcr.io/karchinlab/tcrtoolkit:main" + } + + process { + """ + input[0] = [ + file("${projectDir}/tests/fixtures/test.qmd"), + [file("${projectDir}/tests/fixtures/valid_samplesheet.csv")], + [["myproj/sample/valid_samplesheet.csv", "valid_samplesheet.csv"]] + ] + input[1] = "TCRtoolkit" + input[2] = "nextflow run main.nf" + """ + } + } + + then { + assert process.success + with(process.out.report_html) { + def html_file = path(get(0)) + assert html_file.exists() + assert html_file.getFileName().toString().endsWith('.html') + // The html output and the staged symlinks are both written into the + // task's work dir, so the staged path is a sibling of html_file. + def staged_file = html_file.getParent().resolve("myproj/sample/valid_samplesheet.csv") + assert staged_file.exists() + assert java.nio.file.Files.isSymbolicLink(staged_file) + } + } + } + + test("Should stage a file under a renamed dest basename") { + // Verifies source and dest basenames can differ - e.g. + // template_discovery_brief.qmd includes a generic template_pheno.qmd, + // which gets symlinked from whichever real notebook applies + // (template_pheno_sc.qmd or template_pheno_bulk.qmd, here stood in for + // by valid_samplesheet.csv) to that shared destination basename. + + tag "notebook" + tag "staged_layout" + + when { + params { + samplesheet = "${projectDir}/tests/fixtures/valid_samplesheet.csv" + container = System.getenv("CONTAINER") ?: "ghcr.io/karchinlab/tcrtoolkit:main" + } + + process { + """ + input[0] = [ + file("${projectDir}/tests/fixtures/test.qmd"), + [file("${projectDir}/tests/fixtures/valid_samplesheet.csv")], + [["template_pheno.qmd", "valid_samplesheet.csv"]] + ] + input[1] = "TCRtoolkit" + input[2] = "nextflow run main.nf" + """ + } + } + + then { + assert process.success + with(process.out.report_html) { + def html_file = path(get(0)) + assert html_file.exists() + assert html_file.getFileName().toString().endsWith('.html') + def staged_file = html_file.getParent().resolve("template_pheno.qmd") + assert staged_file.exists() + assert java.nio.file.Files.isSymbolicLink(staged_file) + } + } + } + } diff --git a/tests/modules/scratch/consensus_clustering.nf.test b/tests/modules/scratch/consensus_clustering.nf.test new file mode 100644 index 0000000..4152673 --- /dev/null +++ b/tests/modules/scratch/consensus_clustering.nf.test @@ -0,0 +1,46 @@ +nextflow_process { + + name "Test CONSENSUS_CLUSTERING" + script "modules/scratch/CONSENSUS_CLUSTERING/main.nf" + process "CONSENSUS_CLUSTERING" + + test("Should build a consensus from real GIANA-enriched cells (GLIPH2/TCRdist absent)") { + + tag "singlecell" + + when { + params { + container = System.getenv("CONTAINER") ?: "syedsazaidi/scratch-tcr:latest" + } + + process { + """ + input[0] = file("${projectDir}/tests/fixtures/singlecell/cluster_to_sc/enriched_seurat.rds") + input[1] = file("${projectDir}/tests/fixtures/singlecell/cluster_to_sc/tcr_export_cells_with_embedding.tsv") + input[2] = file("${projectDir}/tests/fixtures/singlecell/cluster_to_sc/NO_FILE_gliph2") + input[3] = file("${projectDir}/tests/fixtures/singlecell/cluster_to_sc/NO_FILE_tcrdist_clone") + input[4] = file("${projectDir}/tests/fixtures/singlecell/cluster_to_sc/giana_export_cells.tsv") + input[5] = file("${projectDir}/modules/scratch/CONSENSUS_CLUSTERING/Clonotype_Clustering_Consensus_Report.qmd") + input[6] = "test_project" + """ + } + } + + then { + assert process.success + with(process.out.seurat_with_consensus) { + assert path(get(0)).exists() + } + with(process.out.export_cells) { + def export_file = path(get(0)) + assert export_file.exists() + def lines = export_file.readLines() + assert lines.size() == 181 + assert lines[0].contains("giana_cluster") + } + with(process.out.report_html) { + assert path(get(0)).exists() + } + } + } +} diff --git a/tests/modules/scratch/tcell_integration.nf.test b/tests/modules/scratch/tcell_integration.nf.test new file mode 100644 index 0000000..531475c --- /dev/null +++ b/tests/modules/scratch/tcell_integration.nf.test @@ -0,0 +1,48 @@ +nextflow_process { + + name "Test TCELL_INTEGRATION" + script "modules/scratch/TCELL_INTEGRATION/main.nf" + process "TCELL_INTEGRATION" + + test("Should integrate real synthetic GEX+VDJ data and produce an enriched Seurat object") { + + tag "singlecell" + + when { + params { + container = System.getenv("CONTAINER") ?: "ghcr.io/karchinlab/tcrtoolkit:main" + } + + process { + """ + input[0] = file("${projectDir}/tests/fixtures/singlecell/gex/contigs_after_qc.tsv") + input[1] = file("${projectDir}/tests/fixtures/singlecell/gex/seurat_annotated.rds") + input[2] = file("${projectDir}/modules/scratch/TCELL_INTEGRATION/TCell_Integration_Report.qmd") + input[3] = "test_project" + """ + } + } + + then { + assert process.success + with(process.out.export_cells) { + def export_file = path(get(0)) + assert export_file.exists() + def lines = export_file.readLines() + // 180 synthetic cells went in; every one should carry a TCR + // (T-AB paired contigs were generated for all of them), so all + // should survive the T-cell subset/filter into the export table. + // 181 = header + 180 cells: every synthetic cell has paired T-AB + // contigs, so all should survive the T-cell subset/filter. + assert lines.size() == 181 + assert lines[0].toLowerCase().contains("cell_id") + } + with(process.out.seurat_tcells_with_tcr) { + assert path(get(0)).exists() + } + with(process.out.report_html) { + assert path(get(0)).exists() + } + } + } +} diff --git a/tests/modules/scratch/vdj_qc.nf.test b/tests/modules/scratch/vdj_qc.nf.test new file mode 100644 index 0000000..73a2ce9 --- /dev/null +++ b/tests/modules/scratch/vdj_qc.nf.test @@ -0,0 +1,60 @@ +nextflow_process { + + name "Test VDJ_QC" + script "modules/scratch/VDJ_QC/main.nf" + process "VDJ_QC" + + test("Should filter contigs and produce a real contigs_after_qc.tsv from synthetic Cell Ranger data") { + + tag "singlecell" + + when { + params { + container = System.getenv("CONTAINER") ?: "ghcr.io/karchinlab/tcrtoolkit:main" + metadata_file = "${projectDir}/assets/NO_FILE" + } + + process { + """ + // sample_sheet's path column must be an absolute path resolvable inside + // the container (see tests/nextflow.config's VDJ_QC containerOptions), so + // it's generated here with the real \${projectDir} baked in rather than + // committed as a static fixture file. + def cellrangerDir = "${projectDir}/tests/fixtures/singlecell/cellranger" + def sheet = new File("\${launchDir}/vdj_qc_sample_sheet.csv") + sheet.text = "sample,path,patient_id,condition,timepoint\\n" + + "PatientA_Base,\${cellrangerDir}/PatientA_Base,PatientA,tumor,Base\\n" + + "PatientA_Post,\${cellrangerDir}/PatientA_Post,PatientA,tumor,Post\\n" + + "PatientB_Base,\${cellrangerDir}/PatientB_Base,PatientB,tumor,Base\\n" + + input[0] = file("${projectDir}/modules/scratch/VDJ_QC/VDJ_QC_analysis.qmd") + input[1] = file(sheet.path) + input[2] = file("${projectDir}/assets/NO_FILE") + input[3] = "test_project" + """ + } + } + + then { + assert process.success + with(process.out.contigs_after_qc) { + def contigs_file = path(get(0)) + assert contigs_file.exists() + def lines = contigs_file.readLines() + // header + 3 samples * 16 contig rows = 49, minus whatever the QC + // filters legitimately drop (all synthetic contigs are productive, + // high_confidence, full_length, paired TRA/TRB, so none should be + // dropped by the default filters) - just assert real rows came through. + assert lines.size() > 1 + assert lines[0].contains("barcode") + assert lines[0].contains("chain") + // both patients and both timepoints should have survived QC + def body = lines[1..-1].join("\n") + assert body.contains("PatientA_Base") || body.contains("PatientA_Post") || body.contains("PatientB_Base") + } + with(process.out.report_html) { + assert path(get(0)).exists() + } + } + } +} diff --git a/tests/nextflow.config b/tests/nextflow.config index c19b1ad..ded9c68 100644 --- a/tests/nextflow.config +++ b/tests/nextflow.config @@ -3,3 +3,27 @@ Nextflow config file for running tests ======================================================================================== */ + +// VDJ_QC reads Cell Ranger `outs/` directories by dereferencing a plain path string +// from inside the samplesheet CSV, not via a declared Nextflow `path` input - so +// Docker's default mounting (task work dir + declared inputs only) doesn't make them +// visible inside the container. Test-only bind mount so the synthetic single-cell +// fixtures under tests/fixtures/singlecell/ are visible; production runs would need +// the equivalent (e.g. a broader host mount in NXF_DOCKER_OPTS / a personal +// ~/.nextflow/config) to read real Cell Ranger output from outside the work tree. +process { + withName: 'VDJ_QC' { + containerOptions = "-v ${projectDir}/tests/fixtures/singlecell:${projectDir}/tests/fixtures/singlecell" + } + + // conf/base.config requests 8 cpus / 60GB for the SC modality processes, sized + // for real single-cell datasets. CI (GitHub Actions ubuntu-latest) only has 4 + // CPUs, so the local executor hard-fails with "Process requirement exceeds + // available CPUs" before the task even starts. Test fixtures are tiny synthetic + // data, so cap these down to values that fit the runner (leave headroom instead + // of requesting all 4 CPUs so other tasks/OS overhead don't starve). + withName: 'VDJ_QC|TCELL_INTEGRATION|CONGA|CONSENSUS_CLUSTERING|REPERTOIRE|MASTER_SUMMARY|CLUSTER_TO_SC|TCRI' { + cpus = 2 + memory = 6.GB + } +} diff --git a/workflows/singlecell.nf b/workflows/singlecell.nf new file mode 100644 index 0000000..473d512 --- /dev/null +++ b/workflows/singlecell.nf @@ -0,0 +1,194 @@ +/* + * SINGLECELL_WORKFLOW — single-cell TCR modality (integrated). + * + * Unified spine (see IMPLEMENTATION_SPEC.md §1.3). Both routes converge after their + * pseudobulk step into: PSEUDOBULK_QC → ANNOTATE_FROM_CONCAT → full shared bulk engine + * (SAMPLE + PATIENT + COMPARE) → repertoire/summary. + * + * Full SC (--input_annotated_object provided): + * VDJ_QC → TCELL_INTEGRATION → SC_TO_CDR3 (phenotype pseudobulk) + * → PSEUDOBULK_QC → ANNOTATE_FROM_CONCAT + * → SAMPLE + PATIENT + COMPARE (shared engine, full route) + * → CLUSTER_TO_SC → CONGA → CONSENSUS (cell-level, GEX-only) + * → REPERTOIRE (cell-level) → MASTER_SUMMARY (full) + * + * VDJ-only (--input_annotated_object absent): + * VDJ_QC → VDJ_TO_BULK (pseudobulk from contigs) + * → PSEUDOBULK_QC → ANNOTATE_FROM_CONCAT + * → SAMPLE + PATIENT + COMPARE (shared engine, full route) + * → BULK_TO_EXPORT (clonotype-level per-cell export) + * → REPERTOIRE (clonotype-level) → MASTER_SUMMARY (CoNGA-excluded) + * (only CLUSTER_TO_SC / CONGA / CONSENSUS are skipped — they need the GEX substrate) + */ + +// ── Single-cell subworkflows (SCRATCH) ──────────────────────────────────── +include { VDJ_QC_SW } from '../subworkflows/scratch/vdj_qc.nf' +include { TCELL_INTEGRATION_SW } from '../subworkflows/scratch/tcell_integration.nf' +include { CONGA_SW } from '../subworkflows/scratch/conga.nf' +include { CONSENSUS_SW } from '../subworkflows/scratch/consensus_clustering.nf' +include { REPERTOIRE_SW } from '../subworkflows/scratch/repertoire.nf' +include { MASTER_SUMMARY_SW } from '../subworkflows/scratch/master_summary.nf' + +// ── Bridges (SC ↔ bulk-engine schema conversion) ────────────────────────── +include { SC_TO_CDR3_SW } from '../subworkflows/bridges/sc_to_cdr3.nf' +include { VDJ_TO_BULK_SW } from '../subworkflows/bridges/vdj_to_bulk.nf' +include { CLUSTER_TO_SC_SW } from '../subworkflows/bridges/cluster_to_sc.nf' +include { SC_SAMPLE_STATS } from '../modules/bridges/sc_sample_stats.nf' +include { BULK_TO_EXPORT } from '../modules/bridges/bulk_to_export.nf' + +// ── Shared bulk engine (main/local — unchanged behavior) ────────────────── +include { ANNOTATE_FROM_CONCAT } from '../subworkflows/local/annotate.nf' +include { PSEUDOBULK_QC_SW } from '../subworkflows/local/pseudobulk_qc.nf' +include { SAMPLE } from '../subworkflows/local/sample.nf' +include { PATIENT } from '../subworkflows/local/patient.nf' +include { COMPARE } from '../subworkflows/local/compare.nf' + +// A workflow-local `def enabled = { x -> ... }` closure isn't visible from +// inside nested if-blocks under this Nextflow version's strict-syntax parser +// ("`enabled` is not defined") - a plain top-level function is. +def enabled(x) { x == null || x == true } + +workflow SINGLECELL_WORKFLOW { + + def nofile = file("${projectDir}/assets/NO_FILE") + + // ── Mandatory inputs (both routes) ──────────────────────────────────── + if (!params.input_vdj_contigs) error "Please provide --input_vdj_contigs" + if (!params.sample_sheet) error "Please provide --sample_sheet" + + def vdjOnly = (!params.input_annotated_object || + params.input_annotated_object == '' || + params.input_annotated_object.endsWith('NO_FILE')) + + if (vdjOnly) { + log.info "==> VDJ-only mode: no GEX annotated object provided. " + + "TCELL_INTEGRATION / CLUSTER_TO_SC / CONGA will be skipped." + } + + ch_sample_sheet = Channel.fromPath(params.sample_sheet, checkIfExists: true) + ch_project_name = Channel.value(params.project_name) + ch_annotated_obj = vdjOnly + ? Channel.fromPath("${projectDir}/assets/NO_FILE") + : Channel.fromPath(params.input_annotated_object, checkIfExists: true) + + // ── Step 1: VDJ QC (both routes) ────────────────────────────────────── + vdj_qc_out = VDJ_QC_SW( ch_sample_sheet, ch_project_name, ch_annotated_obj ) + + // VDJ QC tables/figures kept for the Master Summary (full-SC route) + def vdj_qc_per_sample_compact = vdj_qc_out.qc_tables.flatten().filter { it.name == 'vdj_qc_per_sample_compact.tsv' }.ifEmpty(nofile) + def vdj_qc_before_after_summary = vdj_qc_out.qc_tables.flatten().filter { it.name == 'qc_contigs_before_after_summary.tsv' }.ifEmpty(nofile) + def vdj_qc_sample_sheet_resolved = vdj_qc_out.qc_tables.flatten().filter { it.name == 'sample_sheet_resolved.tsv' }.ifEmpty(nofile) + def vdj_qc_clone_rank_abundance = vdj_qc_out.qc_tables.flatten().filter { it.name == 'clone_rank_abundance.tsv' }.ifEmpty(nofile) + def vdj_qc_before_after_retention_fig = vdj_qc_out.qc_figures.flatten().filter { it.name == 'qc_before_after_retention.png' }.ifEmpty(nofile) + def vdj_qc_pairing_bar_fig = vdj_qc_out.qc_figures.flatten().filter { it.name == 'pairing_bar_by_sample.png' }.ifEmpty(nofile) + def vdj_qc_clone_rank_abundance_fig = vdj_qc_out.qc_figures.flatten().filter { it.name == 'clone_rank_abundance.png' }.ifEmpty(nofile) + def vdj_qc_multiple_chains_fig = vdj_qc_out.qc_figures.flatten().filter { it.name == 'multiple_chains_by_sample.png' }.ifEmpty(nofile) + + // ── Step 2: pseudobulk → clonotype table (route-specific source) ────── + def sc_samplesheet = nofile + if (vdjOnly) { + VDJ_TO_BULK_SW( vdj_qc_out.contigs_after_qc ) + pseudobulk_map = VDJ_TO_BULK_SW.out.sample_map + sc_samplesheet = VDJ_TO_BULK_SW.out.samplesheet_utf8 + } else { + tcell_out = TCELL_INTEGRATION_SW( + vdj_qc_out.contigs_after_qc, ch_annotated_obj, ch_project_name + ) + SC_TO_CDR3_SW( tcell_out.export_cells ) + pseudobulk_map = SC_TO_CDR3_SW.out.sample_map + } + + // ── Step 3: tcrtoolkit pseudobulk QC gate (both routes) ─────────────── + PSEUDOBULK_QC_SW( pseudobulk_map ) + + // ── Step 4: OLGA annotation on QC-passed pseudobulk (shared engine) ─── + ANNOTATE_FROM_CONCAT( PSEUDOBULK_QC_SW.out.sample_map, PSEUDOBULK_QC_SW.out.concat_cdr3 ) + + def processed_samples = ANNOTATE_FROM_CONCAT.out.processed_samples + def concat_cdr3_sorted = ANNOTATE_FROM_CONCAT.out.concat_cdr3_sorted + def cdr3_pgen = ANNOTATE_FROM_CONCAT.out.cdr3_pgen + def olga_stats = ANNOTATE_FROM_CONCAT.out.olga_stats + + // ── Step 5: FULL shared bulk route (Option A — no truncation) ───────── + // Synthesize the pre-filter-stats sidecar so SC data can use main's 4-arg SAMPLE. + SC_SAMPLE_STATS( processed_samples ) + + SAMPLE( processed_samples, SC_SAMPLE_STATS.out.pre_filter_stats, cdr3_pgen, olga_stats ) + PATIENT( processed_samples ) + COMPARE( concat_cdr3_sorted, cdr3_pgen ) + + // Note: the shared bulk template_qc REPORT is intentionally NOT run here — it renders a + // bulk-samplesheet-metadata QC notebook that doesn't fit single-cell-derived data. + // Single-cell reporting is handled by REPERTOIRE + MASTER_SUMMARY (below). + + // ── Step 6: cell-level clustering — FULL-SC ONLY (needs the GEX/Seurat substrate) ── + // Produces the enriched/consensus Seurat + export that REPERTOIRE / MASTER_SUMMARY use + // when a GEX object is present. In VDJ-only mode this whole block is skipped and a + // clonotype-level export is synthesized instead (below). + conga_report = Channel.empty() + consensus_report = Channel.empty() + + if (!vdjOnly) { + // Reuse tcrdist3 outputs computed inside SAMPLE (no second run). + CLUSTER_TO_SC_SW( + tcell_out.seurat_tcells_with_tcr, + tcell_out.export_cells, + PATIENT.out.giana_clusters, + PATIENT.out.gliph2_cluster_details, + SAMPLE.out.tcrdist_clone_df, + SAMPLE.out.tcrdist_output.map { _meta, f -> f } + ) + enriched_seurat = CLUSTER_TO_SC_SW.out.enriched_seurat + + if (enabled(params.run_conga)) { + conga_out = CONGA_SW( enriched_seurat, tcell_out.export_cells, ch_project_name ) + conga_report = conga_out.report_html + } + + if (enabled(params.run_consensus)) { + def gliph2_export = CLUSTER_TO_SC_SW.out.gliph2_export.ifEmpty(nofile) + def tcrdist_export = CLUSTER_TO_SC_SW.out.tcrdist_export.ifEmpty(nofile) + def giana_export = CLUSTER_TO_SC_SW.out.giana_export.ifEmpty(nofile) + CONSENSUS_SW( + enriched_seurat, tcell_out.export_cells, + gliph2_export, tcrdist_export, giana_export, ch_project_name + ) + consensus_report = CONSENSUS_SW.out.report_html + } + + rep_seurat = enabled(params.run_consensus) ? CONSENSUS_SW.out.seurat_with_consensus : enriched_seurat + rep_export = enabled(params.run_consensus) ? CONSENSUS_SW.out.export_cells : tcell_out.export_cells + + } else { + // VDJ-only: no Seurat. Build a clonotype-level per-cell export from the pseudobulk + // so REPERTOIRE / MASTER_SUMMARY run without a GEX object (CoNGA/consensus skipped). + BULK_TO_EXPORT( concat_cdr3_sorted, sc_samplesheet ) + rep_seurat = Channel.fromPath("${projectDir}/assets/NO_FILE") + rep_export = BULK_TO_EXPORT.out.export_cells + } + + // ── Step 7: REPERTOIRE + MASTER_SUMMARY — BOTH routes ───────────────── + // Cell-level + full with a GEX object; clonotype-level repertoire and a CoNGA-excluded + // summary without one (only CoNGA and the cell-cluster mapping are truly GEX-gated). + repertoire_report = Channel.empty() + if (enabled(params.run_repertoire)) { + REPERTOIRE_SW( rep_seurat, rep_export, ch_project_name ) + repertoire_report = REPERTOIRE_SW.out.report_html + } + + if (enabled(params.run_master_summary)) { + master_barrier = Channel.empty() + .mix(conga_report, consensus_report, repertoire_report) + .collect() + .ifEmpty([nofile]) + + MASTER_SUMMARY_SW( + rep_seurat, rep_export, + vdj_qc_per_sample_compact, vdj_qc_before_after_summary, + vdj_qc_sample_sheet_resolved, vdj_qc_clone_rank_abundance, + vdj_qc_before_after_retention_fig, vdj_qc_pairing_bar_fig, + vdj_qc_clone_rank_abundance_fig, vdj_qc_multiple_chains_fig, + master_barrier, ch_project_name + ) + } +} diff --git a/workflows/tcrtoolkit.nf b/workflows/tcrtoolkit.nf index f962d0b..cd0822b 100644 --- a/workflows/tcrtoolkit.nf +++ b/workflows/tcrtoolkit.nf @@ -107,24 +107,206 @@ workflow TCRTOOLKIT { ) } - // Report - works on channel of tuples [report name, [report files]] + // Report - works on channel of tuples [notebook template, files to stage, staged directory layout] ch_reports = channel.empty() - // QC report requires sample-level aggregate outputs. - ch_qc_report = SAMPLE.out.sample_csv + def sample_stats_agg = SAMPLE.out.sample_csv .collectFile(name: "sample_stats.csv", keepHeader: true, skip: 1, sort: true) + + // Only stage AIRR-converted files when CONVERT ran (adaptive/cellranger); + // template_discovery_brief.qmd's VDJdb section otherwise reads the raw + // input directly, which already has AIRR-standard frequency columns. + def convert_files = (input_format == 'adaptive' || input_format == 'cellranger') + ? CONVERT.out.sample_map_converted.map { _meta, f -> f }.collect() + : channel.value([]) + + // template_discovery_brief.qmd includes a single generic template_pheno.qmd - + // RENDER_NOTEBOOK stages whichever real notebook applies under that shared + // name (see staged_layout below), since Quarto's {{< include >}} shortcode is + // a static textual splice with no native runtime if/else. pheno_sc needs the + // per-cell/per-phenotype pseudobulk files, which only exist for cellranger + // input with sobject_gex supplied; everything else gets pheno_bulk, which + // only needs TCRPHENO output (produced for every sample regardless of format). + def use_pheno_sc = (input_format == 'cellranger' && params.sobject_gex) + def pheno_notebook = use_pheno_sc ? file(params.template_pheno_sc) : file(params.template_pheno_bulk) + // .collect() on a channel that never emits (channel.empty(), which is what + // CONVERT.out.pseudobulk_phenotype_files is outside the cellranger+sobject_gex + // case) never emits either - not even an empty list - which would silently + // starve every downstream .combine() in ch_discovery_report and make + // RENDER_NOTEBOOK(template_discovery_brief) never get scheduled at all (no + // error, just silently skipped - confirmed directly with a minimal repro). + // Guard with the same channel.value([]) fallback already used for convert_files. + def pseudobulk_pheno_files = use_pheno_sc + ? CONVERT.out.pseudobulk_phenotype_files.map { _meta, files -> files }.flatten().collect() + : channel.value([]) + + ch_qc_report = sample_stats_agg .combine(ANNOTATE.out.concat_cdr3_sorted) .map { sample_stats_csv, concat_cdr3_sorted -> tuple( file(params.template_qc), [sample_stats_csv, - concat_cdr3_sorted] + concat_cdr3_sorted], + [] ) } ch_reports = ch_reports.mix(ch_qc_report) - // Another report - // ch_reports = ch_reports.mix( ... ) + // Discovery brief reads its inputs from a project_dir// + // / layout, so staged_layout tells RENDER_NOTEBOOK where to + // symlink each staged file - each entry is a [dest_path, source_basename] + // pair (source and dest basenames usually match, but not always - see + // gliph2 below). + // + // .collect()-produced list channels are wrapped via `.map { l -> [l] }` + // before every .combine() below - otherwise .combine() flattens the + // list's contents into the tuple instead of keeping it as one element. + // + // .combine() (no `by:`) is a safe pairing here, not a risky cross-product: + // every channel below is a whole-run aggregate that emits exactly one item + // per pipeline run (a .collectFile() result or a .collect()'d list), so + // there's nothing to key by - there's only ever one item on each side. + ch_discovery_report = sample_stats_agg + .combine(ANNOTATE.out.concat_cdr3_sorted) + .combine(COMPARE.out.shared_cdr3) + .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 -> + // template_details_part1.qmd pulls in template_sample.qmd via a + // {{< include >}} shortcode resolved relative to its own directory, + // so that sibling file has to be staged alongside it too - Nextflow + // only stages the single notebook file named in the tuple otherwise. + 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 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] } + tuple( + file(params.template_details_part1), + report_files, + staged_layout + ) + } + ch_reports = ch_reports.mix(ch_details_part1_report) + + // template_giana.qmd/template_gliph.qmd need patient-level clustering + // outputs that only exist when 'patient' is in workflow_level (GIANA's + // concat crashes outright otherwise), so template_patient_clustering.qmd + // is resolved to an "on" (includes both) or "off" (placeholder) wrapper, + // mirroring the template_pheno.qmd trick above. + def details_part2_base = sample_stats_agg + .combine(ANNOTATE.out.concat_cdr3_sorted) + .combine(COMPARE.out.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) + + // template_details_part2.qmd pulls in these files via {{< include >}} + // shortcodes resolved relative to its own directory, so they have to be + // staged alongside it too - see the equivalent part1 comment above. + 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 + // gliph2 files keep their patient-prefixed basename (e.g. + // "patientA_all_motifs.txt") all the way through, since + // template_gliph.qmd reads that same name from each patient's + // subdir - source and dest basenames match here. + 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 + 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 -> + tuple( + file(params.template_details_part2), + [sample_stats_csv, concat_cdr3_sorted, shared_cdr3] + 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.name}", shared_cdr3.name] + ] + part2_staged_layout_extra + ) + } + } + ch_reports = ch_reports.mix(ch_details_part2_report) REPORT( ch_reports )