Introduction to ECLIPSE

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 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 scRepertoire package.

This vignette will walk through the basic workflow of the ECLIPSE package. Please refer to our pre-print for more details about this method.

Installation and loading

We need to first load three packages to run the vignette:

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:

install_github("eburns10/ECLIPSE")
library(ECLIPSE)

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). 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:

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) 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:

# 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)
#> [1] "/tmp/Rtmp210Elj/Rinst1b8221977df0/ECLIPSE/extdata/vignette/S15N"
#> [2] "/tmp/Rtmp210Elj/Rinst1b8221977df0/ECLIPSE/extdata/vignette/S15T"
#> [3] "/tmp/Rtmp210Elj/Rinst1b8221977df0/ECLIPSE/extdata/vignette/S3N" 
#> [4] "/tmp/Rtmp210Elj/Rinst1b8221977df0/ECLIPSE/extdata/vignette/S3T"

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():

# Generates the vector of file paths
contig_paths <- list.files(path = vignette_dir,
                           pattern = "all_contig", 
                           recursive = TRUE, 
                           full.names = TRUE)
contig_paths
#> [1] "/tmp/Rtmp210Elj/Rinst1b8221977df0/ECLIPSE/extdata/vignette/S15N/all_contig_annotations.csv.gz"
#> [2] "/tmp/Rtmp210Elj/Rinst1b8221977df0/ECLIPSE/extdata/vignette/S15T/all_contig_annotations.csv.gz"
#> [3] "/tmp/Rtmp210Elj/Rinst1b8221977df0/ECLIPSE/extdata/vignette/S3N/all_contig_annotations.csv.gz" 
#> [4] "/tmp/Rtmp210Elj/Rinst1b8221977df0/ECLIPSE/extdata/vignette/S3T/all_contig_annotations.csv.gz"

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:

# Existing cell barcodes. They aren't the original!
head(rownames(vignette_object[[]]))
#> [1] "CTAGTGATCAGCAACT-20" "CCACCTAGTGAGGCTA-20" "TTCTCAACACCCATTC-20"
#> [4] "CGTCAGGTCCCATTAT-20" "GGGTTGCTCCTCAATT-20" "ATTCTACCAAAGTCAA-20"

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:

# 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()
#> [1] "CTAGTGATCAGCAACT-1" "CCACCTAGTGAGGCTA-1" "TTCTCAACACCCATTC-1"
#> [4] "CGTCAGGTCCCATTAT-1" "GGGTTGCTCCTCAATT-1" "ATTCTACCAAAGTCAA-1"

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”).

# Looking at the vector of file_paths
contig_paths
#> [1] "/tmp/Rtmp210Elj/Rinst1b8221977df0/ECLIPSE/extdata/vignette/S15N/all_contig_annotations.csv.gz"
#> [2] "/tmp/Rtmp210Elj/Rinst1b8221977df0/ECLIPSE/extdata/vignette/S15T/all_contig_annotations.csv.gz"
#> [3] "/tmp/Rtmp210Elj/Rinst1b8221977df0/ECLIPSE/extdata/vignette/S3N/all_contig_annotations.csv.gz" 
#> [4] "/tmp/Rtmp210Elj/Rinst1b8221977df0/ECLIPSE/extdata/vignette/S3T/all_contig_annotations.csv.gz"

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:

# 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)
#> 
#>   1   2   3   4 
#> 250 250 250 250

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:

# Show donor column
table(vignette_object$donor)
#> 
#> S15  S3 
#> 500 500

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):

# Show Sample column
table(vignette_object$Sample)
#> 
#> S15_N S15_T  S3_N  S3_T 
#>   250   250   250   250

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 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
  5. 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)
  6. Formatting the object for downstream analysis with 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:

# 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":

# 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")
#> 
#> Reading TCR contig data in...
#> QC Filtering:
#>  - 2682 initial contigs found before filtering
#>  - 2682 contigs were from true cells (coming from 1000 cells)
#>  - 0 contigs were removed for not being high confidence as defined by Cell Ranger
#>  - 62 orphan contigs were found with no observed pairing. Cells containing these contigs are unlikely to have a clonotype predicted
#>  - 622 contigs were removed for other reasons (ex. irregular CDR3, TCRγ/δ contig, duplicated, etc...)
#>  - After all filtering, 2060 contigs from 1000 true cells remained
#> 
#> Finding clones with extra chains...
#> Running donor 1/2 (S15):
#> 5 clones with an extra TCRα chain were detected, comprising 14.2% of cells (71 total cells)
#> 0 clones with an extra TCRβ chain were detected, comprising 0% of cells (0 total cells)
#> 
#> Running donor 2/2 (S3):
#> 2 clones with an extra TCRα chain were detected, comprising 3.8% of cells (19 total cells)
#> 0 clones with an extra TCRβ chain were detected, comprising 0% of cells (0 total cells)
#> 
#> Running VDJdive::clonoStats() for clone EM assignment...
#> Extracting results from VDJdive...
#> Finalizing clonal assignments...

Plot from ECLIPSE

#> 5 cells were detected in clones that appear to be MAIT or iNKT cells
#> 84.1% of cells in the Seurat object were assigned a clonotype using high confidence prediction from VDJdive::clonoStats()
#> 100% of cells in the Seurat object were assigned a clonotype in the end (either from prediction or traditional analysis)
#> These final clonal annotations are stored in CTaa
#> Summary of the top 5 largest clones:
#> # A tibble: 5 × 4
#> # Groups:   CTaa [5]
#>   CTaa                                   Sample Clone_Size Percent_of_Repertoire
#>   <chr>                                  <chr>       <int>                 <dbl>
#> 1 CALGSNFGNEKLTF:::CGCKSYGKLTF_CASSQGLA… S15_N          47                  18.8
#> 2 CAVQGSGGSYIPTF_CSARTLASGDNEQFF         S15_N          40                  16  
#> 3 CAASMSGTYKYIF_CASSLAGTTQETQYF          S3_T           30                  12  
#> 4 CAVGPDNYGNNRLAF_CASSIAPGTSGGYNEQFF     S3_T           29                  11.6
#> 5 CLVGAGSYQLTF_CASSPLDNSGNTIYF           S15_T          25                  10

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:

# Final ECLIPSE clonotype
head(object_after_ECLIPSE$CTaa) %>% as.vector()
#> [1] "CAASMSGTYKYIF_CASSLAGTTQETQYF" "NA_CASSLGGSYEQYF"             
#> [3] "CAASMSGTYKYIF_CASSLAGTTQETQYF" "CAGFNAGNMLTF_CASSWGNEQFF"     
#> [5] "CAASMSGTYKYIF_CASSLAGTTQETQYF" "CALNTGGFKTIF_CASSPRGPYNEQFF"

We can also compare these final clonotypes to what traditional analysis would have determined, now stored in the scR_CTaa column:

# Clonotype from traditional analysis
head(object_after_ECLIPSE$scR_CTaa) %>% as.vector()
#> [1] "CAASMSGTYKYIF_CISVPAALGIRDRGNLEETQYF;CASSLAGTTQETQYF"
#> [2] "NA_CASSLGGSYEQYF"                                    
#> [3] "CAASMSGTYKYIF_CASSLAGTTQETQYF"                       
#> [4] "CAGFNAGNMLTF_CSARDFADSGTSTDTQYF;CASSWGNEQFF"         
#> [5] "CAASMSGTYKYIF_CASSLAGTTQETQYF"                       
#> [6] "CALNTGGFKTIF_CASSPRGPYNEQFF"

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:

object_after_ECLIPSE <- tcrDoubletDetect(object_after_ECLIPSE,
                                         singleChainLimit = 2,
                                         totalChainLimit = 4)
#> 4 cells (0.4% of total) were removed due to having >2 TCRα or TCRβ chains originally
#> 0 cells (0% of total) were removed due to having >4 total chains originally

Plots from tcrDoubletDetectPlots from tcrDoubletDetect 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 scRepertoire

At this point we’re ready for downstream analysis. ECLIPSE generates Seurat objects formatted for use with the popular 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 scRepertoire. For example, we can look at clonal sharing across samples:

clonalCompare(object_after_ECLIPSE,
              top.clones = 10,
              graph = "alluvial",
              cloneCall = "aa",
              group.by = "Sample") +
    theme(legend.position = "none")

Plot showing clone sharing

We can also visualize which samples have a larger fraction of expanded clonotypes:

clonalHomeostasis(object_after_ECLIPSE,
                  cloneCall = "aa",
                  group.by = "Sample")

Plot showing clone sizes

Or we can look at repertoire diversity across samples:

clonalDiversity(object_after_ECLIPSE,
                metric = "shannon",
                cloneCall = "aa",
                group.by = "Sample")

Plot showing TCR diversity

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. For more details about our method, please refer to our pre-print.

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:

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:

s15_contigs <- postqc_contigs %>% filter(donor == "S15")

three_chain_clones <- findThreeChainClones(s15_contigs,
                                           chain = "a",
                                           mode = "show",
                                           printStats = TRUE)
#> Returning a list. The first table shows all beta chains predicted to have a double alpha pairing. The second table shows how/why many of these clones were called as such. Please note, the second table is pre-filtering, so not all clones may been in the first table since some may have been filtered out.

This provides a list containing:

  1. A table of all clones with 2 TCRα chains after filtering
three_chain_clones[[1]]
#> # A tibble: 5 × 2
#>   beta_chain         predicted_double_alpha_pairing 
#>   <chr>              <chr>                          
#> 1 CASSFMAGPYNEQFF    CLPRLEYGNKLVF;CASPRGGGSQGNLIF  
#> 2 CASSLEAAGVGEQYF    CTTASWVPKAAGNKLTF;CAVGGSGNTPLVF
#> 3 CASSQGLAGLEETQYF   CALGSNFGNEKLTF;CGCKSYGKLTF     
#> 4 CASTTTDRGRGANVLTF  CAVSNFGNEKLTF;CAVMTTDSWGKLQF   
#> 5 CAWSGEGLAGVSYNEQFF CSERIFNKFYF;CALRGGGFKTIF
  1. Pre-filtering table that shows how some of these clones were called
three_chain_clones[[2]]
#>   total_cells cell_counts_per_alpha_pairing       beta_chain
#> 1          38                       26 11 1 CASSQGLAGLEETQYF
#>   predicted_double_alpha_pairing observed_alpha_pairing1
#> 1     CALGSNFGNEKLTF;CGCKSYGKLTF          CALGSNFGNEKLTF
#>      observed_alpha_pairing2 observed_alpha_pairing3
#> 1 CALGSNFGNEKLTF;CGCKSYGKLTF             CGCKSYGKLTF

ECLIPSE additional arguments

Additional arguments for the ECLIPSE function
Argument Description
verbose 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
returnFilteredContigs By default FALSE. TRUE would return the contigs after QC filtering
allowDoubleChains 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
combineTcrArgs 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)
detailedEclipseStats 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…)
format 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

Seurat object columns description

Columns made by the ECLIPSE function
Column Description
highConfChains 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
scR_CTgene, scR_CTnt, scR_CTaa, scR_CTstrict, scR_conalProportion, scR_clonalFrequency, scR_cloneSize columns added by scRepertoire::combineExpression, see scRepertoire documentation for details. These provide information about the cell if it was called with 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
cloneCallSource whether the final clonotype list (in CTaa) is from the prediction of VDJdive or from traditional analysis from scRepertoire
CTaa, clonalProportion, clonalFrequency, cloneSize 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
cell_unconventional_subset, clone_unconventional_subset 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
EM_max_prop, EM_2ndmax_prop, EM_max_2ndmax_ratio 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

Session info

sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so;  LAPACK version 3.12.0
#> 
#> locale:
#>  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#>  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
#>  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#>  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#>  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
#> 
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#>  [1] knitr_1.52         ECLIPSE_0.99.0     scRepertoire_2.9.1 lubridate_1.9.5   
#>  [5] forcats_1.0.1      stringr_1.6.0      dplyr_1.2.1        purrr_1.2.2       
#>  [9] readr_2.2.0        tidyr_1.3.2        tibble_3.3.1       ggplot2_4.0.3     
#> [13] tidyverse_2.0.0    remotes_2.5.0      BiocStyle_2.41.0  
#> 
#> loaded via a namespace (and not attached):
#>   [1] gridExtra_2.3.1             testthat_3.3.2             
#>   [3] rlang_1.3.0                 magrittr_2.0.5             
#>   [5] otel_0.2.0                  matrixStats_1.5.0          
#>   [7] compiler_4.6.1              reshape2_1.4.5             
#>   [9] systemfonts_1.3.2           vctrs_0.7.3                
#>  [11] ggalluvial_0.12.6           gsl_2.1-9                  
#>  [13] quantreg_6.1                pkgconfig_2.0.3            
#>  [15] fastmap_1.2.0               XVector_0.53.0             
#>  [17] labeling_0.4.3              ggraph_2.2.2               
#>  [19] utf8_1.2.6                  rmarkdown_2.32             
#>  [21] tzdb_0.5.0                  ragg_1.5.2                 
#>  [23] MatrixModels_0.5-4          xfun_0.61                  
#>  [25] cachem_1.1.0                jsonlite_2.0.0             
#>  [27] DelayedArray_0.39.8         BiocParallel_1.47.0        
#>  [29] tweenr_2.0.3                parallel_4.6.1             
#>  [31] R6_2.6.1                    bslib_0.12.0               
#>  [33] stringi_1.8.9               RColorBrewer_1.1-3         
#>  [35] parallelly_1.48.0           brio_1.1.5                 
#>  [37] GenomicRanges_1.65.4        jquerylib_0.1.4            
#>  [39] Rcpp_1.1.2                  Seqinfo_1.3.2              
#>  [41] SummarizedExperiment_1.43.0 iNEXT_3.0.2                
#>  [43] future.apply_1.20.2         IRanges_2.47.5             
#>  [45] Matrix_1.7-6                splines_4.6.1              
#>  [47] igraph_2.3.4                timechange_0.4.0           
#>  [49] tidyselect_1.2.1            abind_1.4-8                
#>  [51] yaml_2.3.12                 viridis_0.6.5              
#>  [53] codetools_0.2-20            listenv_1.1.0              
#>  [55] plyr_1.8.9                  lattice_0.23-1             
#>  [57] Biobase_2.73.2              withr_3.0.3                
#>  [59] S7_0.2.2                    evaluate_1.0.5             
#>  [61] survival_3.8-12             future_1.76.0              
#>  [63] polyclip_1.10-7             pillar_1.11.1              
#>  [65] BiocManager_1.30.27         MatrixGenerics_1.25.0      
#>  [67] stats4_4.6.1                generics_0.1.4             
#>  [69] sp_2.2-3                    S4Vectors_0.51.10          
#>  [71] hms_1.1.4                   scales_1.4.0               
#>  [73] globals_0.19.1              immApex_1.7.1              
#>  [75] glue_1.8.1                  maketools_1.3.2            
#>  [77] tools_4.6.1                 sys_3.4.3                  
#>  [79] SparseM_1.84-2              buildtools_1.0.0           
#>  [81] graphlayouts_1.2.5          dotCall64_1.2              
#>  [83] cowplot_1.2.0               tidygraph_1.3.1            
#>  [85] grid_4.6.1                  evmix_2.12                 
#>  [87] SingleCellExperiment_1.35.2 ggforce_0.5.0              
#>  [89] VDJdive_1.15.3              cli_3.6.6                  
#>  [91] textshaping_1.0.5           spam_2.11-4                
#>  [93] viridisLite_0.4.3           S4Arrays_1.13.2            
#>  [95] ggdendro_0.2.0              gtable_0.3.6               
#>  [97] sass_0.4.10                 digest_0.6.39              
#>  [99] progressr_1.0.0             BiocGenerics_0.59.12       
#> [101] SparseArray_1.13.4          ggrepel_0.9.8              
#> [103] rjson_0.2.23                SeuratObject_5.4.0         
#> [105] farver_2.1.2                memoise_2.0.1              
#> [107] htmltools_0.5.9             lifecycle_1.0.5            
#> [109] MASS_7.3-66