File format expectations#
Audience: everyone (beginners get “what should I have?”, experts get exact file/key expectations)
Time: 10–15 minutes
What you’ll learn: which files Cellucid opens and how it reads each, what
it expects of an .h5ad or an AnnData Zarr store, what a file says about
itself in uns['cellucid'], and the exact datasets.json contract of a
dataset catalog
Prerequisites: none (helpful: you know which loading option you plan to use)
What Cellucid reads#
Every path (the file buttons, a dropped file, a link, the sample list, a server, a notebook) points the viewer at one of these inputs. Each is read as AnnData: whatever the file, data mapping, analysis, sessions, figure export and annotation work the same. Read in place means only the parts a view needs are read, from disk or with range requests from a link. The tag says which ecosystem’s tools write the format.
Input |
Tag |
Versions |
How it is read |
|---|---|---|---|
AnnData |
Python |
anndata 0.6 and later |
in place |
AnnData Zarr: a |
Python, R |
Zarr v2 and v3, anndata 0.7 and later |
in place |
Loom |
any |
Loom 2.0.1 and 3.0.0 (loompy, velocyto, SCENIC, LoomExperiment, loomR) |
in place (Open Loom and 10x Cell Ranger files) |
10x Cell Ranger |
any |
Cell Ranger 1.2 and later |
in place; alone it has no embedding, so open its |
Cell Ranger |
any |
Cell Ranger 1.2 and later |
its |
MuData |
Python |
mudata 0.2 and later |
in place, with its modalities (Multimodal data: MuData and the Modality select) |
MuData Zarr (a folder or |
Python, R |
mudata 0.2 and later, Zarr v2 and v3 |
in place, with its modalities (Multimodal data: MuData and the Modality select) |
Seurat or SingleCellExperiment object: |
R |
SeuratObject 3.0 and later ( |
cannot be read in parts: read whole once into this browser’s storage and kept for next time; an uncompressed file’s arrays in place (Open R objects and MatrixMarket folders, Which formats are read in parts) |
A gzipped copy of a file read in parts: |
any |
as the file it copies |
cannot be read in parts (gzip has no index): read whole once, decompressed as it arrives into this browser’s storage and kept for next time, then read in parts from there; the uncompressed file is opened instead when the host has both. |
MatrixMarket folder: a |
any |
Cell Ranger 2 and later, STARsolo, kb-python, alevin-fry, GEO uploads, sci-RNA-seq3 ( |
cannot be read in parts: its cells are drawn from its lists and tables first, then the matrix is read once keeping the rows of the cells genes are read for, both kept in this browser’s storage for next time, with the folder’s cell tables for the embedding (Open R objects and MatrixMarket folders) |
A dataset catalog: a folder holding |
any |
schema 2 |
The format is decided by the file’s content, not only its name: an .h5
that is a Loom or an AnnData file opens as what it is, and a file Cellucid
recognises but cannot show (a Cell Ranger molecule_info.h5, a MuData whose
modalities share features rather than cells, a count matrix with no
embedding, an h5Seurat file, a Seurat object whose counts live outside the
.rds) is refused with what to do instead
(Troubleshooting (data loading)).
A Cellucid-ready AnnData is not a format of its own: it is an ordinary
.h5ad or Zarr store that cellucid.prepare() (Python) or cellucid_prepare()
(R) wrote for fast reading (the expression matrix gene-major, arrays chunked
for range requests) with uns['cellucid'] filled in. The exact contract is
Cellucid-ready AnnData specification;
how to write one is in Preparing an AnnData for Cellucid
(Python) and Data Preparation API (R). anndata reads it like
any other file, and Cellucid reads any other file like it.
What a file says about itself: uns['cellucid']#
An AnnData file may carry a mapping uns['cellucid'] that tells Cellucid its
identity and the choices it opens with. Every key is optional, and a file
without the mapping opens exactly as any AnnData file does.
Key |
Value |
Used for |
|---|---|---|
|
the integer |
the version of this mapping; a later version is refused, asking for a newer Cellucid |
|
the dataset’s stable id: 1–180 ASCII letters, digits, |
the dataset id sessions and community annotations key on (Dataset identity (why it matters)) |
|
text |
the name shown in the dataset list and the Dataset box |
|
text (may be empty) |
the Dataset box’s description |
|
a mapping |
how to cite the dataset (the Citation row of Details) |
|
a mapping |
where the data comes from (the Source row of the Dataset box’s Details, linked to |
|
a mapping of launch defaults (below) |
the choices the dataset opens with |
|
a mapping |
the outlier filter, which reads one column’s array on its slider’s first move instead of computing it from the latent space (Outlier filtering (per categorical field)) |
|
a mapping |
reading the first cells of a large file first; a record that does not describe the file’s cells is not used, never refused (Cellucid-ready AnnData specification) |
|
text: the records |
never read by the web app |
|
a mapping by gene set name: the gene sets a pull, an applied history or Save AnnData with this state… wrote |
never read by the web app |
|
a mapping by column name: what each column a pull wrote came from |
never read by the web app |
Rules the reader applies:
Text is stored as text; a one-element list of text is read as that text. It must not start or end with white space or hold a character with no glyph (a control or zero-width character).
An address (
citation.url,source.url) is an absolutehttp(s)URL.A key outside this table, or a wrong value, refuses the file with a message naming it, so a misspelt default never passes unnoticed.
history_applied,gene_setsandpulledare the records the Python and R packages keep on the object a person works with, and that the web app writes into a copy saved with the History header’s ⋯ › Save AnnData with this state… (Save AnnData with this state), so that object or file opens again in the viewer after a pull or an apply.
defaults holds the launch defaults, each optional: embedding1d,
embedding2d, embedding3d (an obsm key, or {key, columns} with 0-based
columns), knn (an obsp key), modality (for a file of several
modalities, the one genes are read from), expression (X, raw/X or
layers/<name>, of that modality), geneIdColumn (a var column), latentKey (an obsm key),
vectorFieldDefault (a vector field id), obsKeys (a list of obs columns),
normalizeEmbeddings (true or false), centroidMinPoints (a positive
integer: the fewest cells a category needs to get outlier quantiles). These
are the launch choices cellucid serve, serve_anndata() and show_anndata()
take (Server Mode (CLI + Python)).
How the file’s own values combine with the route that opens it:
Id: the file’s id is the dataset id. A catalog lists the file under an id, which must equal the file’s (else the file is refused); a server that names an id serves the file under it.
Name: for a file you open yourself (from disk, a link or a catalog), the file’s
nameis shown, else itsuns['title'](as CELLxGENE writes it), else the file name. A server that names the dataset shows its name.Description, citation, source: the file’s, whatever route opens it.
Launch defaults: the file’s, with every choice a server names on its command line taking precedence.
Beside uns['cellucid'] and uns['title'], the one other part of uns
Cellucid reads is the colours Scanpy keeps for a categorical obs column,
uns['<column>_colors']: the column opens in them
(Color-by behavior). They are read with
the column’s categories when the column is first shown: opening a file reads
no category list, only how many categories each column has, so a column of
one category per cell (barcodes stored as a categorical) costs nothing until
it is shown. A column with more categories than there are open cells, and
more than one request of the connection reads, is read codes first, then the names and colours of the categories those codes name
alone: a share of the cells reads a share of its categories (1 % of a
1.2M-category column, about 12,000 names, not 35 MB).
A dataset catalog: datasets.json#
A dataset catalog is a folder holding datasets.json and the files it lists. The Sample dataset list reads one (www.cellucid.com reads
https://theislab.github.io/cellucid-datasets/anndata/), and so does a link to
a folder (Open from a link).
{
"schema": 2,
"default": "pancreas",
"datasets": [
{
"id": "pancreas",
"path": "pancreas.zarr/",
"session": {
"path": "pancreas.cellucid-session",
"bytes": 3422,
"sha256": "14eeb50c90eacd56a78704b2dd03756fb8147cdc2dfc7754d4059d6f6d7753c1"
}
},
{ "id": "suo", "path": "suo.h5ad" }
]
}
schemais the integer2. A catalog without it is refused, naming the three keys a catalog must hold.defaultis the id of the dataset the catalog opens on.datasetslists one dataset or more. Ids are distinct, and so are paths. No other key is read; an unknown key is refused.id: the dataset’s stable id, with the same rule asuns['cellucid']['id']. When the file states an id, it must be this one.path: where the dataset is, relative to the folder: a store folder (ending in/: an AnnData or MuData Zarr store), or a file whose name a format in the table above names (an.h5ad, a.zarr.zip, an.h5mu, a Seurat.rds, …). Each part is written in canonical URL encoding (asencodeURIComponentwrites it, e.g.cell%20atlas.h5ad), without.,.., a query or a fragment, and the path stays below the folder.session(optional): the dataset’s default view, a published.cellucid-sessionfile — a session file of the view passed through the publish step, which keeps one tracker of one root (no step, no cells to open, no layout) and binds it to the catalog’s dataset id (see Starting views named by a catalog) — with its exact byte length (bytes) and lowercase SHA-256 (sha256). It is applied when the dataset is opened from the catalog (Starting views named by a catalog). The length and digest pin it to the catalog: a session file that differs is refused rather than applied.
The dataset list shows each dataset’s name and cell count, read from its
file’s metadata and uns alone (no matrix, column or index is read); the
dataset’s own file is opened when it is chosen. A Zarr store folder on a web
host needs consolidated metadata (.zmetadata for Zarr v2, the root
zarr.json of v3), which prepare() writes.
To compute bytes and sha256 of a session file:
import hashlib
from pathlib import Path
data = Path("pancreas.cellucid-session").read_bytes()
print(len(data), hashlib.sha256(data).hexdigest())
The worked public example is Publish a Custom Dataset Repository; the shorter publishing workflow is Share Without Running a Server (Publish a Dataset Catalog).
AnnData H5AD and Zarr (what Cellucid reads)#
The browser reads one H5AD file, one ZIP containing a complete Zarr store
(Zarr v2 or v3, sharded or not), a Zarr store folder from your disk (the
Open… › Folder…, or a dropped folder), or a Zarr store folder on a web
host (through its consolidated metadata). Server mode and Jupyter accept H5AD paths, Zarr
store folders (v2 or v3), zipped Zarr stores, an in-memory AnnData object, and
every other input of the table above. The server serves
a file prepare() wrote as it is on disk, and the browser reads it with the
same reader; an in-memory AnnData, and an AnnData, MuData, Loom or 10x Cell
Ranger file (or outs/ folder) not in priority order, are served as a Zarr
store in priority order made as the browser reads it, holding what the
browser presents from the file itself (a MuData object is written once to an
.h5mu). R objects and MatrixMarket folders given by path are served as they
are and read once by the browser.
Compression the browser reads#
H5AD: uncompressed, gzip (with or without shuffle), LZF (h5py’s own filter), Fletcher-32 checksums, and hdf5plugin’s Blosc, Zstandard, LZ4 and bitshuffle (uncompressed, LZ4 or zstd blocks) filters (
write_h5ad(compression=hdf5plugin.Blosc(...))and the like). A dataset compressed with a filter outside these (bzip2, SZIP, N-bit, scale-offset, Blosc2) is refused when it is read, naming the filter; the rest of the file still opens.Zarr: uncompressed, gzip, zlib, Blosc (blosclz, LZ4, LZ4HC, zlib, Zstandard; byte and bit shuffle, and the delta flag of a Blosc frame), LZ4, Zstandard (Zarr v3’s default), bz2, LZMA (numcodecs’ default xz container), CRC-32C, variable-length strings, and Zarr v3 sharding. An array using another codec (Snappy, ZFP, numcodecs’ Delta filter, fixed-scale-offset, …), or Blosc’s Snappy or a Blosc2 chunk (format version 3 and later), is refused when it is read, naming the codec.
A Zarr folder on Amazon S3 or Google Cloud Storage: a bucket that does not let its readers list it answers “403 Forbidden” for an object that does not exist, and zarr-python leaves out every chunk that holds only the fill value (an all-False
mask, an all-zero block). Once the bucket has served the store’s metadata, such a 403 is read as the absent chunk it is (its fill value); a bucket that refuses the metadata itself is private, and the store is refused saying so.Served from Python:
cellucid serve,serve_anndata()andshow_anndata()check every array’s filters and codecs before they start. A store holding an array the browser cannot decode is decoded on the kernel, chunk by chunk as the browser asks (h5py or zarr, with their codec plugins); the terminal names the arrays.
Minimum requirements (every loading route)#
You must have at least one obsm array Cellucid can draw: dense, numeric, one
row per cell, at least one column (a one-dimensional array is one column), with
finite values that fit in 32-bit floats and not the same coordinates for every
cell.
Which array each dimension shows by default:
obsm["X_umap_1d"],obsm["X_umap_2d"]andobsm["X_umap_3d"]fill their own dimensions; a suffixed key and its column count must agree exactly.When a file declares none of the three and carries the plain
X_umapthatsc.tl.umap()writes, that array fills the dimension of its width (1, 2 or 3 columns), so an ordinary Scanpy object loads as written.When the file has no UMAP at all, a deterministic best guess fills the dimensions instead (t-SNE, force layouts, spatial, …, and last the first two columns of a wide array such as
X_pca). See Data mapping.
This holds for every loading route: the browser file picker, cellucid serve,
and the notebook viewers. In the browser any other array can then be turned
on under Session › Data mapping and shown in any view; cellucid serve takes
--embedding-1d/--embedding-2d/--embedding-3d KEY[:c0,c1].
Files from older AnnData releases#
Files written before anndata 0.8 (2022) open as written, from the file pickers,
a dropped file, a link and cellucid serve alike:
anndata 0.7, and every file SeuratDisk’s
Convert()writes: dataframes whose categorical columns keep their categories under__categories, arrays without encoding attributes, and (Zarr) a store without a root.zattrs;anndata 0.6 and earlier:
obsandvaras one record per cell (compound datasets) with categories inuns, a compoundobsm,rawstored asraw.X/raw.var, and sparse matrices named byh5sparse_format.
Every column a file stores is shown, including those a SeuratDisk file leaves
out of its column-order (anndata itself drops those). Ordered categories and
missing values keep their meaning. Nothing needs converting first.
A column the browser cannot read#
One obs or var column the browser does not understand never refuses the
file: a column of a compound, complex or opaque type, an encoding a later
anndata introduced, the wrong length, or a column-order entry the frame does
not hold is left out, and the rest of the dataset opens. The Dataset box’s
Details list it under Not read, with the reason (“obs column ‘qc’ uses
an unsupported element type; the column was not read.”). The same holds for
raw.var and each MuData modality’s var. Opening looks at each column
itself, not at the parts a categorical or nullable column is stored in (its
codes and categories, its values and mask: read when the column is), so a
categorical with malformed codes is listed and refused when it is chosen, with
the same reason, while the rest of the dataset reads. Categorical codes may be
unsigned integers, as some writers store them (anndata writes signed ones,
with -1 for a missing value). What a file needs is its index, its shape and
X; a defect there still refuses it.
Integers stay exact#
An integer column wider than 16 bits (int32, int64, uint32, uint64, a
nullable Int64) is held as float64: every integer up to 2^53 stays exact,
so cell or donor ids and deep cells’ counts above 16,777,216 are never merged
by rounding, in colours, filters, highlights, analyses and exports. Narrower
integers and every float column are held as float32, as before.
prepare() and show_anndata(adata) take an AnnData you have already read, so
they hold the columns anndata read; pass the file’s path to cellucid serve,
serve_anndata or show_anndata to keep the unlisted ones.
Optional (but commonly expected)#
Obs fields (metadata for coloring/filtering):
adata.obs[...]Gene metadata (for gene search / naming):
adata.var[...]and/oradata.var.indexGene expression:
adata.X(dense or sparse)Connectivity:
adata.obsp["connectivities"](KNN graph)
Vector fields (optional overlays)#
Vector fields are optional per-cell displacement vectors (e.g. RNA velocity, drift) that Cellucid can render as an animated overlay.
If you expect vector fields, the key requirements are:
Correct width: exactly as many columns as the embedding the vectors are drawn over.
Correct row order: vectors must match the same cell order as points/embeddings.
Discoverable naming / metadata: so the viewer can find the fields.
See the overlay UI docs:
AnnData naming convention (obsm)#
Cellucid discovers vector fields from adata.obsm keys using scVelo’s
convention:
<field>_<basis>: the field<field>drawn over the embeddingX_<basis>
Examples:
velocity_umapoverX_umap,velocity_umap_3doverX_umap_3dT_fwd_umap,T_bwd_umapoverX_umapvelocity_pcaoverX_pca,velocity_branchesoverX_branches
Notes:
A key that starts with
X_(X_umap,X_umap_3d) is an embedding, never a vector field.The key is split at one
_into a field and a basis whoseX_<basis>the file holds; when several splits match, the longest basis wins. A key with no match is an ordinaryobsmarray, sovelocity_umap_2dbeside a bareX_umap(noX_umap_2d) is not a vector field: name itvelocity_umap.A field is bound to every enabled view of
X_<basis>when both arrays have the same number of columns. The keys of one field on every embedding are one field:velocity_umapandvelocity_umap_3dare bothvelocity.The overlay dropdown shows the fields bound to the embedding the view shows; a view showing another embedding (for example a t-SNE) has none.
How to confirm vector fields exist (quick checks)#
Print
adata.obsm.keys()and look for keys likevelocity_umapbesideX_umap.
Naming and schema details:
Common “format mismatch” problems (and what they mean)#
“Cellucid found nothing to plot”#
Meaning:
No
obsmarray can be drawn: every dense one has another row count, places no cell, or holds a value that cannot be drawn. The message lists each entry and why. Anobsmwith no dense numeric array at all (empty, or only tables and sparse arrays) is refused as having no cell embedding (Symptom: “… has no cell embedding, and Cellucid draws cells at embedding coordinates”).
Fix:
Add an embedding (for example
sc.tl.umap(adata)) or fix the array the message names. A file without a UMAP but with any drawable array opens on a best guess (Data mapping).
“Fields list is empty”#
Meaning:
obsis empty, oruns['cellucid']['defaults']['obsKeys']names only some columns.
Fix:
Ensure
adata.obsactually contains columns and is not all missing.
“Gene search returns nothing”#
Meaning:
There is no gene expression (
X), or the genes were named from avarcolumn whose vocabulary nobody is searching in.
Fix:
Provide
adata.Xand name the genes from thevarcolumn your readers will type.In Jupyter/server mode, pass
gene_id_column=...if needed.
Edge cases and limits (read if you’re debugging weirdness)#
NaN/Inf in embeddings: a cell whose row holds NaN or an infinity has no position in that embedding (one computed on some of the cells, a MuData modality that did not measure it). The embedding opens; the cell is not drawn, picked or selected in its views, a short notice counts such cells and names the first, and every other view, filter and analysis keeps them. In a latent space such a cell gets no outlier quantile; in a vector field it has no arrow. Only an embedding with no finite row, or a value past float32, is refused, naming the cell.
Duplicate gene names: made unique as AnnData’s
var_names_make_unique()does (MATR3,MATR3-1), and reported.Huge categorical fields: any number of categories loads and the legend scrolls, but palettes cannot keep 50k–100k colours distinct; consider aggregating.
Browser memory: there is no fixed size ceiling for an
.h5ador Zarr file. Each array read is held to the size its metadata declares, and one the browser cannot allocate is refused as too large, naming the bytes. A server hands the file to the browser, which reads only what it asks for.Byte order: never a compatibility problem. H5AD is byte-order-agnostic, because HDF5 converts to host order while reading; Zarr records its own byte order per array and the reader swaps when it has to.
Next steps#
Pick a workflow: Data Loading Overview (Every Route)
Load locally without Python: Browser File Picker (No Python Setup)
Serve an
.h5ador Zarr store from Python: Server Mode (CLI + Python)Publish a public dataset catalog: Publish a Custom Dataset Repository
Debug loading failures: Troubleshooting (data loading)