--- title: "Introduction to ECLIPSE" subtitle: "ECLIPSE: Enhanced CLonotypic Inference via Prediction of Single-cell Expression" author: - name: Ethan C. Burns email: ethan.burns@yale.edu affiliation: Yale School of Medicine, New Haven, CT, USA - name: David A. Braun affiliation: Yale School of Medicine, New Haven, CT, USA package: ECLIPSE abstract: | This vignette displays how to use the `ECLIPSE` package for the analysis of single-cell T cell receptor sequencing data (scTCR-seq). The package utilizes Seurat objects and `cellranger vdj/multi` output files as its main inputs. Core functionalities include the annotation of T cell clones, the modification of `cellranger vdj/multi` output files to reflect clones with extra TCR chains, and the filtering of cells with excessive TCR chains from Seurat objects. Please refer to our [pre-print](https://www.biorxiv.org/content/10.64898/2026.02.18.706444v1) for more details. output: BiocStyle::html_document vignette: > %\VignetteIndexEntry{ECLIPSE} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r knitr, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", dev = "ragg_png" ) ``` # Introduction Traditional analysis of single-cell TCR sequencing (scTCR-seq) data defines T cell clones based on the paired CDR3 sequences of the TCRα and TCRβ chains. However, as >50% of cells in some samples possess only 1 or >2 TCR chains, this approach is ill-suited to interpret the clonotypes of a substantial proportion of cells. ECLIPSE, building off of `r Biocpkg("VDJdive")`, was developed to address this limitation of standard scTCR-seq analysis. By analyzing chain pairings across a sample, it is able to resolve clonal ambiguity, allowing for the prediction of chains lost of dropout and the deciphering of true biological expression of extra TCR chains from technical artifacts. With less missing data and larger clone sizes, users are then able to make best use of their scTCR-seq data. The ECLIPSE workflow is also quite streamlined, typically utilizing just 2 core functions (`ECLIPSE` --> `tcrDoubletDetect`). After completing these two functions, users are ready for convenient downstream analysis with the `r Biocpkg("scRepertoire")` package. This vignette will walk through the basic workflow of the ECLIPSE package. Please refer to our [pre-print](https://www.biorxiv.org/content/10.64898/2026.02.18.706444v1) for more details about this method. # Installation and loading We need to first load three packages to run the vignette: ```{r loadLibraries, results = "hide", message = FALSE, warning = FALSE} library(remotes) # only needed to insall the package library(tidyverse) # needed for data wrangling (or use base R instead) library(scRepertoire) # needed for downstream analysis ``` Please use the following code to download `ECLIPSE` from GitHub and load it: ```{r githubInstall, eval = FALSE} install_github("eburns10/ECLIPSE") ``` ```{r loadECLIPSE} library(ECLIPSE) ``` ```{r loadKnitr, echo = FALSE, message = FALSE} require(knitr) ``` # Test dataset ECLIPSE includes a small test dataset of 1,000 cells from 2 patients (S3 and S15) with clear cell renal cell carcinoma (derived from [Braun et al., 2021]( https://doi.org/10.1016/j.ccell.2021.02.013)). Each patient has two samples: one from tumor tissue and one from matched adjacent normal tissue. To load the dataset in, we can use the following code: ```{r loadSeuratObject} data("vignette_object") ``` # Preparing inputs for `ECLIPSE` 2 main inputs are required for `ECLIPSE`: a Seurat object that contains scRNA-seq data and contig files from `cellranger vdj/multi` (preferably `all_contig_annotations.csv` files). ## Contig files The first input is contig files from `cellranger multi` or `cellranger vdj`, specifically `all_contig_annotations.csv` or `filtered_contig_annotations.csv` files. **We strongly recommend using all_contig_annotations** files as the filtered contig files remove some biologically true contigs (ex. TCRα chains that use the TRDV1 gene segment, [see here]( https://doi.org/10.3389/fimmu.2023.1286688)) as well as non-productive contigs. While these non-productive chains may not influence T cell antigen specificity, their presence in the contig files allows for enhanced fidelity of clonal tracking by `ECLIPSE`. To feed the contig files into `ECLIPSE`, there are two options: ### Option 1: use folder paths For this option, a vector of folder paths that contain contig files is needed. `ECLIPSE` will find the contig files within the folders you provide, however it requires that each folder only contains 1 set of contig files (i.e. you cannot put the contig files from each sample into 1 folder). In order to create a vector of folder paths, we can use the `list.dirs()` function: ```{r getFolderPath} # Finding where the vignette data for ECLIPSE is stored vignette_dir <- system.file("extdata/vignette", package = "ECLIPSE") # Generates the vector of folder paths and excludes the current directory tcr_folder_paths <- list.dirs(path = vignette_dir, full.names = TRUE)[-1] print(tcr_folder_paths) ``` ### Option 2: use contig file paths The second option also requires a vector of paths, but instead you specify the full path to the contig file. This option would be better if you have many contig files from different samples in one folder. To make the vector, here we can use `list.files()`: ```{r getFilePath} # Generates the vector of file paths contig_paths <- list.files(path = vignette_dir, pattern = "all_contig", recursive = TRUE, full.names = TRUE) contig_paths ``` ## Seurat object meta.data 4 arguments of `ECLIPSE` must be provided column names from the Seurat meta.data: `original_barcode`, `batch`, `donor`, and `group`. We will create these columns now. ### Column to pass to `original_barcode` This column must contain the original 10X barcode for each cell. Because many scRNA-seq analysis pipelines add prefixes/suffixes to cell barcodes, this column is required so that the TCR contigs (which contain the original 10X barcodes) can be matched to their respective cell in the Seurat object. Typically, barcodes will end with -1, but this may not always be true, such as if `cellranger aggr` was used. Always check your raw contig and counts files to be sure! For our dataset, the raw contig files have cell barcode suffixes of -1, so let's look at the barcodes in our Seurat object to see if they will match: ```{r checkBarcodes} # Existing cell barcodes. They aren't the original! head(rownames(vignette_object[[]])) ``` As you can see, they do not match. Let's change the suffixes to -1 and make a new column called `cell_barcode` that we can pass to the `original_barcode` argument of ECLIPSE: ```{r makeBarcodeColumn} # Making a column with these barcodes where the suffix is -1 vignette_object$cell_barcode <- str_replace(rownames(vignette_object[[]]), pattern = "-.*$", replacement = "-1") head(vignette_object$cell_barcode) %>% as.vector() ``` ### Column to pass to `batch` This column tells `ECLIPSE` which contig file holds the TCR information for each cell in the Seurat object. The position of the contig file in the vector must be provided here (ex. 2, but not "2"). ```{r checkContigPaths} # Looking at the vector of file_paths contig_paths ``` For example, `contig_paths[1]` holds a path for the TCR contigs for a cells from the tumor tissue of S15, so any cell from that sample must have 2 (integer) but not "2" (character) in this new column. We can make this column, here called `index`, as such: ```{r makeBatchColumn} # Since normal tissue from sample 15 is first (S15N), it is 1. # Tumor tissue from 15 is next (S15T), so it is 2, etc... vignette_object[[]] <- vignette_object[[]] %>% mutate(index = case_when( Sample == "S15_N" ~ 1, Sample == "S15_T" ~ 2, Sample == "S3_N" ~ 3, Sample == "S3_T" ~ 4)) # Checking the column table(vignette_object$index) ``` ### Column to pass to `donor` Next, we need to tell `ECLIPSE` which donor (or mouse) each cell was from. As `ECLIPSE` calls clonotypes by utilizing chain pairing across a repertoire, the more cells that are available, the more accurate this prediction will be (regardless of time/condition/treatment). For this reason, a column is required to provide the donor/mouse. Even if we want to compare S3 tumor vs. normal and S15 tumor vs. normal downstream, here we must provide S3 and S15. Fortunately, this column is already created for us, and it should be already present in most real-world uses. Let's look at this column, called `donor`: ```{r checkDonorColumn} # Show donor column table(vignette_object$donor) ``` ### Column to pass to `group` Lastly, we need to tell `ECLIPSE` how we want the final clonal statistics (i.e. clone cell count, frequency, etc..) to be calculated after clones are called. For our example, either S15/S3 or S3T/S3N/S15T/S15N would be equally valid. If a clone is only 1 cell, then if we provide the donor, the frequency will be 1/500 vs. if we provide the sample, then it will be 1/250. **Please note that this column can and often will be the same as the ** **column passed to donor**. Let's choose by sample here, which again is already created for us through the column `Sample` (and typically will be created in most real cases): ```{r checkSampleColumn} # Show Sample column table(vignette_object$Sample) ``` # Running the `ECLIPSE` function ## Description of function Now we are ready to run the `ECLIPSE` function to analyze our scTCR-seq data. Briefly, this function does the following: 1. Reading in contig data 2. Filtering contigs (i.e. must be in Seurat, high_confidence, not duplicated, TCRα or TCRβ, have a valid CDR3, etc...) 3. Calling `findThreeChainClones` to find clones with biological expression of 3 total TCR chains and modifying the contigs to address this 4. Utilizing the implementation of the EM algorithm in `r Biocpkg("VDJdive")`'s clonoStats() function. This function analyzes chain pairings across a sample to determine the probability of each potential chain pairing for cells possessing only 1 or >2 total TCR chains 4. Determining the final clonotype (stored in a column called `CTaa`). If it is highly likely the cell is in 1 clone, then that clone is used. If no clonotype is predicted with high confidence, then clonal prediction is not performed, and the final clonotype is instead determined by `scRepertoire::combineTCR()` using parameters passed to `ECLIPSE` from the `combineTcrArgs` argument (see supplemental information section) 5. Formatting the object for downstream analysis with `r Biocpkg("scRepertoire")` For a full list of columns added by ECLIPSE and their description, please refer to the supplemental information section. ## Running `ECLIPSE` To run `ECLIPSE`, there are two options, depending on whether we generated a vector of folders containing contigs or contig file paths. If we want to use a vector of folder paths, we can use the following code: ```{r runEclipseFolder} # Option 1: Running with paths of folders containing contigs object_after_ECLIPSE <- ECLIPSE(contig_type = "all", contig_folder_paths = tcr_folder_paths, seurat_object = vignette_object, batch = "index", donor = "donor", group = "Sample", original_barcode = "cell_barcode", verbose = FALSE) ``` Note that we use the `contig_folder_paths` argument, omit the `contig_file_paths` argument, and `contig_type = "all"`. Together, this tells `ECLIPSE` to search for `all_contig_annotations.csv` files in the folders that we provide. If we want to provide TCR contig file paths, we can use the `contig_file_paths` argument, omit the `contig_folder_paths` argument, and `contig_type = "manual"`: ```{r runEclipseFile, fig.alt = "Plot from ECLIPSE"} # Option 2: Running with contig paths object_after_ECLIPSE <- ECLIPSE(contig_type = "manual", contig_file_paths = contig_paths, seurat_object = vignette_object, batch = "index", donor = "donor", group = "Sample", original_barcode = "cell_barcode") ``` As seen above, by default (verbose = TRUE), `ECLIPSE` will print some key stats during filtering as well as a QC plot that shows how well clonal prediction went. For this experiment, most cells (shown in orange) received their final clonotype by prediction. Many of these cells also were fully assigned to one clone by the EM algorithm (i.e. y = 1), so overall the analysis went quite well. Let's take a look at the final clonotypes that `ECLIPSE` decided on, now stored in the `CTaa` column of the Seurat meta.data: ```{r checkClonotypes} # Final ECLIPSE clonotype head(object_after_ECLIPSE$CTaa) %>% as.vector() ``` We can also compare these final clonotypes to what traditional analysis would have determined, now stored in the `scR_CTaa` column: ```{r checkTraditionalClonotypes} # Clonotype from traditional analysis head(object_after_ECLIPSE$scR_CTaa) %>% as.vector() ``` ECLIPSE also provides clone sizes and clonal proportions in the `clonalFrequency` and `clonalProportion columns,` respectively, with matched columns having a scR_prefix to show what would happen with traditional analysis. For a full list of columns added by `ECLIPSE`, please refer to the supplemental information section. # Running `tcrDoubletDetect` Now that the `ECLIPSE` function has been ran, we should now run the 2nd core ECLIPSE function, `tcrDoubletDetect`, to filter out cells with excessive TCR chains (these cells are likely a mix of doublets and cells with TCR contamination). Here we remove any cells with >2 TCRα chains, >2 TCRβ chains, or >4 total (α + β) chains: ```{r runDoubletDetect, fig.alt = "Plots from tcrDoubletDetect"} object_after_ECLIPSE <- tcrDoubletDetect(object_after_ECLIPSE, singleChainLimit = 2, totalChainLimit = 4) ``` This function by default also prints plots that may be useful in setting additional filtering thresholds. Here we can see that cells with 4 total TCR chains have much higher RNA counts, so we may want to remove these cells or any cells with ~ >7k nCount_RNA as they may be enriched for doublets. # Downstream analysis with `r Biocpkg("scRepertoire")` At this point we're ready for downstream analysis. ECLIPSE generates Seurat objects formatted for use with the popular `r Biocpkg("scRepertoire")` package, so functions introduced in that package typically can be used here as well. One key thing to note is that since `ECLIPSE` predicts clones based on CDR3 amino acid sequences, we must always use the argument `cloneCall = "aa"` in scRepertoire functions. Shown below are a couple potential types of downstream analysis made possible with `r Biocpkg("scRepertoire")`. For example, we can look at clonal sharing across samples: ```{r runCompareClonotypes, fig.alt = "Plot showing clone sharing"} clonalCompare(object_after_ECLIPSE, top.clones = 10, graph = "alluvial", cloneCall = "aa", group.by = "Sample") + theme(legend.position = "none") ``` We can also visualize which samples have a larger fraction of expanded clonotypes: ```{r runClonalHomeostasis, fig.alt = "Plot showing clone sizes"} clonalHomeostasis(object_after_ECLIPSE, cloneCall = "aa", group.by = "Sample") ``` Or we can look at repertoire diversity across samples: ```{r runClonalDiversity, fig.alt = "Plot showing TCR diversity"} clonalDiversity(object_after_ECLIPSE, metric = "shannon", cloneCall = "aa", group.by = "Sample") ``` # Conclusions We hope this vignette is a helpful introduction to using ECLIPSE. If you have any questions, please email ethan.burns [@] yale.edu or use our issues [page](https://github.com/eburns10/ECLIPSE/issues). For more details about our method, please refer to our [pre-print](https://www.biorxiv.org/content/10.64898/2026.02.18.706444v1). # Supplemental information ## Helper function: `findThreeChainClones` While not discussed above, ECLIPSE contains a third function that may may be of use. This function was designed to help the `ECLIPSE` function find clones with biological expression of 3 TCR chains, but can be also used to see which clones were called with 3 TCR chains and how that occurred. To see these clones we can use the code below. First, we generate QC filtered contigs using the `returnFilteredContigs` argument of `ECLIPSE`: ```{r makeContigs} postqc_contigs <- ECLIPSE(contig_type = "all", contig_folder_paths = tcr_folder_paths, seurat_object = vignette_object, batch = "index", donor = "donor", group = "Sample", original_barcode = "cell_barcode", verbose = FALSE, returnFilteredContigs = TRUE) ``` Next, we filter on a sample we are interested in, and pass the contigs onto `findThreeChainClones` using ` mode = "show"` and `chain = "a"` to show clones with 2 TCRα chains: ```{r runFindThreeChainClones} s15_contigs <- postqc_contigs %>% filter(donor == "S15") three_chain_clones <- findThreeChainClones(s15_contigs, chain = "a", mode = "show", printStats = TRUE) ``` This provides a list containing: 1. A table of all clones with 2 TCRα chains after filtering ```{r getOutput1} three_chain_clones[[1]] ``` 2. Pre-filtering table that shows how some of these clones were called ```{r getOutput2} three_chain_clones[[2]] ``` ## `ECLIPSE` additional arguments ```{r makeArgsTable, echo = FALSE} args <- c("`verbose`", "`returnFilteredContigs`", "`allowDoubleChains`", "`combineTcrArgs`", "`detailedEclipseStats`", "`format`") descriptions <- c( paste( "By default `TRUE`. Prints stats such as QC filtering efficacy,", "how many clones were called with 3 chains, and final percentages", "of cells with a TCR called" ), paste( "By default `FALSE`. `TRUE` would return the contigs after", "QC filtering" ), paste( "By default `\"both\"`, whereby clones can have 2 TCRα or", "TCRβ chains. If `\"alpha\"` or `\"beta\"` is provided, only", "clones with 2 of that type will be permitted. If `\"neither\"` is", "provided, then only clones with 2 chains will be called" ), paste( "A list that determines how `scRepertoire::combineTCR()` is called.", "As `ECLIPSE` runs `combineTCR/combineExpression` to allow users to", "combine the predicted `ECLIPSE` clonotype with a", "traditionally-defined clonotype, this tells which arguments", "`combineTCR` should be run with. Specifically, it tells the 4", "arguments 1) `removeNA`, 2) `removeMulti`, 3) `filterMulti`, and 4)", "`filterNonproductive` in that order. If `ECLIPSE` cannot call a", "clonotype with high accuracy, the clonal annotation from this will", "be used as the final clonotype. By default, this is", "`list(FALSE, FALSE, FALSE, TRUE)`, which calls `combineTCR` with its", "default arguments (i.e. no non-productive contigs are retained but", "clones with 1 TCR chain or >2 are allowed)" ), paste( "By default `FALSE`. This adds information about the EM algorithm", "assignment to the Seurat object (i.e. what proportion of the cell", "was assigned to the clone listed, etc...)" ), paste( "By default `\"Blank\"`. This tells how missing data is stored in", "contig files. Typically this is `\"\"` so `\"Blank\"` should be", "specified, however older versions of Cell Ranger use `\"None\"`", "instead. If this is the case, please provide `\"None\"` here" ) ) argsTable <- data.frame("Argument" = args, "Description" = descriptions) kable(argsTable, caption = "Additional arguments for the ECLIPSE function") ``` ## Seurat object columns description ```{r makeColsTable, echo = FALSE} cols <- c("`highConfChains`", paste("`scR_CTgene`, `scR_CTnt`, `scR_CTaa`, `scR_CTstrict`,", "`scR_conalProportion`, `scR_clonalFrequency`,", "`scR_cloneSize`"), "`cloneCallSource`", "`CTaa`, `clonalProportion`, `clonalFrequency`, `cloneSize`", "`cell_unconventional_subset`, `clone_unconventional_subset`", "`EM_max_prop`, `EM_2ndmax_prop`, `EM_max_2ndmax_ratio`") descriptions <- c( paste("all TCR chains that were sequenced in the cell and passed QC", "filtering by `ECLIPSE`. Helpful to compare to final clone (`CTaa`)", "used for analysis"), paste("columns added by `scRepertoire::combineExpression`, see", BiocStyle::Biocpkg("scRepertoire"), "documentation for details. These", "provide information about the cell if it was called with", BiocStyle::Biocpkg("scRepertoire"), "and can be useful in comparing to the final clone", "(`CTaa`) used for analysis. Please note the \"scR_\" prefix is added", "so ECLIPSE can add other columns with the same name that can", "interact with scRepertoire"), paste("whether the final clonotype list (in `CTaa`) is from", "the prediction of ", BiocStyle::Biocpkg("VDJdive"), "or from traditional analysis from", BiocStyle::Biocpkg("scRepertoire")), paste("columns in the style of what would be added by", "`scRepertoire:::combineExpression`. However, `CTaa` here stores", "the final clonotype of the cell as predicted by `ECLIPSE`"), paste("`cell_unconventional_subset` tells if the chains detected suggest", "the cell is a iNKT or MAIT cell. `clone_unconventional_subset` gives", "similar information, but uses the final clonotype of the cell", "instead of what chains were detected in that particular cell (and", "therefore will capture more cells as being a iNKT or MAIT cell). For", "both of these columns, `ECLIPSE` searches for 3 subsets: 1) iNKT", "cells (must have TRAV10 + TRAJ18 + TRBV25-1), 2) conventional MAIT", "cells (must have TRAV1-2 + TRAJ33), and 3) unconventional MAIT cells", "(must have TRAV1-2 + TRAJ12 or TRAV1-2 + TRAJ20. Please note that", "these are for human samples only, murine T cells use different gene", "segments that are not searched for by `ECLIPSE`"), paste("the proportion of the cell assigned by the EM algorithm to the", "most and 2nd most likely clone. The ratio of these two proportions", "(biggest/2nd biggest) is also provided. These columns are only", "included if `detailedEclipseStats == TRUE`") ) colsTable <- data.frame("Column" = cols, "Description" = descriptions) kable(colsTable, caption = "Columns made by the ECLIPSE function") ``` # Session info ```{r checkSessionInfo} sessionInfo() ```