Zero-shot Quickstart (Single Sample)#
This quickstart embeds a single spatial transcriptomics sample with a pretrained TERRA model — no training required — and uses the embeddings to identify cell types and spatial niches.
You will:
Download a public single-cell-resolution spatial dataset (10x Xenium).
Download a pretrained TERRA model from the Hugging Face Hub.
Run the harmonize → tokenize → embed pipeline to obtain cell and neighborhood embeddings.
Visualize and cluster the embeddings.
Note
TERRA requires an NVIDIA GPU for the embedding step.
Working with several sections or donors? See Zero-shot Quickstart (Multiple Samples). For gene-level analyses, see Downstream Analysis. Setup details are in the Installation guide and concepts in the User Guide.
1. Setup#
import logging
import numpy as np
import scanpy as sc
import squidpy as sq
from terra import download_pretrained, harmonize_tokenize_embed_pipeline
from terra.datasets import read_xenium_10x
logging.basicConfig(level="INFO") # see TERRA progress
sc.settings.set_figure_params(dpi=80, frameon=False)
2. Download the demo dataset#
We use the 10x Genomics Xenium Prime FFPE Human Skin sample — human, single-cell resolution, raw counts, and the 5,000-gene Xenium Prime panel (an established standard; TERRA also works with smaller panels). read_xenium_10x downloads and assembles it for you.
# Xenium Prime FFPE Human Skin (5,000-gene panel). The reader downloads the
# two small standalone files (~44 MB) on first run and caches them.
adata = read_xenium_10x(
"https://cf.10xgenomics.com/samples/xenium/3.0.0/Xenium_Prime_Human_Skin_FFPE/Xenium_Prime_Human_Skin_FFPE",
"data/xenium_skin",
)
adata.obs["cell_id"] = adata.obs_names.astype(str)
adata.obs["sample"] = "skin"
# Crop a contiguous tissue window (so spatial neighborhoods stay intact) to keep
# the tutorial fast. Centre it on the median cell position -- the bounding-box
# corner is often empty space. Widen `half_window` (microns) for more cells.
xy = adata.obsm["spatial"]
center = np.median(xy, axis=0)
half_window = 500
keep = (np.abs(xy - center) <= half_window).all(axis=1)
adata = adata[keep].copy()
print(adata)
# Quick look: total counts per cell, in space.
adata.obs["total_counts"] = np.asarray(adata.X.sum(axis=1)).ravel()
sq.pl.spatial_scatter(adata, color="total_counts", shape=None, size=4)
3. Download a pretrained TERRA model#
download_pretrained fetches a self-contained model bundle (checkpoint, tokenizer, and the gene-reference files used for harmonization) and returns the local folder path.
# By default the model is cached in the Hugging Face cache; pass
# local_dir="terra_model" to download it into a folder of your choice.
model_dir = download_pretrained("Lotfollahi-lab/TERRA-112M")
4. Embed the data (zero-shot)#
harmonize_tokenize_embed_pipeline runs the full workflow: it maps genes to the model’s vocabulary, builds the per-cell neighborhood token sequences, and runs the model. The embeddings land in adata.obsm.
adata = harmonize_tokenize_embed_pipeline(
adata=adata,
sample_key=None, # a single sample
batch_key="sample",
model_folder_path=model_dir,
cache_directory_path="./terra_cache",
batch_size=128, # lower this if you run out of GPU memory
)
# Zero-shot embeddings now live in adata.obsm:
# "cell_emb" -> each cell's own expression
# "neighborhood_emb" -> the cell together with its spatial neighborhood
print(list(adata.obsm))
Sanity check: are the embeddings informative?#
A very high average cosine similarity (≈1) means the embeddings have collapsed and carry little signal.
for emb_key in ["cell_emb", "neighborhood_emb"]:
X = adata.obsm[emb_key]
X = X / np.linalg.norm(X, axis=1, keepdims=True)
n = X.shape[0]
S = X.sum(axis=0)
avg_cos = (S @ S - n) / (n * (n - 1))
print(f"{emb_key}: average cosine similarity = {avg_cos:.3f}")
5. Visualize the embeddings#
Compute a UMAP for each embedding space.
for emb_key in ["cell_emb", "neighborhood_emb"]:
sc.pp.neighbors(adata, use_rep=emb_key, key_added=emb_key)
sc.tl.umap(adata, neighbors_key=emb_key)
sc.pl.umap(adata, neighbors_key=emb_key, title=emb_key)
6. Niche & cell-type identification#
Cluster the neighborhood embedding to find spatial niches, and the cell embedding to find cell types.
sc.tl.leiden(adata, neighbors_key="neighborhood_emb", key_added="niche",
resolution=0.6, flavor="igraph", n_iterations=2, directed=False)
sc.tl.leiden(adata, neighbors_key="cell_emb", key_added="cell_type",
resolution=0.6, flavor="igraph", n_iterations=2, directed=False)
sc.tl.umap(adata, neighbors_key="neighborhood_emb")
sc.pl.umap(adata, neighbors_key="neighborhood_emb", color="niche",
palette="tab20", title="Niches")
sc.tl.umap(adata, neighbors_key="cell_emb")
sc.pl.umap(adata, neighbors_key="cell_emb", color="cell_type",
palette="tab20b", title="Cell types")
# Niches and cell types in space, with distinct colour palettes.
sq.pl.spatial_scatter(adata, color="niche", shape=None, size=6, palette="tab20")
sq.pl.spatial_scatter(adata, color="cell_type", shape=None, size=6, palette="tab20b")
Next steps#
Zero-shot Quickstart (Multiple Samples) — the same workflow across multiple sections/donors with batch integration.
Downstream Analysis — gene-level embeddings, spatial gene-pair scores, EMD spatial structure, and in-silico perturbation.