# Quickstart Tutorial

In this tutorial, we will construct a Symphony reference from two PBMC datasets from 2 technologies (10x 3’v1 and 3’v2), then map a third dataset from a new technology (10x 5’) with Symphony. The analysis follows from Fig. 2 of the paper (but with downsampled datasets to fit within CRAN limits on subdirectory size).

# Installation

Install Symphony with standard commands.

    install.packages('symphony')

Once Symphony is installed, load it up!

    library(symphony)

    # Other packages for this tutorial
    suppressPackageStartupMessages({
        # Analysis
        library(harmony)
        library(irlba)
        library(data.table)
        library(dplyr)

        # Plotting
        library(ggplot2)
        library(ggthemes)
        library(RColorBrewer)
    })

# Load the data

Get the expression and metadata.

    load('../data/pbmcs_exprs_small.rda')
    load('../data/pbmcs_meta_small.rda')

    dim(pbmcs_exprs_small)

    ## [1] 1767 1200

    dim(pbmcs_meta_small)

    ## [1] 1200    7

    pbmcs_meta_small %>% head(4)

    ##                            cell_id donor nUMI nGene percent_mito cell_type
    ## 17642   fivePrime_GATCGCGAGACCGGAT    5p 6580  2020   0.05714286         B
    ## 4820  threepfresh_GATCGTACAAAGGTGC  3pv2 4426  1207   0.03366471     T_CD8
    ## 13347   fivePrime_AACTCAGGTTACGCGC    5p 4750  1670   0.05642105         B
    ## 10634      threepv1_CGTGATGAGTCCTC  3pv1 4121  1228   0.01504489     T_CD4
    ##       cell_type_broad
    ## 17642               B
    ## 4820                T
    ## 13347               B
    ## 10634               T

Subset the dataset into reference and query.

    idx_query = which(pbmcs_meta_small$donor == "5p") # use 5' dataset as the query
    ref_exp_full = pbmcs_exprs_small[, -idx_query]
    ref_metadata = pbmcs_meta_small[-idx_query, ]
    query_exp = pbmcs_exprs_small[, idx_query]
    query_metadata = pbmcs_meta_small[idx_query, ]

# Build Symphony reference

There are two options for how to build a Symphony reference. Option 1 (`buildReferenceFromHarmonyObj`) is the more modular option, meaning that the user has more control over the preprocessing steps prior to reference compression. Option 2 (`buildReference`) builds a reference starting from expression, automating the procedure more but offering less flexibility.

We’ll demonstrate both options below.

## Option 1: Build from Harmony object (preferred method)

This option consists of more steps than Option 2 but allows your code to be more modular and flexible if you want to do your own preprocessing steps before the Harmony integration step. We recommend this option for most users.

It is important to generate `vargenes_means_sds` (containing variable gene means and standard deviations used to scale the genes) as well as save the loadings for the PCA step.

Starting with the reference expression,

    ref_exp_full[1:5, 1:2] # Sparse matrix with the normalized genes x cells matrix

    ## 5 x 2 sparse Matrix of class "dgCMatrix"
    ##         threepfresh_GATCGTACAAAGGTGC threepv1_CGTGATGAGTCCTC
    ## LYZ                         1.181536                1.766987
    ## FTL                         2.678021                2.370840
    ## HLA-DRA                     1.708152                .
    ## CD74                        1.181536                1.766987
    ## S100A4                      1.708152                2.113817

Select variable genes and subset reference expression by variable genes (the command below will select the top 1,000 genes per batch, then pool them)

    var_genes = vargenes_vst(ref_exp_full, groups = as.character(ref_metadata[['donor']]), topn = 1000)
    ref_exp = ref_exp_full[var_genes, ]
    dim(ref_exp)

    ## [1] 1600  770

Calculate and save the mean and standard deviations for each gene

    vargenes_means_sds = tibble(symbol = var_genes, mean = Matrix::rowMeans(ref_exp))
    vargenes_means_sds$stddev = rowSDs(ref_exp, vargenes_means_sds$mean)
    head(vargenes_means_sds)

    ## # A tibble: 6 × 3
    ##   symbol   mean stddev
    ##   <chr>   <dbl>  <dbl>
    ## 1 LYZ      1.78   1.91
    ## 2 HLA-DRA  1.87   1.69
    ## 3 FTL      3.55   1.22
    ## 4 CD74     2.54   1.53
    ## 5 S100A9   1.62   1.83
    ## 6 S100A4   2.53   1.46

Scale data using calculated gene means and standard deviations

    ref_exp_scaled = scaleDataWithStats(ref_exp, vargenes_means_sds$mean, vargenes_means_sds$stddev, 1)

Run PCA (using SVD), save gene loadings (`s$u`)

    set.seed(0)
    s = irlba(ref_exp_scaled, nv = 20)
    Z_pca_ref = diag(s$d) %*% t(s$v) # [pcs by cells]
    loadings = s$u

Run Harmony integration. It is important to set `return_object = TRUE`.

    set.seed(0)
    ref_harmObj = harmony::RunHarmony(
            data_mat = t(Z_pca_ref),  ## PCA embedding matrix of cells
            meta_data = ref_metadata, ## dataframe with cell labels
            theta = c(2),             ## cluster diversity enforcement
            vars_use = c('donor'),    ## variable to integrate out
            nclust = 100,             ## number of clusters in Harmony model
            max_iter = 20,            ## max number of iterations
            return_object = TRUE      ## return the full Harmony model object
    )

    ## Transposing data matrix

    ## Using automatic lambda estimation

    ## Thetas: 2

    ## Initializing state using k-means centroids initialization

    ## Initializing centroids

    ## Harmony 1/20

    ## Harmony 2/20

    ## Harmony 3/20

    ## Harmony 4/20

    ## Harmony 5/20

    ## Harmony 6/20

    ## Harmony 7/20

    ## Harmony 8/20

    ## Harmony 9/20

    ## Harmony 10/20

    ## Harmony 11/20

    ## Harmony converged after 11 iterations

To run the next function `buildReferenceFromHarmonyObj()`, you need to input the saved gene loadings (`loadings`) and `vargenes_means_sds`.

    # Compress a Harmony object into a Symphony reference
    reference = buildReferenceFromHarmonyObj(
                            ref_harmObj,            # output object from RunHarmony()
                            ref_metadata,           # reference cell metadata
                            vargenes_means_sds,     # gene names, means, and std devs for scaling
                            loadings,               # genes x PCs matrix
                            verbose = TRUE,         # verbose output
                            do_umap = TRUE,         # set to TRUE to run UMAP
                            save_uwot_path = './testing_uwot_model_1') # file path to save uwot model

    ## Save metadata, vargenes (S), and loadings (U)

    ## Save R, Z_orig, Z_corr, and betas from Harmony object

    ## Calculate final L2 normalized reference centroids (Y_cos)

    ## Calculate reference compression terms (Nr and C)

    ## UMAP

    ## Saved uwot model

    ## Finished nicely.

Save Symphony reference for later mapping (modify with your desired output path)

    saveRDS(reference, './testing_reference1.rds')

Let’s take a look at what the reference object contains: * meta_data: metadata * vargenes: variable genes, means, and standard deviations used for scaling * loadings: gene loadings for projection into pre-Harmony PC space * R: Soft cluster assignments * Z_orig: Pre-Harmony PC embedding * Z_corr: Harmonized PC embedding * centroids: locations of final Harmony soft cluster centroids * cache: pre-calculated reference-dependent portions of the mixture model * umap: UMAP coordinates * save_uwot_path: path to saved uwot model (for query UMAP projection into reference UMAP coordinates)

    str(reference)

    ## List of 12
    ##  $ meta_data     :'data.frame':  770 obs. of  7 variables:
    ##   ..$ cell_id        : chr [1:770] "threepfresh_GATCGTACAAAGGTGC" "threepv1_CGTGATGAGTCCTC" "threepv1_AAGAATCTTGGAGG" "threepfresh_CTGAAACCACACTGCG" ...
    ##   ..$ donor          : chr [1:770] "3pv2" "3pv1" "3pv1" "3pv2" ...
    ##   ..$ nUMI           : int [1:770] 4426 4121 1535 11956 6273 1840 2702 4260 3436 3966 ...
    ##   ..$ nGene          : int [1:770] 1207 1228 560 2685 1615 584 945 1188 1360 1486 ...
    ##   ..$ percent_mito   : num [1:770] 0.0337 0.015 0.0261 0.0278 0.0289 ...
    ##   ..$ cell_type      : chr [1:770] "T_CD8" "T_CD4" "B" "DC" ...
    ##   ..$ cell_type_broad: chr [1:770] "T" "T" "B" "DC" ...
    ##  $ vargenes      : tibble [1,600 × 3] (S3: tbl_df/tbl/data.frame)
    ##   ..$ symbol: chr [1:1600] "LYZ" "HLA-DRA" "FTL" "CD74" ...
    ##   ..$ mean  : Named num [1:1600] 1.78 1.87 3.55 2.54 1.62 ...
    ##   .. ..- attr(*, "names")= chr [1:1600] "LYZ" "HLA-DRA" "FTL" "CD74" ...
    ##   ..$ stddev: Named num [1:1600] 1.91 1.69 1.22 1.53 1.83 ...
    ##   .. ..- attr(*, "names")= chr [1:1600] "LYZ" "HLA-DRA" "FTL" "CD74" ...
    ##  $ loadings      : num [1:1600, 1:20] 0.1247 0.0811 0.1181 0.0621 0.118 ...
    ##  $ R             : num [1:100, 1:770] 6.82e-15 9.10e-15 4.30e-10 6.55e-06 2.19e-10 ...
    ##  $ Z_orig        : num [1:20, 1:770] -4.332 0.976 -3.186 0.138 -0.154 ...
    ##   ..- attr(*, "dimnames")=List of 2
    ##   .. ..$ : chr [1:20] "PC_1" "PC_2" "PC_3" "PC_4" ...
    ##   .. ..$ : chr [1:770] "4820" "10634" "8538" "4091" ...
    ##  $ Z_corr        : num [1:20, 1:770] -4.318 0.982 -3.45 -0.647 0.14 ...
    ##   ..- attr(*, "dimnames")=List of 2
    ##   .. ..$ : chr [1:20] "harmony_1" "harmony_2" "harmony_3" "harmony_4" ...
    ##   .. ..$ : chr [1:770] "4820" "10634" "8538" "4091" ...
    ##  $ betas         : num [1:3, 1:20] 0 0.0456 -0.0276 0 1.0923 ...
    ##  $ centroids     : num [1:20, 1:100] 0.98667 -0.00733 -0.00897 -0.07746 -0.03014 ...
    ##  $ cache         :List of 2
    ##   ..$ : num [1:100, 1] 9.26 9.15 1.01 2.11 2.21 ...
    ##   ..$ : num [1:100, 1:20] 111.78 108.84 -5.14 -9.22 -9.62 ...
    ##  $ centroids_pc  :'data.frame':  100 obs. of  20 variables:
    ##   ..$ harmony_1 : num [1:100] 12.07 11.9 -5.1 -4.37 -4.34 ...
    ##   ..$ harmony_2 : num [1:100] -0.0897 0.5405 1.1173 1.126 -13.4035 ...
    ##   ..$ harmony_3 : num [1:100] -0.11 -0.86 1.62 -1.65 5.41 ...
    ##   ..$ harmony_4 : num [1:100] -0.9474 1.4517 3.4934 0.0963 1.6395 ...
    ##   ..$ harmony_5 : num [1:100] -0.369 1.097 -2.194 -0.798 2.239 ...
    ##   ..$ harmony_6 : num [1:100] 1.006 0.0319 3.1755 0.9361 -0.3074 ...
    ##   ..$ harmony_7 : num [1:100] -0.323 1.065 -1.75 -0.103 0.424 ...
    ##   ..$ harmony_8 : num [1:100] 0.263 -0.664 1.796 -0.158 2.605 ...
    ##   ..$ harmony_9 : num [1:100] 0.3112 -0.0644 -6.7224 2.0645 -5.3623 ...
    ##   ..$ harmony_10: num [1:100] 0.1039 0.0475 -3.1802 -2.4699 -4.2902 ...
    ##   ..$ harmony_11: num [1:100] 0.841 -0.314 -2.491 2.116 0.811 ...
    ##   ..$ harmony_12: num [1:100] 0.0611 0.2309 -2.7394 -0.7262 6.1387 ...
    ##   ..$ harmony_13: num [1:100] 0.2167 -0.0994 1.4813 -1.2057 2.7081 ...
    ##   ..$ harmony_14: num [1:100] 0.3124 0.1959 -3.4292 0.0667 11.9295 ...
    ##   ..$ harmony_15: num [1:100] -0.4326 0.0604 9.3133 4.3998 3.3167 ...
    ##   ..$ harmony_16: num [1:100] 0.516 0.204 4.849 -5.036 4.631 ...
    ##   ..$ harmony_17: num [1:100] 0.084 -0.138 -3.681 2.332 4.296 ...
    ##   ..$ harmony_18: num [1:100] -0.141 -0.023 6.053 -4.378 -0.725 ...
    ##   ..$ harmony_19: num [1:100] -0.131 -0.395 1.064 -0.815 1.997 ...
    ##   ..$ harmony_20: num [1:100] -0.512 -0.0299 -8.6529 -0.3687 3.1037 ...
    ##  $ umap          :List of 1
    ##   ..$ embedding: num [1:770, 1:2] -6.19 -5.1 7.61 7.81 6.41 ...
    ##   .. ..- attr(*, "scaled:center")= num [1:2] -0.399 -6.788
    ##   .. ..- attr(*, "dimnames")=List of 2
    ##   .. .. ..$ : chr [1:770] "4820" "10634" "8538" "4091" ...
    ##   .. .. ..$ : chr [1:2] "UMAP1" "UMAP2"
    ##  $ save_uwot_path: chr "./testing_uwot_model_1"

The harmonized embedding is located in the `Z_corr` slot of the reference object.

    dim(reference$Z_corr)

    ## [1]  20 770

    reference$Z_corr[1:5, 1:5]

    ##                 4820      10634        8538       4091      11682
    ## harmony_1 -4.3182082 -4.2682743  -4.4732203  8.8529806 12.5182543
    ## harmony_2  0.9824022  1.2234426 -13.4352427 -2.4438572 -0.1809930
    ## harmony_3 -3.4499540 -3.1225019   4.8807087  0.3364357  0.4249415
    ## harmony_4 -0.6467730 -0.1944421   0.6915965  0.3436260 -5.0423894
    ## harmony_5  0.1395471 -0.9906149   1.3975835 -4.3560081 -0.3085832

Visualize reference UMAP

    reference = readRDS('./testing_reference1.rds')
    umap_labels = cbind(ref_metadata, reference$umap$embedding)
    plotBasic(umap_labels, title = 'Reference', color.mapping = pbmc_colors)

## Option 2: Build from scratch (starting with expression)

This option computes a reference object starting from expression in a unified pipeline, automating the preprocessing steps.

    # Build reference
    set.seed(0)
    reference = symphony::buildReference(
                ref_exp_full,
                ref_metadata,
                vars = c('donor'),         # variables to integrate over
                K = 100,                   # number of Harmony clusters
                verbose = TRUE,            # verbose output
                do_umap = TRUE,            # can set to FALSE if want to run umap separately later
                do_normalize = FALSE,      # set to TRUE if input counts are not normalized yet
                vargenes_method = 'vst',   # method for variable gene selection ('vst' or 'mvp')
                vargenes_groups = 'donor', # metadata column specifying groups for variable gene selection
                topn = 1000,               # number of variable genes to choose per group
                d = 20,                    # number of PCs
                save_uwot_path = './testing_uwot_model_2' # file path to save uwot model
    )

    ## Finding variable genes using vst method

    ## Total 1600 genes for downstream steps

    ## Scaling and PCA

    ## Running Harmony integration

    ## Transposing data matrix

    ## Using automatic lambda estimation

    ## Thetas: 2

    ## Initializing state using k-means centroids initialization

    ## Initializing centroids

    ## Harmony 1/20

    ## Harmony 2/20

    ## Harmony 3/20

    ## Harmony 4/20

    ## Harmony 5/20

    ## Harmony 6/20

    ## Harmony 7/20

    ## Harmony 8/20

    ## Harmony 9/20

    ## Harmony 10/20

    ## Harmony 11/20

    ## Harmony converged after 11 iterations

    ## Computing reference compression terms

    ## Running UMAP

    ## Saved uwot model

    # Save reference (modify with your desired output path)
    saveRDS(reference, './testing_reference2.rds')

Visualize reference UMAP

    reference = readRDS('./testing_reference2.rds')
    umap_labels = cbind(ref_metadata, reference$umap$embedding)
    plotBasic(umap_labels, title = 'Reference', color.mapping = pbmc_colors)

# Map query

In order to map a new query dataset onto the reference, you will need a reference object saved from the steps above, as well as query cell expression and metadata.

The query dataset is assumed to have been normalized in the same manner as the reference cells (here, default is log(CP10k+1) normalization).

    # Read in Symphony reference to map to
    reference = readRDS('./testing_reference1.rds')
    # Map query
    query = mapQuery(query_exp,             # query gene expression (genes x cells)
                     query_metadata,        # query metadata (cells x attributes)
                     reference,             # Symphony reference object
                     do_normalize = FALSE,  # perform log(CP10k) normalization on query
                     do_umap = TRUE)        # project query cells into reference UMAP

    ## Scaling and synchronizing query gene expression

    ## Found 1600 out of 1600 reference variable genes in query dataset

    ## Project query cells using reference gene loadings

    ## Clustering query cells to reference centroids

    ## Correcting query batch effects

    ## UMAP

    ## All done!

Note: Symphony assumes that the query is normalized in the same manner as the reference. Our default implementation currently uses log(CP10k+1) normalization.

Let’s take a look at what the query object contains: * Z: query cells in reference Harmonized embedding * Zq_pca: query cells in pre-Harmony reference PC embedding (prior to correction) * R: query cell soft cluster assignments * Xq: query cell design matrix for correction step * umap: query cells projected into reference UMAP coordinates (using uwot) * meta_data: metadata

    str(query)

    ## List of 7
    ##  $ exp      :Formal class 'dgCMatrix' [package "Matrix"] with 6 slots
    ##   .. ..@ i       : int [1:99120] 1 2 3 5 10 13 14 15 26 27 ...
    ##   .. ..@ p       : int [1:431] 0 267 497 727 942 1197 1384 1608 1877 2047 ...
    ##   .. ..@ Dim     : int [1:2] 1767 430
    ##   .. ..@ Dimnames:List of 2
    ##   .. .. ..$ : chr [1:1767] "LYZ" "FTL" "HLA-DRA" "CD74" ...
    ##   .. .. ..$ : chr [1:430] "fivePrime_GATCGCGAGACCGGAT" "fivePrime_AACTCAGGTTACGCGC" "fivePrime_ACATCAGGTCTTGTCC" "fivePrime_GCCTCTATCATGTCCC" ...
    ##   .. ..@ x       : num [1:99120] 2.15 3.9 5.41 3.4 3.17 ...
    ##   .. ..@ factors : list()
    ##  $ meta_data:'data.frame':   430 obs. of  7 variables:
    ##   ..$ cell_id        : chr [1:430] "fivePrime_GATCGCGAGACCGGAT" "fivePrime_AACTCAGGTTACGCGC" "fivePrime_ACATCAGGTCTTGTCC" "fivePrime_GCCTCTATCATGTCCC" ...
    ##   ..$ donor          : chr [1:430] "5p" "5p" "5p" "5p" ...
    ##   ..$ nUMI           : int [1:430] 6580 4750 4432 5831 5558 5360 4749 5152 4929 4194 ...
    ##   ..$ nGene          : int [1:430] 2020 1670 1668 2027 1890 1388 1661 1805 1417 1575 ...
    ##   ..$ percent_mito   : num [1:430] 0.0571 0.0564 0.0399 0.0326 0.0587 ...
    ##   ..$ cell_type      : chr [1:430] "B" "B" "B" "T_CD4" ...
    ##   ..$ cell_type_broad: chr [1:430] "B" "B" "B" "T" ...
    ##  $ Z        : num [1:20, 1:430] -3.347 -13.307 6.039 0.14 0.196 ...
    ##   ..- attr(*, "dimnames")=List of 2
    ##   .. ..$ : chr [1:20] "harmony_1" "harmony_2" "harmony_3" "harmony_4" ...
    ##   .. ..$ : chr [1:430] "17642" "13347" "13638" "17934" ...
    ##  $ Zq_pca   : num [1:20, 1:430] -3.089 -14.01 6.805 1.207 -0.721 ...
    ##   ..- attr(*, "dimnames")=List of 2
    ##   .. ..$ : chr [1:20] "PC_1" "PC_2" "PC_3" "PC_4" ...
    ##   .. ..$ : chr [1:430] "17642" "13347" "13638" "17934" ...
    ##  $ R        : num [1:100, 1:430] 5.35e-11 1.34e-11 1.80e-08 9.21e-10 8.07e-06 ...
    ##  $ Xq       :Formal class 'dgCMatrix' [package "Matrix"] with 6 slots
    ##   .. ..@ i       : int [1:860] 0 1 0 1 0 1 0 1 0 1 ...
    ##   .. ..@ p       : int [1:431] 0 2 4 6 8 10 12 14 16 18 ...
    ##   .. ..@ Dim     : int [1:2] 2 430
    ##   .. ..@ Dimnames:List of 2
    ##   .. .. ..$ : NULL
    ##   .. .. ..$ : NULL
    ##   .. ..@ x       : num [1:860] 1 1 1 1 1 1 1 1 1 1 ...
    ##   .. ..@ factors : list()
    ##  $ umap     : num [1:430, 1:2] 6.91 6.92 6.97 -4.15 7.02 ...
    ##   ..- attr(*, "dimnames")=List of 2
    ##   .. ..$ : chr [1:430] "17642" "13347" "13638" "17934" ...
    ##   .. ..$ : chr [1:2] "UMAP1" "UMAP2"

Predict query cell types using k-NN. Setting confidence = TRUE also returns the prediction confidence scores (proportion of neighbors with winning vote).

    query = knnPredict(query,       # query object
                       reference,   # reference object
                       reference$meta_data$cell_type, # reference cell labels for training
                       k = 5,       # number of reference neighbors to use for prediction
                       confidence = TRUE)

Query cell type predictions are now in the cell_type_pred_knn column.

    head(query$meta_data)

    ##                          cell_id donor nUMI nGene percent_mito cell_type
    ## 17642 fivePrime_GATCGCGAGACCGGAT    5p 6580  2020   0.05714286         B
    ## 13347 fivePrime_AACTCAGGTTACGCGC    5p 4750  1670   0.05642105         B
    ## 13638 fivePrime_ACATCAGGTCTTGTCC    5p 4432  1668   0.03993682         B
    ## 17934 fivePrime_GCCTCTATCATGTCCC    5p 5831  2027   0.03258446     T_CD4
    ## 14432 fivePrime_AGGGATGGTGGGTATG    5p 5558  1890   0.05865419         B
    ## 18442 fivePrime_GGGACCTCATGCCACG    5p 5360  1388   0.03750000     T_CD4
    ##       cell_type_broad cell_type_pred_knn cell_type_pred_knn_prob
    ## 17642               B                  B                     1.0
    ## 13347               B                  B                     1.0
    ## 13638               B                  B                     1.0
    ## 17934               T              T_CD4                     1.0
    ## 14432               B                  B                     1.0
    ## 18442               T              T_CD4                     0.6

## Visualization of mapping

    # Sync the column names for both data frames
    reference$meta_data$cell_type_pred_knn = NA
    reference$meta_data$cell_type_pred_knn_prob = NA
    reference$meta_data$ref_query = 'reference'
    query$meta_data$ref_query = 'query'

    # Add the UMAP coordinates to the metadata
    meta_data_combined = rbind(query$meta_data, reference$meta_data)
    umap_combined = rbind(query$umap, reference$umap$embedding)
    umap_combined_labels = cbind(meta_data_combined, umap_combined)

    # Plot UMAP visualization of all cells
    plotBasic(umap_combined_labels, title = 'Reference and query cells', color.by = 'ref_query')

Plot the reference and query side by side.

    plotBasic(umap_combined_labels, title = 'Reference and query cells',
              color.mapping = pbmc_colors, facet.by = 'ref_query')

And that’s a wrap! If you run into issues or have questions about Symphony or this tutorial, please open an issue on GitHub.

    sessionInfo()

    ## R version 4.4.3 (2025-02-28)
    ## Platform: aarch64-apple-darwin20.0.0
    ## Running under: macOS Sonoma 14.6
    ##
    ## Matrix products: default
    ## BLAS/LAPACK: /opt/anaconda3/envs/resiq/lib/libopenblas.0.dylib;  LAPACK version 3.12.0
    ##
    ## locale:
    ## [1] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
    ##
    ## time zone: America/New_York
    ## tzcode source: system (macOS)
    ##
    ## attached base packages:
    ## [1] stats     graphics  grDevices utils     datasets  methods   base
    ##
    ## other attached packages:
    ##  [1] RColorBrewer_1.1-3  ggthemes_5.2.0      ggplot2_4.0.3
    ##  [4] dplyr_1.2.1         data.table_1.18.2.1 irlba_2.3.7
    ##  [7] Matrix_1.7-5        harmony_2.0.2       Rcpp_1.1.1-1.1
    ## [10] symphony_0.1.3
    ##
    ## loaded via a namespace (and not attached):
    ##  [1] sass_0.4.10         utf8_1.2.6          generics_0.1.4
    ##  [4] class_7.3-23        stringi_1.8.7       lattice_0.22-9
    ##  [7] digest_0.6.39       magrittr_2.0.5      evaluate_1.0.5
    ## [10] grid_4.4.3          fastmap_1.2.0       jsonlite_2.0.0
    ## [13] RSpectra_0.16-2     purrr_1.2.2         scales_1.4.0
    ## [16] RhpcBLASctl_0.23-42 codetools_0.2-20    jquerylib_0.1.4
    ## [19] cli_3.6.6           rlang_1.2.0         RcppAnnoy_0.0.23
    ## [22] uwot_0.2.4          cowplot_1.2.0       withr_3.0.2
    ## [25] RANN_2.6.2          cachem_1.1.0        yaml_2.3.12
    ## [28] otel_0.2.0          tools_4.4.3         vctrs_0.7.3
    ## [31] R6_2.6.1            lifecycle_1.0.5     stringr_1.6.0
    ## [34] pkgconfig_2.0.3     pillar_1.11.1       bslib_0.10.0
    ## [37] gtable_0.3.6        glue_1.8.1          xfun_0.57
    ## [40] tibble_3.3.1        tidyselect_1.2.1    knitr_1.51
    ## [43] farver_2.1.2        htmltools_0.5.9     rmarkdown_2.31
    ## [46] labeling_0.4.3      compiler_4.4.3      S7_0.2.2