This vignette is for readers who want to know how a
MultiAssaySpatialExperiment is put together: which slots
exist, what the mapping tables mean, and which invariants the class
maintains. For day-to-day use start with Introduction to
MultiAssaySpatialExperiment, which works through a complete
example, and Working with MultiAssaySpatialExperiment for
construction and subsetting recipes.
A MASE object has three parts. It inherits the
assays, specimen metadata and sampleMap of a
MultiAssayExperiment. It adds four spatial
element layers: points, shapes, images and labels. And it carries one
extra mapping table, spatialMap, that says where in space
each assay column sits, alongside imgData, which associates
specimens with images.
The quickest way to see all three is to build a small object and take it apart.
mat <- matrix(rnorm(20), nrow = 5, ncol = 4,
dimnames = list(paste0("Gene", 1:5), paste0("Cell", 1:4)))
pts <- DataFrame(
x = c(1.2, 2.5, 3.1, 4.8),
y = c(1.5, 2.3, 3.7, 4.2),
instance_id = paste0("Cell", 1:4))
mase <- MultiAssaySpatialExperiment(
experiments = ExperimentList(rna = mat),
colData = DataFrame(row.names = paste0("Cell", 1:4)),
sampleMap = DataFrame(
assay = factor("rna", "rna"),
primary = paste0("Cell", 1:4),
colname = paste0("Cell", 1:4)
),
points = PointsLayerList(coords = pts),
spatialMap = DataFrame(
assay = factor("rna", "rna"),
colname = paste0("Cell", 1:4),
element_type = "points",
region = factor("coords", "coords"),
instance_id = paste0("Cell", 1:4)
)
)
mase
#> A MultiAssaySpatialExperiment object of 1 listed
#> experiment with a user-defined name and respective class.
#> Containing an ExperimentList class object of length 1:
#> [1] rna: matrix with 5 rows and 4 columns
#> Functionality:
#> experiments() - obtain the ExperimentList instance
#> colData() - the primary/phenotype DataFrame
#> sampleMap() - the sample coordination DataFrame
#> `$`, `[`, `[[` - extract colData columns, subset, or experiment
#> *Format() - convert into a long or wide DataFrame
#> assays() - convert ExperimentList to a SimpleList of matrices
#> exportClass() - save data to flat files
#> Spatial elements:
#> spatialImages: 0 elements
#> spatialLabels: 0 elements
#> spatialPoints: 1 element
#> spatialShapes: 0 elements
#> imgData: NULL
#> spatialMap: presentThe assays live in an ExperimentList, one element per
assay. Elements may be plain matrices, or any class supporting
[ and colnames():
SummarizedExperiment, SingleCellExperiment,
SpatialExperiment, or
SpatialFeatureExperiment.
experiments(mase)
#> ExperimentList class object of length 1:
#> [1] rna: matrix with 5 rows and 4 columns
colData(mase)
#> DataFrame with 4 rows and 0 columns
sampleMap(mase)
#> DataFrame with 4 rows and 3 columns
#> assay primary colname
#> <factor> <character> <character>
#> 1 rna Cell1 Cell1
#> 2 rna Cell2 Cell2
#> 3 rna Cell3 Cell3
#> 4 rna Cell4 Cell4colData holds one row per specimen, the
biological unit, and its row names are the primary identifiers. An
observation is an assay column: a cell, spot or bin.
Those are different things, and sampleMap is what relates
them:
| Column | Type | Description |
|---|---|---|
assay |
factor | Name of the assay in ExperimentList |
primary |
character | Row name in colData (specimen identifier) |
colname |
character | Column name in the assay |
Each row reads “column X of assay Y belongs to specimen Z”. Several assay columns may map to one specimen, which is how replicates and multi-assay specimens are expressed.
Each spatial slot is a named list, so a single object can carry several layers of the same kind: cell centroids and transcript locations as two points layers, say, or cell boundaries and tissue regions as two shapes layers.
| Slot | Accessor | Class | Each element is |
|---|---|---|---|
points |
spatialPoints() |
PointsLayerList |
a DataFrame with x, y and
instance_id (plus z or annotations) |
shapes |
spatialShapes() |
ShapesLayerList |
a DataFrame with an sf
geometry column and instance_id |
images |
spatialImages() |
RasterLayerList |
a raster: an array, a terra SpatRaster, or
a path on disk |
labels |
spatialLabels() |
RasterLayerList |
a raster whose pixel values are instance identifiers |
spatialPoints(mase)
#> PointsLayerList of length 1
#> [1] coords: DFrame (4 x 3)
spatialPoints(mase)[["coords"]]
#> DataFrame with 4 rows and 3 columns
#> x y instance_id
#> <numeric> <numeric> <character>
#> 1 1.2 1.5 Cell1
#> 2 2.5 2.3 Cell2
#> 3 3.1 3.7 Cell3
#> 4 4.8 4.2 Cell4
# empty in this minimal object, but the accessors always exist
spatialShapes(mase)
#> ShapesLayerList of length 0
spatialImages(mase)
#> RasterLayerList of length 0spatialMap is the piece that makes the container more
than a bag of slots. It is a five-column DataFrame:
| Column | Type | Description |
|---|---|---|
assay |
factor | Name of the assay in ExperimentList |
colname |
character | Column name in the assay |
element_type |
character | Which spatial slot: "points" or
"shapes" |
region |
character | Layer name within that slot (e.g. "coords",
"cells") |
instance_id |
character | Row identifier within that layer |
spatialMap(mase)
#> DataFrame with 4 rows and 5 columns
#> assay colname element_type region instance_id
#> <factor> <character> <character> <factor> <character>
#> 1 rna Cell1 points coords Cell1
#> 2 rna Cell2 points coords Cell2
#> 3 rna Cell3 points coords Cell3
#> 4 rna Cell4 points coords Cell4Each row reads “column X of assay Y is located at row Z of layer W”. Because the link is a table rather than a slot on the assay, several assays can share one set of geometries, and subsetting the object rewrites this table so the links stay consistent instead of leaving orphaned geometry behind.
The triple (element_type, region, instance_id) resolves
to a row of slot(mase, element_type)[[region]], and
validity requires that every spatialMap row matches a
sampleMap row on (assay, colname) and that
every instance_id exists in the layer it names.
imgData plays the same role for images, with one row per
image-specimen pair: sample_id (a colData row
name), image_id (a name in the images slot),
scaleFactor for pixel-to-coordinate conversion, and, once
loaded, the raster payload or file reference in data,
width, height and path.
Putting those together, the object is a small relational schema:
Entity-relationship diagram of the MASE schema. Tan entities are inherited from MultiAssayExperiment; blue entities are added by MASE.
Each box is one accessor’s return value, and its rows are that
object’s columns, with PK marking an identifying column and
FK one that points at another box. The line endings are
standard crow’s-foot notation:
a double bar means exactly one, a circle-and-bar means zero or one, a crow’s foot with a circle means zero or more, and a crow’s foot with a bar means one or more.
So the line from colData to sampleMap reads
“one specimen has zero or more sampleMap rows”, which is
the replicate case described above.
For construction beyond this minimal example, including multi-assay objects, shapes, images and labels, and the validity errors you are likely to hit first, see the Working with MultiAssaySpatialExperiment vignette.
sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats4 stats graphics grDevices utils datasets methods
#> [8] base
#>
#> other attached packages:
#> [1] MultiAssaySpatialExperiment_0.99.12 MultiAssayExperiment_1.39.1
#> [3] SummarizedExperiment_1.43.0 Biobase_2.73.2
#> [5] GenomicRanges_1.65.4 Seqinfo_1.3.2
#> [7] IRanges_2.47.5 S4Vectors_0.51.9
#> [9] BiocGenerics_0.59.12 generics_0.1.4
#> [11] MatrixGenerics_1.25.0 matrixStats_1.5.0
#> [13] BiocStyle_2.41.0
#>
#> loaded via a namespace (and not attached):
#> [1] sass_0.4.10 class_7.3-24
#> [3] SparseArray_1.13.2 KernSmooth_2.23-27
#> [5] lattice_0.23-1 digest_0.6.39
#> [7] magrittr_2.0.5 evaluate_1.0.5
#> [9] grid_4.6.1 fastmap_1.2.0
#> [11] jsonlite_2.0.0 Matrix_1.7-6
#> [13] e1071_1.7-17 DBI_1.3.0
#> [15] BiocManager_1.30.27 SingleCellExperiment_1.35.2
#> [17] jquerylib_0.1.4 abind_1.4-8
#> [19] cli_3.6.6 rlang_1.3.0
#> [21] units_1.0-1 XVector_0.53.0
#> [23] cachem_1.1.0 DelayedArray_0.39.6
#> [25] yaml_2.3.12 otel_0.2.0
#> [27] S4Arrays_1.13.0 tools_4.6.1
#> [29] SpatialExperiment_1.23.0 buildtools_1.0.0
#> [31] R6_2.6.1 proxy_0.4-29
#> [33] lifecycle_1.0.5 classInt_0.4-11
#> [35] magick_2.9.1 bslib_0.12.0
#> [37] Rcpp_1.1.2 sf_1.1-2
#> [39] xfun_0.60 sys_3.4.3
#> [41] knitr_1.51 rjson_0.2.23
#> [43] htmltools_0.5.9 rmarkdown_2.32
#> [45] maketools_1.3.2 compiler_4.6.1