Skip to contents

Overview

cellucid_prepare() writes single-cell data – embeddings, cell and gene metadata, gene expression, a neighbour graph, and vector fields – as a Cellucid-ready AnnData: a standard AnnData store, written as AnnData Zarr, that Python’s anndata reads like any other and the Cellucid web app opens directly. Gene expression is stored gene-major, so the viewer reads one gene with one small range of the file, and uns['cellucid'] carries the dataset’s name, citation, source, and launch defaults.

format chooses the output, and has no default because only you know where the store goes: format = "zarr" writes a .zarr folder of small files, the way to publish on GitHub (no file over 100 MB, no LFS); format = "zarr.zip" writes the same store as one .zarr.zip file. out_path ends in the matching suffix. (An .h5ad file is written by Python’s cellucid.prepare(format = "h5ad"); this package writes no HDF5, so it needs no HDF5 library.)

From an object

A Seurat, SingleCellExperiment or SummarizedExperiment object is written as it is: data = reads its cells, metadata, reductions, assays and neighbour graph the way the Cellucid web app reads the same object saved with saveRDS(), and any other argument replaces one component. To look at an object before writing it, cellucid_view(object) opens it in the viewer from memory (see vignette("viewer", package = "cellucid")).

data("pbmc_small", package = "SeuratObject")
pbmc <- SeuratObject::UpdateSeuratObject(pbmc_small)
#> Validating object structure
#> Updating object slots
#> Ensuring keys are in the proper structure
#> Updating matrix keys for DimReduc 'pca'
#> Updating matrix keys for DimReduc 'tsne'
#> Ensuring keys are in the proper structure
#> Ensuring feature names don't have underscores or pipes
#> Updating slots in RNA
#> Updating slots in RNA_snn
#> Setting default assay of RNA_snn to RNA
#> Updating slots in pca
#> Updating slots in tsne
#> Setting tsne DimReduc to global
#> Setting assay used for NormalizeData.RNA to RNA
#> Setting assay used for ScaleData.RNA to RNA
#> Setting assay used for RunPCA.RNA to RNA
#> Setting assay used for BuildSNN.RNA.pca to RNA
#> No assay information could be found for FindClusters
#> Warning: Adding a command log without an assay associated with it
#> Setting assay used for RunTSNE.pca to RNA
#> Setting assay used for JackStraw.RNA.pca to RNA
#> Setting assay used for ScoreJackStraw.pca to RNA
#> Setting assay used for ProjectDim.RNA.pca to RNA
#> Setting assay used for FindVariableFeatures.RNA to RNA
#> Validating object structure for Assay 'RNA'
#> Validating object structure for Graph 'RNA_snn'
#> Validating object structure for DimReduc 'pca'
#> Validating object structure for DimReduc 'tsne'
#> Object representation is consistent with the most current Seurat version

store <- cellucid::cellucid_prepare(
  file.path(tempdir(), "pbmc_small.zarr"), "zarr",
  data = pbmc,
  dataset_id = "pbmc-small",
  dataset_name = "PBMC (small)",
  latent_key = "X_pca",
  force = TRUE
)
list.dirs(file.path(store, "obsm"), full.names = FALSE, recursive = FALSE)
#> [1] "X_pca"  "X_tsne"

With several assays the store is a Cellucid-ready MuData, one modality per assay; modalities = "RNA" writes one.

Minimal example

library(cellucid)

umap <- matrix(c(0, 0,
                 1, 0,
                 0, 1,
                 1, 1),
               ncol = 2, byrow = TRUE)
pca <- matrix(c(0, 3, 1,
                2, 5, 1,
                4, 0, 2,
                1, 1, 1),
              ncol = 3, byrow = TRUE)

obs <- data.frame(
  cluster = factor(c("A", "A", "B", "B")),
  score = c(0.1, 0.2, 0.3, 0.4),
  row.names = paste0("cell", 1:4)
)

expr <- matrix(c(0, 1,
                 2, 3,
                 4, 5,
                 0, 0),
               nrow = 4, byrow = TRUE,
               dimnames = list(rownames(obs), c("ENSG01", "ENSG02")))

var <- data.frame(symbol = c("CD3E", "MS4A1"), row.names = colnames(expr))

store <- cellucid_prepare(
  out_path = file.path(tempdir(), "vignette.zarr"),
  format = "zarr",
  embeddings = list(X_umap = umap, X_pca = pca),
  dataset_id = "vignette",
  dataset_name = "Vignette dataset",
  obs = obs,
  var = var,
  gene_expression = expr,
  embedding_defaults = c("2d" = "X_umap"),
  latent_key = "X_pca",
  var_gene_id_column = "symbol",
  force = TRUE
)

list.files(store, all.files = TRUE, no.. = TRUE)
#>  [1] ".zattrs"    ".zgroup"    ".zmetadata" "layers"     "obs"       
#>  [6] "obsm"       "obsp"       "uns"        "var"        "varm"      
#> [11] "varp"       "X"

In the web app at https://www.cellucid.com, open a .zarr.zip file from your computer (see below), or a .zarr folder or .zarr.zip file published at a web address with the Link or server address bar. In Python:

import anndata
adata = anndata.read_zarr("vignette.zarr")

What goes where

Argument In the store
data every element below; several assays or experiments as modalities (mod/<name>, a Cellucid-ready MuData)
embeddings obsm[<key>], float32, every column
obs obs, every column
var var, every column
gene_expression X, gene-major float32, float64 for integer counts above 2^24 (csc_matrix for a sparse matrix)
connectivities obsp['connectivities'], float32 csr_matrix
vector_fields obsm['<field>_<basis>'] for the embedding X_<basis>
dataset_id, dataset_name, dataset_description uns['cellucid'] id, name, description; uns['title']
citation_text, citation_url uns['cellucid']['citation']
source_name, source_url uns['cellucid']['source']
embedding_defaults, latent_key, obs_keys, var_gene_id_column, vector_field_default, normalize_embeddings, centroid_min_points, modality uns['cellucid']['defaults'], only the ones you set
latent_key (with centroid_min_points) uns['cellucid']['outlierQuantiles']: each categorical obs column’s outlier quantiles in that latent space

Embeddings are stored at their own coordinates; the viewer chooses the columns it plots and scales them to its view. embedding_defaults names the embedding each view dimension shows first, by obsm key (its first columns) or as list(key = "X_pca", columns = 1:3).

latent_key names the latent space the viewer’s outlier filter measures in, such as PCA or scVI, never a 2D or 3D layout. cellucid_prepare() computes the outlier quantiles of every categorical obs column there (each cell’s distance to its category’s centre, as the share of its category at that distance or closer) and stores them, so the filter works as soon as the file opens. A category needs centroid_min_points cells (10 when not set); ?cellucid_prepare states the rule.

Vector fields are stored under scVelo’s names and drawn over the embedding they are named for: vector_fields = list(velocity = list(X_umap = velocity_umap, X_branches = velocity_branches)) stores obsm['velocity_umap'] and obsm['velocity_branches'], which the viewer lists as the one field velocity and draws whenever a view shows X_umap or X_branches. Each array has one column per column of its embedding, and the embedding’s key starts with X_. cellucid_transition_drift(transition_matrix, umap, normalize_rows = TRUE) computes such a field from a cell-cell transition matrix (CellRank’s, for example): each cell’s transition-weighted mean successor position minus its own, ready for vector_fields = list(T_fwd = list(X_umap = drift)).

Cell names are the row names your inputs agree on, and gene names the var row names (else the column names of gene_expression). Repeated names are made unique the way anndata’s var_names_make_unique() does it – the second MATR3 becomes MATR3-1 – with a message saying what changed.

Choosing what an object writes

With data =, any other argument replaces the one component it names, and modalities chooses the assays (Seurat) or experiments (SingleCellExperiment) to write, in order. An object’s matrices are read in their own genes x cells orientation; nothing needs transposing.

cellucid_prepare(
  "seurat.zarr", "zarr",
  data = seu,
  dataset_id = "seurat-dataset",
  dataset_name = "Seurat dataset",
  obs = seu[[c("seurat_clusters", "orig.ident")]],   # only these columns
  modalities = "RNA",                                 # one assay: a plain AnnData
  embedding_defaults = c("2d" = "X_umap"),
  latent_key = "X_pca"
)

cellucid_prepare(
  "sce.zarr", "zarr",
  data = sce,                                         # logcounts, colData, reducedDims, altExps
  dataset_id = "sce-dataset",
  dataset_name = "SingleCellExperiment dataset",
  latent_key = "X_PCA"                                # reducedDim names keep their case
)

A matrix you take out of an object yourself and pass as gene_expression is cells x genes: transpose a Seurat or SingleCellExperiment matrix with Matrix::t() first. cellucid_prepare() checks the dimensions but never guesses the orientation.

One file instead of a folder

format = "zarr.zip" writes the same store as one .zarr.zip file: the format for everywhere but GitHub – single-file hosts (a Zenodo record, a Figshare or Hugging Face file, a bucket URL, a lab server), local files, cellucid serve and Jupyter. A .zarr folder is the format for GitHub, which refuses files over 100 MB and answers many small files at once over HTTP/2. The archive’s entries are stored uncompressed (each chunk is compressed already), so the web app reads any part of it with one range request, and its metadata comes last, so it opens with one read of the archive’s end.

archive <- cellucid_prepare(
  out_path = file.path(tempdir(), "vignette.zarr.zip"),
  format = "zarr.zip",
  embeddings = list(X_umap = umap),
  dataset_id = "vignette",
  dataset_name = "Vignette dataset",
  force = TRUE
)
file.size(archive)
#> [1] 15756