Design of MultiAssaySpatialExperiment

library(MultiAssaySpatialExperiment)
library(SummarizedExperiment)
library(S4Vectors)

Scope

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.

Anatomy of a MultiAssaySpatialExperiment

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: present

What it inherits from MultiAssayExperiment

The 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       Cell4

colData 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.

The spatial layers it adds

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 0

The tables that tie observations to space

spatialMap 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       Cell4

Each 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.

The relational schema

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.

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.

Session info

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