Vector fields (velocity / drift overlays)#
Cellucid “vector fields” are per-cell displacement vectors in embedding space.
They enable overlays such as:
RNA velocity particle flow (scVelo-style, if you already have vectors)
CellRank drift vectors derived from a transition matrix
This page documents:
Mental model (beginner-friendly)#
You have an embedding
X_<basis>inobsm, such asX_umapwith shape(n_cells, 2), or a 50-columnX_pca.You compute (or already have) vectors with the same shape.
Cellucid visualizes those vectors as an animated overlay on top of the embedding.
Vectors are not absolute positions. Each row is a displacement at the corresponding cell, and the viewer integrates those displacements into animated particle flow.
Practical path (common workflows)#
1) From a CellRank transition matrix → drift vectors#
from cellucid import compute_transition_drift
drift = compute_transition_drift(T, adata.obsm["X_umap"], normalize_rows=True)
Where:
Tis(n_cells, n_cells)(dense or sparse)adata.obsm["X_umap"]is(n_cells, k), any number of columnsk; the drift has the same shape
2) Store drift in adata.obsm using Cellucid naming conventions#
from cellucid import add_transition_drift_to_obsm
key = add_transition_drift_to_obsm(
adata,
T,
basis="umap",
field_prefix="T_fwd",
normalize_rows=True,
)
print("Wrote vector field to:", key)
This computes the drift over obsm['X_<basis>'], every column of it, and
writes obsm['<field_prefix>_<basis>']:
T_fwd_umap(overX_umap, the defaultbasis="umap")T_fwd_umap_3dwithbasis="umap_3d",T_fwd_brancheswithbasis="branches"
It refuses a basis whose X_<basis> is missing, and a field_prefix whose
key would not read back as that field on that embedding (for example when
another embedding’s longer basis claims it).
3) Open it, or store it in a Cellucid-ready AnnData#
show_anndata(adata, dataset_name=..., dataset_id=...) shows the new field
directly. To ship it in a prepared file:
from cellucid import prepare
path = prepare(
"my_study.h5ad",
format="h5ad",
embeddings={"X_umap": adata.obsm["X_umap"]},
dataset_id="my-study-v1",
dataset_name="My study",
obs=adata.obs,
var=adata.var,
gene_expression=adata.X,
vector_fields={
# A field name, then the embedding it is drawn over; stored as obsm["T_fwd_umap"]
"T_fwd": {"X_umap": adata.obsm[key]},
},
)
Naming conventions (important)#
The viewer discovers a vector field by scVelo’s naming convention: an obsm
key <field>_<basis> is the field <field> drawn over obsm['X_<basis>'], for
any basis — velocity_umap over X_umap, T_fwd_umap_3d over X_umap_3d,
velocity_branches over X_branches. These are the keys
add_transition_drift_to_obsm() writes, and the keys
prepare() stores vector_fields={field: {embedding: array}}
under. A key that starts with X_ is an embedding, never a vector field; when
a key splits into several bases the file holds, the longest basis wins. The
field is bound when the vector and its embedding have the same number of
columns, and is listed under its <field> part (velocity for
velocity_umap and velocity_umap_3d). The full rule:
Cellucid-ready AnnData specification.
API reference#
- cellucid.compute_transition_drift(transition_matrix, embedding, *, normalize_rows)[source]#
Compute exact per-cell transition drift in embedding space.
The drift of cell
iis the transition-weighted mean position of its successors minus its own position:T @ E - E, with each row ofTfirst divided by its sum whennormalize_rowsis True.- Parameters:
transition_matrix (numpy.ndarray or scipy.sparse matrix) – Non-negative, finite
(n_cells, n_cells)cell-cell transition matrix (CellRank’s, for example).embedding (numpy.ndarray) – Finite
(n_cells, n_dims)coordinates the drift is computed in; a 1D array is not accepted. Values must be representable in float32.normalize_rows (bool) – Required. True divides every row of
transition_matrixby its sum (every row sum must then be nonzero); False uses it as given.
- Returns:
(n_cells, n_dims)float64 drift, at the precision it was computed in; it is rounded to float32 once, when it is written.- Return type:
- Raises:
TypeError – A non-bool
normalize_rows, or non-numeric input.ValueError – A shape mismatch, a negative or non-finite value, a value outside the float32 range, or a zero row sum with
normalize_rows=True.
- cellucid.add_transition_drift_to_obsm(adata, transition_matrix, *, basis='umap', field_prefix='T_fwd', normalize_rows, overwrite=False)[source]#
Compute transition drift over
obsm['X_<basis>']and store it as<field_prefix>_<basis>.The drift is computed in the coordinates of the embedding
X_<basis>, every column of it, and stored asobsm['<field_prefix>_<basis>'], as wide as the embedding: the key the viewer binds to that embedding by default (T_fwd_umapoverX_umap,T_fwd_umap_3doverX_umap_3d,T_fwd_branchesoverX_branches).- Parameters:
adata (anndata.AnnData) – The AnnData holding
obsm['X_<basis>']; changed in place.transition_matrix (numpy.ndarray or scipy.sparse matrix) – Non-negative, finite
(n_obs, n_obs)cell-cell transition matrix.basis (str) – The embedding
obsm['X_<basis>']the drift is computed in.field_prefix (str) – The vector field name: the drift is stored as
obsm['<field_prefix>_<basis>'].normalize_rows (bool) – Required; see
compute_transition_drift().overwrite (bool) – Replace an existing
obsm['<field_prefix>_<basis>'].
- Returns:
The
obsmkey written.- Return type:
- Raises:
KeyError –
obsm['<field_prefix>_<basis>']exists andoverwriteis False.ValueError –
obsm['X_<basis>']is missing, the key would not read back as the fieldfield_prefixon that embedding, or the inputs are invalid (seecompute_transition_drift()).
Edge cases (do not skip)#
Row normalization (transition matrices)#
Choose
normalize_rows=Trueto divide each transition row by its sum, ornormalize_rows=Falsewhen the matrix already has the exact row semantics you intend. The choice is required.With
normalize_rows=True, a row that sums to 0 (a cell with no successor) is refused:Every transition_matrix row must have a nonzero finite row sum when normalize_rows=True.Give the cell a successor (a self-transition), or pass an already row-stochastic matrix withnormalize_rows=False, where an empty row is a cell drifting to the origin.
Shape mismatches#
transition_matrixmust be(n_cells, n_cells)embeddingmust be(n_cells, k), any number of columnskThe product
T @ embeddingmust produce(n_cells, k)
Missing embeddings for a field#
If a dataset has a 3D UMAP but the field has only an array for the 2D one, the 3D overlay will not be available.
Troubleshooting (symptom → diagnosis → fix)#
Symptom: “embedding must be a 2D array, got shape …”#
Fix:
Ensure you pass an array shaped
(n_cells, dim)(not a flattened vector).
Symptom: “transition_matrix must have shape (n, n), got …”#
Fix:
Confirm
Tis(n_cells, n_cells)and aligned to the same cell order as the embedding.
Symptom: “Vector overlay is not available / not visible in the viewer”#
Likely causes:
The vector field is not in
obsmunder a<field>_<basis>key beside itsX_<basis>(avelocity_umap_2dbeside a bareX_umapwith noX_umap_2dno longer binds; scVelo’svelocity_umapdoes).The vector does not have as many columns as its embedding.
The view shows another embedding than the one the field is named for.
How to confirm:
Confirm the vector field exists in
adata.obsm(or the prepared file’sobsm) with the expected key.
Fix:
Store the vectors with
add_transition_drift_to_obsm, or viaprepare(..., vector_fields={...}).Name each array after its embedding, such as
velocity_umapforX_umapandvelocity_umap_3dforX_umap_3d.
Symptom: “Drift vectors look backwards”#
Likely causes:
You used a backward transition matrix or the wrong convention for
field_prefix.
Fix:
Compute/label forward vs backward consistently (e.g.,
field_prefix="T_fwd"vs"T_bwd").Validate by checking a few expected transitions in a small subset.
See also#
Data preparation (prepare) for storing vector fields via
prepare(...)The R package’s
cellucid_transition_drift(), the same computation, held to the same shared conformance fixture: Vector fields (velocity / displacement)