Skip to content

SpatialData

SCIMAP Pro speaks SpatialData natively. There is no separate API and no conversion step in your analysis code: the same data argument that takes an AnnData takes a SpatialData object or a path to a .zarr store.

import scimappro as sp
import spatialdata as sd

sdata = sd.read_zarr("sample.zarr")

sdata = sp.tl.spatialDistance(sdata, sdataTable="table")
sdata.tables["table"].uns["spatial_distance"]

What data accepts

You pass scimappro does
AnnData works on it directly
"cells.h5ad" reads it (or streams it, with streamData=True)
SpatialData works on the table named by sdataTable
"sample.zarr" read_zarr, then as above

All four go through one resolver, scimappro._data, so every function behaves the same way. There is no isinstance branch scattered through the codebase and no function that supports only some of the four.

Choosing a table

sdataTable names the table to operate on. It is always the last named parameter of every function, so the positional arguments of functions with a required second argument are unaffected.

sp.tl.phenotype(sdata, "phenotype_workflow.csv", sdataTable="cells")

Leave it out when the store has exactly one table. With several, scimappro raises a ValueError that lists them:

ValueError: This SpatialData has several tables; pass sdataTable to say which
one to use. Available tables: ['cells', 'nuclei', 'membranes'].

Naming a table that does not exist raises a KeyError listing the ones that do.

Coordinates and image ids

scimappro reads coordinates from obs['X_centroid'] / obs['Y_centroid'] and groups cells by obs['imageid']. A SpatialData table usually has neither, because that information lives in the elements the table annotates.

When those columns are missing, scimappro derives them and stores them on the table, preserving row order:

  • imageid comes from the table's region-key column.
  • X_centroid / Y_centroid come from the annotated shapes, points, or labels — centroids of the geometry each row refers to.

You do not have to do anything for this to happen; pass the store and the functions find what they need. Existing columns are never overwritten, so if your table already has X_centroid, that is what gets used.

sp.tl.spatialCooccurrence(sdata, sdataTable="cells")
# Derived obs columns ['imageid', 'X_centroid', 'Y_centroid'] from the
# SpatialData elements.

Reading and writing

outputDir decides whether you get an object back or a store on disk.

sdata = sp.tl.spatialDistance(sdata, sdataTable="table")

The table is modified in place inside the container, and the same SpatialData comes back.

sp.tl.spatialDistance(sdata, sdataTable="table", outputDir="results")
# -> results/<inputFilename>.zarr, returns None
sdata = sd.read_zarr("sample.zarr")
sp.tl.spatialDistance(sdata, sdataTable="table", outputDir="sample.zarr")
# -> rewrites only the 'table' element inside sample.zarr

When outputDir points at the store the object was read from, scimappro rewrites just that one table rather than the whole store.

Functions that replace the table

tl.classify, tl.rename, pp.dropFeatures, and helpers.addROI_omero build a new table rather than annotating the existing one. For SpatialData input they replace the table inside the container you passed and return that container.

Keep the annotation columns

The replacement table must keep its region_key and instance_key obs columns intact, or SpatialData's TableModel will refuse the write-back. pp.dropFeatures(sdata, dropMetaColumns=["region"]) will fail for exactly this reason.

Converting an existing cell table

pp.toSpatialData turns a scimap-style AnnData (or .h5ad) into a SpatialData: one circles element per image holding that image's cell centroids, with the whole table attached to those elements.

sdata = sp.pp.toSpatialData(adata)                 # radius from obs['Area']
sp.pp.toSpatialData(adata, outputDir="converted")  # writes converted/<name>.zarr

Details worth knowing:

  • Every obs column is preserved, so nothing is lost even though the geometry is two-dimensional.
  • radius takes an obs column name or a plain number when you do not want the default sqrt(Area / pi).
  • CellID is generated as 1..n when the table has no instance-key column.
  • Image ids that are not valid SpatialData element names are sanitised into a separate region column; imageid itself is left untouched.

Streaming

streamData=True applies to .h5ad paths only — see Streaming.

Round trip

import scimappro as sp
import spatialdata as sd

adata = sp.pp.mcmicro_to_scimap("quantification/quant.csv")
adata = sp.pp.rescale(adata, gate="manual_gates.csv")
adata = sp.tl.phenotype(adata, phenotype="phenotype_workflow.csv")

sp.pp.toSpatialData(adata, outputDir="converted")

sdata = sd.read_zarr("converted/adata_scimap.zarr")
sp.tl.spatialCooccurrence(sdata, outputDir="converted/adata_scimap.zarr")