Write single-cell data as a Cellucid-ready AnnData (or MuData) store
cellucid_prepare.Rdcellucid_prepare() writes a Seurat, SingleCellExperiment or
SummarizedExperiment object, or 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 (format 2, or 3
with zarr_format = 3), that Python's anndata reads without a
warning and the Cellucid web app opens directly, with the dataset's name,
citation, and launch defaults in uns['cellucid'].
Usage
cellucid_prepare(
out_path,
format,
data = NULL,
embeddings = NULL,
dataset_id = NULL,
dataset_name = NULL,
obs = NULL,
var = NULL,
gene_expression = NULL,
connectivities = NULL,
vector_fields = NULL,
modalities = NULL,
gene_identifiers = NULL,
dataset_description = NULL,
citation_text = NULL,
citation_url = NULL,
source_name = NULL,
source_url = NULL,
embedding_defaults = NULL,
latent_key = NULL,
obs_keys = NULL,
var_gene_id_column = NULL,
vector_field_default = NULL,
normalize_embeddings = NULL,
centroid_min_points = NULL,
modality = NULL,
priority_seed = 0,
zarr_format = NULL,
force = FALSE
)Arguments
- out_path
Required path of the store to write, ending in the suffix of
format: a folder whose name ends in".zarr"forformat = "zarr", a file whose name ends in".zarr.zip"forformat = "zarr.zip". A path that does not matchformatis refused with the path to write instead. Missing parent folders are created.- format
Required kind of output, with no default, because each suits a different place and only you know where the store goes:
"zarr"writes a.zarrfolder of small files, the way to publish on GitHub (no file over 100 MB, no Git LFS);"zarr.zip"writes the same store as one.zarr.zipfile, for everywhere else: single-file hosts (Zenodo, Figshare, Hugging Face, S3, a lab server), local files,cellucid serveand Jupyter. See the section Which format?. Matched asmatch.arg()matches. An.h5adfile is written by Python'scellucid.prepare(format = "h5ad"); this package writes no HDF5, so it depends on no HDF5 library.- data
Optional single-cell object to write: a Seurat object made by Seurat 3.0 or later (read with SeuratObject 4 or 5;
AssayandAssay5assays), a SingleCellExperiment, or a SummarizedExperiment. Every other argument then defaults to what the object holds, and an argument given replaces that one component; see the section Writing an object. With several assays (Seurat) or alternative experiments (SingleCellExperiment) the store is a Cellucid-ready MuData. SeuratObject and SingleCellExperiment are needed only for their own objects.- embeddings
Named list mapping each
obsmkey to a numeric matrix with one row per cell, such aslist(X_umap = umap, X_pca = pca). A numericdata.frame, a Matrix matrix, or a plain numeric vector (one column) is accepted too. Each one is stored whole, every column, as a float32 array at its own coordinates; the viewer chooses the columns it plots and scales them to its view. Every value must be within the float32 range. A row holdingNAorNaN(or an infinity) is a cell the embedding does not place (one computed on some of the cells): it is written as given, the viewer leaves it out of that embedding's views, and in the latent space it gets no outlier quantile; at least one cell must have finite coordinates. Each key becomes a path in the store, see the note on names below.- dataset_id
Portable identifier of the dataset, stored as
uns['cellucid']['id']: 1–180 ASCII letters, numbers,.,_, or-, beginning with a letter or number, not ending with., and not a Windows device name. Sessions and community annotations are keyed on it.- dataset_name
Non-empty name of the dataset, shown to the reader verbatim and stored as
uns['cellucid']['name']anduns['title']; see the note on displayed text below.- obs
Optional
data.frameof cell metadata, one row per cell. Every column is stored, see the note on column encodings below.- var
Optional
data.frameof gene metadata, one row per column ofgene_expression. Every column is stored. Supplying it withoutgene_expressionis an error.- gene_expression
Optional cells x genes matrix: a Matrix sparse matrix (stored as a gene-major
csc_matrix), a dense numeric matrix (stored as a dense array in tall, gene-major chunks), or a matrix held outside R's memory – a BPCells matrix or a DelayedArray backend such as HDF5Array – read block by block (see Details) and stored as acsc_matrix. Values must be finite and within the float32 range, and are stored as float32 – or the whole matrix as float64 when every value is a whole number and one exceeds2^24in magnitude: integer counts float32 cannot hold exactly. That is decided on the genes stored, aftergene_identifiersselects them. A stored zero of a sparse matrix is no value and is dropped; the matrix stays sparse. Rows are always cells; Cellucid never transposes the matrix, so transpose a genes x cells matrix witht()first.- connectivities
Optional cells x cells neighbour graph, a Matrix sparse matrix or a numeric matrix, stored as
obsp['connectivities'](a float32csr_matrix). It must be exactly symmetric, with finite, non-negative weights; a stored zero is not an edge and is dropped, and so is the diagonal (a self-loop is no edge: Seurat'sRNA_snnhas 1 there). A directed graph (Seurat'sRNA_nn) is refused.- vector_fields
Optional per-cell vectors drawn over an embedding, keyed by field and then by the embedding each array belongs to:
list(velocity = list(X_umap = v2, X_umap_3d = v3, X_branches = vb)). Each array has one row per cell and one column per column of its embedding, and is stored under scVelo's name for it,obsm['<field>_<basis>']for the embeddingX_<basis>(velocity_umap,velocity_umap_3d,velocity_branches), which the viewer lists as the field'<field>'and draws over that embedding. The embedding's key must therefore start withX_, and each stored key must read back as its own field and embedding: a field named like an embedding (X_...), or a key another embedding's longer basis claims (fieldT_fwdonX_umapbeside an embeddingX_fwd_umap), is refused.- modalities
Optional character vector naming the modalities of
datato write, in this order: Seurat assays or SingleCellExperiment experiments ("main", or the main experiment's name, and eachaltExpNames()).NULLwrites every one, the Seurat active assay or the main experiment first. One modality is written as a plain AnnData.- gene_identifiers
Optional character vector of the genes to store, in the order given.
NULLstores every gene. The identifiers are matched against the gene names the viewer shows (var_gene_id_column, or thevarrow names) after repeated names were made unique, and must be distinct.- dataset_description
Optional description of the dataset, shown verbatim; stored as
uns['cellucid']['description'].- citation_text
Optional text that says how to cite the dataset, stored as
uns['cellucid']['citation']['text']. Required whenevercitation_urlis supplied.- citation_url
Optional absolute
http://orhttps://address of the publication, stored asuns['cellucid']['citation']['url'].- source_name
Optional name of where the data comes from, stored as
uns['cellucid']['source']['name']. Required wheneversource_urlis supplied.- source_url
Optional absolute
http://orhttps://address of the source, stored asuns['cellucid']['source']['url'].- embedding_defaults
Optional launch default: the embedding each view dimension shows first, by
"1d","2d", or"3d". A value is anobsmkey, plotted by its first columns, such asc("2d" = "X_umap"), orlist(key = , columns = )naming the 1-based columns to plot, such aslist("3d" = list(key = "X_pca", columns = 1:3)). Stored asuns['cellucid']['defaults']['embedding<d>d'], columns 0-based. Without it the viewer chooses.- latent_key
Optional
embeddingskey of the latent space the viewer's outlier filter measures in, such as"X_pca"or"X_scVI": a latent space such as scVI or PCA, never a 2D or 3D layout. The outlier quantiles of every categoricalobscolumn (each cell's distance to its category's centre, as the share of its category at that distance or closer) are computed here and stored inuns['cellucid']['outlierQuantiles'], so the viewer filters outliers at once instead of reading the whole latent space to compute them. See the section Outlier quantiles. Also stored as the launch defaultdefaults['latentKey'].- obs_keys
Optional launch default: the
obscolumns the viewer shows, in this order. Every column is stored either way. Stored asdefaults['obsKeys'].- var_gene_id_column
Optional launch default: the
varcolumn whose values name the genes in the viewer, such as gene symbols beside Ensembl row names.NULL: thevarrow names name them; Data mapping in the viewer can choose a column later. A column of text or a factor names genes as it is, an integer column by its digits; a double or logical column cannot name genes and is refused. Stored asdefaults['geneIdColumn'].- vector_field_default
Optional launch default: the field of
vector_fieldsselected first, by the name used there ("velocity"), which is the id the viewer lists. One of its embeddings must be shown when the file opens: one named inembedding_defaultsor found by the viewer's default rule, or any other 1 to 3 column embedding that is notlatent_key. Stored asdefaults['vectorFieldDefault'].- normalize_embeddings
Optional launch default:
FALSEshows embeddings at their own coordinates instead of scaled to the view. Stored asdefaults['normalizeEmbeddings'].- centroid_min_points
Optional positive integer: the fewest cells a category needs to get outlier quantiles (10 when
NULL). Stored as the launch defaultdefaults['centroidMinPoints'], and withlatent_keyasuns['cellucid']['outlierQuantiles']['minPoints'].- modality
Optional launch default for a multimodal store: the modality the viewer opens with. Stored as
defaults['modality']. Without it the viewer opens the first modality named like gene expression (rna,gex,gene expression,gene_expression,sct, any letter case), else the first.var_gene_id_columnthen names a column of that modality'svar.gene_expression,varandgene_identifiersdescribe one feature space and are refused for an object of several: change its assay first, or write one withmodalities.- priority_seed
The seed of the cells' priority order, a whole number in [0, 2^32 - 1]. The cells are stored by ascending priority – the
"uniform"rank ofcellucid_subsample_cells()with this seed – so every subsample Cellucid opens with this seed is the first rows of every array and reads nothing of the cells it leaves out. Each row's original position is kept inobs[['cellucid_original_position']];cellucid_restore_original_order()puts the cells back in the order given.NULLstores the cells in the order given, without a priority. An object in a Cellucid-ready file's stored order (itscellucid$priorityrecord describes its cells) is put back in its original order first, whatever the seed, so every cell keeps its original position; acellucid_original_positioncolumn with no record that describes the cells names no order and is not written (with a warning). Python'scellucid.prepare(priority_seed = )takes the same values.- zarr_format
Optional Zarr format of the store:
2(the default, whenNULL), read by every anndata release since 0.8 and by zarr-python 2 and 3, or3, read by anndata 0.11 and later (zarr-python 3). The web app reads both equally fast. Python'scellucid.prepare(zarr_format = )takes the same values.- force
TRUEreplaces an existing store atout_path, once the new one is complete. An existing folder that is not a Zarr store is never replaced.
Details
The store is an AnnData Zarr group (Zarr format 2 unless zarr_format = 3):
X: gene expression, gene-major float32 (float64 for integer counts above2^24), acsc_matrixwith int32indicesandindptrand no stored zeros, or a dense array;obsandvar: data frames, the index in_index;obsm: every embedding and vector field, float32;obsp/connectivities: the neighbour graph, float32csr_matrix;uns/titleanduns/cellucid:schema(1),id,name, and, when supplied,description,citation(text,url),source(name,url),defaultsholding only the launch defaults that were set, withlatent_key,outlierQuantiles, and in priority orderpriority(seed,specVersion,nObs– the cell count it was written with, which a reader verifies it by –levels);in priority order (the default,
priority_seed): every per-cell element –obs,obsm,X's rows, both axes ofobsp– by ascending priority,obs[['cellucid_original_position']](uint32) holding each row's original position, and beside a sparseXvarm[['cellucid_prefix_counts']](uint32, one column per level oflevels): each gene's values in the rows below that level;the consolidated metadata (
.zmetadata; format 3: the rootzarr.json), so a store on a web host opens with one request.
Every element carries the encoding-type and encoding-version
attributes anndata 0.8 and later write. Every chunk of values wider
than one byte is byte-shuffled, so the similar high bytes of neighbouring
values compress together – except the expression values themselves, which
compress best as they are – and every chunk is compressed with DEFLATE level 6,
which browsers decompress natively: in format 2 numcodecs' Shuffle
filter and the compressor {"id": "zlib", "level": 6}; in format 3
the blosc codec (byte shuffle, zlib level 6) for a shuffled array and
the gzip codec for every other, only codecs of the Zarr v3
specification (numcodecs.shuffle is not one). A chunk is the unit of every read: every array with one
row per cell (data frame columns and index, embeddings as a chunk of rows with
every column, masks, cell maps, outlier quantiles) holds 65,536
(2^16) values per chunk, so a subsample – the first rows of a store in
priority order – reads little beyond its own cells; arrays read whole
(var, the expression matrix's pointers, the neighbour graph) up to
262,144 (2^18); and the data and indices of the expression matrix,
which the viewer reads one gene (or its first values) at a time, 16,384
(2^14). Python's cellucid.prepare() writes the same store,
element for element, from the same input.
A .zarr.zip holds the same files, byte for byte, as entries of one ZIP
archive at its top level, each stored without further compression, so the web
app reads any chunk with one range request. The chunks come first and the
metadata documents last, the consolidated metadata the very last entry: the
web app reads the end of an archive when it opens it, and with it everything
that describes the store.
The store is written beside out_path under a temporary name and moved
into place only once it is complete: a failed or interrupted call leaves
nothing at out_path, and with force = TRUE the store it replaces
stays untouched until then.
Note on column encodings: the columns of obs and var are stored
with anndata's own rules. A factor is a categorical with its levels in
their order (ordered for an ordered factor). A character column is a
categorical, its levels in code-point order, when a value repeats or is
missing, and a string array otherwise. Category codes take the integer type
pandas gives them: int8 for fewer than 127 categories, int16 for fewer than
32,767, else int32. Every other column keeps its own type, as in Python: a
logical column is a boolean array, or a nullable boolean when it holds
NA; an integer column is int32 (R's integer), or a nullable integer
when it holds NA; a double column is float64 whether or not its values
are whole numbers, with NA stored as NaN, and must hold no infinite
value. Any other column, a
Date for example, is refused with the conversion that makes it storable
when it is given in obs; a column of an object given as data is
left out instead, with a message naming it, as cellucid-python leaves it out.
Note on names: cell names are the row names obs,
gene_expression, and the embeddings agree on; where several are set they
must be the same names in the same order, and with none the cells are named
"0" to "n - 1". Gene names are the var row names, else the
column names of gene_expression. Repeated names are made unique the way
anndata's obs_names_make_unique() and
var_names_make_unique() do it – the second "MATR3" becomes
"MATR3-1" – and what changed is reported as a message. Every
obs and var column name and every obsm key becomes a path
in the store, so it must be a portable file name on every platform: no slash,
backslash, colon, asterisk, question mark, quotation mark, <, >,
|, or control character, no trailing space or dot, not a
Windows device name, not _index for a column, and distinct from its
neighbours by more than letter case ("Sex" and "sex" are one
folder on macOS and Windows).
Note on displayed text: dataset_name, dataset_description,
citation_text, and source_name are drawn verbatim, so each must be
free of control characters (U+0001–U+001F,
U+007F–U+009F), of the zero-width characters U+200B,
U+2060, and U+FEFF, and of leading or trailing whitespace of any
kind. Nothing is trimmed: trimming would rewrite text the caller never asked to
change.
Note on required arguments: out_path and format carry no
default, and without data neither do embeddings,
dataset_id, and dataset_name, because no value of any of them is
a reasonable guess about someone else's dataset. Leaving one out is reported as
what it is – cellucid_prepare() is missing 1 required argument: –
followed by the argument's name and what a valid value of it is.
Writing an object
With data, the object's contents are read as the Cellucid web app reads
the same object saved with saveRDS(), so a session saved on either
applies to the other:
cells and
obs: Seuratmeta.data(andactive.identasactive_ident);colData(and the size factors); columns of a type AnnData does not store are left out and named;obsm: every Seurat reduction and everyreducedDimasX_<name>, the name's letter case kept (X_umap,X_UMAP); one that does not cover every cell is left out and named;expression: an assay's log-normalised
data(elsecounts) asX;logcounts(elsecounts, else the first assay) asX; Seurat v5 split layers joined. A Cellucid-ready AnnData holds one matrix per modality, socountsbesidedata, the other assays, andscale.dataare not written;var:meta.features(Assay5meta.data) with the variable features ashighly_variable;rowData;obsp/connectivities: the first symmetric neighbour graph the viewer's default rule would open with, its diagonal removed: one namedconnectivities, then Seurat's<active assay>_snn, then any other shared-nearest-neighbour graph (<assay>_snn, orSNN, in any case; acolPairthe same), in code-point order; a graph passed over (not symmetric, or not over every cell) or left out (RNA_nn) is named;identity and launch defaults: the object's own record,
misc$cellucidormetadata(x)$cellucid, shaped asuns['cellucid']:list(id = , name = , description = , citation = list(text = , url = ), source = list(name = , url = ), defaults = list(embedding2d = "X_umap", latentKey = "X_pca", ...)); a Seuratproject.nameother than"SeuratProject"names it too.
Matrices kept outside R's memory (BPCells, HDF5Array and other DelayedArray
backends) stay there and are read a block at a time (about \(2^{24}\)
positions: whole cells, or whole genes when the backend keeps genes together),
never whole and never as one dgCMatrix: one pass counts each gene's
values (and refuses a value that cannot be stored), then each value is placed
at its gene's position, its cell's row beside it, and each gene sorted by row
(on every core; its prefix counts read from its sorted rows). The genes one
pass places hold at most a quarter of the machine's memory (8 bytes a stored
value, 12 for float64 counts), and the pass reads only their rows; a larger
matrix takes one pass per quarter, twice when it is written, X's values being
written before its cells. Nothing else of the matrix is held, and
there is no limit on its stored values (more than \(2^{31} - 1\) are
written with 64-bit pointers). The bytes are those the same values in a
dgCMatrix give. Seurat objects
made before 3.0, and SingleCellExperiment objects before 1.8, are refused with
the line that updates them.
Several assays or experiments are written as a Cellucid-ready MuData:
MuData's own Zarr layout (mudata.read_zarr() reads it), the cells'
obs, obsm, obsp and uns['cellucid'] at the root,
each modality an AnnData under mod/<name> (gene-major X and
var), mod-order listing them, and
obsmap/varmap placing each modality's cells and features. R writes
MuData as Zarr only; Python's cellucid.prepare(data = mdata) writes the
same store, and .h5mu.
Which format?
Where the store goes decides format:
a GitHub repository or its GitHub Pages site:
"zarr". GitHub refuses files over 100 MB, and a folder of small files publishes any size without Git LFS; GitHub answers over HTTP/2, 100 requests at a time, so a folder's one request per chunk costs nothing there.everything else – a single-file host (Zenodo, Figshare, Hugging Face, S3, a lab web server), your own disk (the web app's file picker),
cellucid serve, Jupyter:"zarr.zip". One file is read in ranges that the web app merges into few requests; most single-file hosts answer over HTTP/1.1, six requests at a time, where a folder's one request per chunk makes many-gene and whole-matrix reads several times slower.
Measured on a 561,947-cell atlas, every read the web app makes, in total:
over GitHub's HTTP/2 (80 ms round trip, 50 MB/s) 8.0 s from the folder and
8.2 s from the .zarr.zip; over HTTP/1.1 (80 ms, 50 MB/s) 46.8 s and
9.0 s; from a local file in a browser 2.6 and 2.7 s.
In a browser, a local Zarr folder reads as fast as a single file. Python's
cellucid.prepare(format = "h5ad") writes an .h5ad, which goes
wherever a .zarr.zip goes; this package writes Zarr only, so it needs
no HDF5 library.
Outlier quantiles
With latent_key, uns['cellucid']['outlierQuantiles'] holds
latentKey, minPoints (centroid_min_points, or 10) and
fields: one float32 array of one value per cell for every obs
column the viewer shows as categorical – a factor, a character column, a
logical column with or without NA – in obs column order.
Its categories are its values; NA belongs to none. For each category of
at least minPoints cells, over the latent values as stored (float32):
the centroid is the column sums, accumulated cell by cell in cell order,
divided by the cell count; a cell's distance is the square root of its squared
differences from the centroid, accumulated column by column; its quantile is
the number of the category's cells at that distance or closer divided by the
cell count, both as float32. Every other cell is NaN. A column with no
category of minPoints cells gets no entry, and a numeric column never
does. The viewer's outlier filter hides the cells above a threshold of these
quantiles. Python's cellucid.prepare() stores the same bytes, and the
web app computes the same bytes for a file that stores none.
Examples
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,
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(), "example.zarr"),
format = "zarr",
embeddings = list(X_umap = umap, X_pca = pca),
dataset_id = "cellucid-example",
dataset_name = "Cellucid example",
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" "X" "layers"
#> [6] "obs" "obsm" "obsp" "uns" "var"
#> [11] "varm" "varp"
# A Seurat object, read as the web app reads it
if (requireNamespace("SeuratObject", quietly = TRUE)) {
data("pbmc_small", package = "SeuratObject")
cellucid_prepare(
file.path(tempdir(), "pbmc_small.zarr"), "zarr",
data = SeuratObject::UpdateSeuratObject(pbmc_small),
dataset_id = "pbmc-small", dataset_name = "PBMC (small)",
force = TRUE
)
}
#> 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
# The same store as one file
cellucid_prepare(
out_path = file.path(tempdir(), "example.zarr.zip"),
format = "zarr.zip",
embeddings = list(X_umap = umap),
dataset_id = "cellucid-example",
dataset_name = "Cellucid example",
force = TRUE
)