Load an SDO
Open a Spatial Data Object in Python.
Each cell screening run produces a Spatial Data Object (SDO) as a RunID.zarr.zip archive. The SDO comes from on-instrument analysis on the AVITI24™ System, the multiomics workflow, or a standalone Cells2Stats run.
The SDO contains the per-cell tables, polony points, images, and segmentation for a run. It follows the scverse SpatialData format and is compatible with AnnData, Scanpy, and other scverse tools. You can load an SDO in three ways:
- SpatialData: Unzip the archive and load all elements into a single object. Recommended for full access to images, segmentation, tables, and points.
- AnnData: Read an individual table from an unzipped store.
- Loader: Load tables, points, and run metadata directly from the zipped archive, on local storage or Amazon S3.
Load with SpatialData
Unzip the archive
The archive contains a single top-level RunID.zarr/ directory. Extract it to a working directory:
unzip /path/to/RunID.zarr.zip -d /path/to/workdir/
The extracted store requires approximately the same disk space as the archive. Retain the .zarr.zip archive after extraction, because CytoCanvas requires the zipped SDO.
Load the store
import spatialdata as sd
sdata = sd.read_zarr("/path/to/workdir/RunID.zarr")
print(sdata)
Access tables
Tables are stored as AnnData objects, with one row per segmented cell. List the available tables, then access a table by name:
print(sorted(sdata.tables))
adata = sdata.tables["global-table-ThreePrimeUntargeted"]
img = sdata.tables["global-table-imaging"]
The tables in an SDO depend on the assays included in the run:
| Table | Contents |
|---|---|
global-table-ThreePrimeUntargeted | 3′ transcriptome counts |
global-table-imaging | CellPaint morphology and protein IF features |
global-table-{batch}-{panel}, such as global-table-B01-A549 | Counts for each target in one OPS / specialized panel, such as a guide RNA |
global-table-metadata | Run-level metadata (no cell data) |
Access images and segmentation
Images, labels (segmentation masks), and shapes (cell boundaries) are stored per well, with keys such as A1-images-cell_paint, A1-labels-Cell, and A1-shapes-Cell. For the full layout, see SDO structure.
To plot a well, use plot_image from the multiomics Python package. Pass a table and a column name to color cells by a measurement or annotation:
from elembio_spatialdata_analysis.vis.spatial import plot_image
plot_image(sdata, well="A1", outlines=True)
plot_image(sdata, well="A1", adata=adata, color="leiden") # after clustering
To access pixel data directly, select a level of the image pyramid. Images load lazily, so only the selected region is read from disk:
image = sdata.images["A1-images-cell_paint"]["scale0"].ds["image"] # (c, y, x)
nucleus = image.sel(c="Nucleus").isel(y=slice(0, 1024), x=slice(0, 1024)).values
Load a table with AnnData
To read a single table without loading the full store, pass the path of the table directory in the unzipped store to anndata.read_zarr:
import anndata as ad
adata = ad.read_zarr("/path/to/workdir/RunID.zarr/tables/global-table-ThreePrimeUntargeted")
Load with Loader
Loader, part of the multiomics Python package, loads tables, points, and run metadata directly from the zipped archive, on local storage or Amazon S3. Use Loader to keep the SDO zipped when you do not need images or segmentation. Loader also reports the contents of a run and combines tables from multiple runs.
from elembio_spatialdata_analysis.load import Loader
loader = Loader("/path/to/RunID.zarr.zip")
print(loader.summary())
Loader also accepts the following inputs:
# Amazon S3 (pass storage_options to specify an AWS profile)
loader = Loader("s3://bucket/path/to/RunID.zarr.zip", storage_options={"profile": "your-profile"})
# Public S3 bucket, no credentials
loader = Loader("s3://public-bucket/path/to/RunID.zarr.zip", storage_options={"anon": True})
# Unzipped store
loader = Loader("/path/to/workdir/RunID.zarr")
# SpatialData object loaded with sd.read_zarr
loader = Loader(sdata)
# Multiple runs
loader = Loader(["/path/to/run1.zarr.zip", "/path/to/run2.zarr.zip"])
Load tables
Loader refers to tables by name without the global-table- prefix:
print(sorted(loader.available_tables)) # e.g. ['ThreePrimeUntargeted', 'imaging']
adata = loader.load_tables(table="ThreePrimeUntargeted", lazy=False)
img = loader.load_tables(table="imaging", lazy=False)
Set lazy=False to load the table into memory, as most Scanpy functions require. When Loader is given multiple runs, load_tables returns a single AnnData object with a run_name column in obs. For OPS / specialized panel tables, set include_target_assignment=True to add each cell's target assignment to obs.
Load points
load_points returns polony locations as a Polars LazyFrame. A run contains many more polonies than cells, so filter by target type, batch, or well before calling .collect():
print(sorted(loader.available_target_types))
points = loader.load_points(target_type="ThreePrimeUntargeted", well="A1").collect()
Link cells across elements
adata.obs["cell_id_global"] uniquely identifies each cell in a run. The same ID appears as the pixel value in {well}-labels-Cell, the index of {well}-shapes-Cell, and the label_Cell_global column in points. Use it to join tables or to locate the pixels and polonies for a cell. For details, see How SDO elements link together.
Next steps
- Filter and normalize a table before clustering or differential expression.
- Follow an end-to-end cell screening tutorial.
- Work through the tutorial notebooks on GitHub.
- For agent-guided workflows, install the multiomics skills and point the agent to your SDO.
- Explore the run in CytoCanvas Desktop or CytoCanvas Studio.