Introduction to MultiAssaySpatialExperiment

Installation

if (!require("BiocManager"))
    install.packages("BiocManager")
BiocManager::install("MultiAssaySpatialExperiment")

Load the package:

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

Citing MultiAssaySpatialExperiment

If you use MultiAssaySpatialExperiment in your research, please cite the package. Citation information is available via:

citation("MultiAssaySpatialExperiment")

Why another spatial data structure?

If you have spent a few decades designing databases, you have seen the same story many times: someone builds a system that works for one use case, and it spreads. Later, a new use case appears that does not quite fit. People bolt on workarounds, and the design gets messy. Eventually, someone steps back and asks: what would we build if we started from the problem we actually have?

Spatial omics is at that stage. We already have excellent tools for single-assay spatial data: SpatialExperiment and SpatialFeatureExperiment handle one experiment at a time—one technology, one matrix, one set of spatial coordinates. They work well when your question is “what genes are expressed in this Visium section?” or “where are these MERFISH cells in tissue?” But increasingly, people ask questions that cut across assays: “I have RNA, protein, and chromatin from the same tissue—how do I link them and their spatial coordinates in one place?” That is a different problem. It needs a multi-assay structure that can carry spatial context and keep everything consistent when you subset, or merge.

MultiAssayExperiment (MAE) solves the multi-assay part. It gives you a central mapping table—the sampleMap—that links assay columns to specimens. One source of truth, explicit foreign keys, and validity checks that keep the links correct. When you subset by specimen, the maps update. No orphaned rows. No hunting through metadata to figure out which assay column belongs to which sample.

What MAE does not have is spatial geometry: coordinates for cells or spots, cell boundaries, tissue images. That is where MultiAssaySpatialExperiment (MASE) comes in. MASE extends MAE with spatial elements—points, shapes, images, labels—and a second central mapping table, the spatialMap, that links assay columns to those elements. Same philosophy as MAE: one place to look, one place to update, referential integrity enforced. Subsetting updates spatialMap (and, for most column- and assay-level operations, linked spatial layers) so you do not have to clean up coordinates or drop orphaned geometries manually.

When to use MASE

Use MultiAssaySpatialExperiment when:

  • You have multiple spatial assays from the same or related tissue (e.g., transcriptomics, proteomics, ATAC-seq).
  • You need one object that holds the experiments, the specimen metadata, the spatial elements (coordinates, shapes, images), and the links between them.
  • You want subsetting and merging that keep all links consistent—no manual cleanup, no orphaned geometry.
  • You need to convert to or from SpatialExperiment or SpatialFeatureExperiment for downstream tools.

Anatomy of a MultiAssaySpatialExperiment

Overview

A MultiAssaySpatialExperiment object contains:

  1. Inherited from MultiAssayExperiment:
    • ExperimentList — your assays (can be matrices, SummarizedExperiment, SingleCellExperiment, SpatialExperiment, or other supported classes)
    • colData — specimen/sample metadata (one row per biological unit)
    • sampleMap — links assay columns to specimens (primary identifiers)
    • metadata — experiment-level metadata
  2. Spatial elements (MASE-specific):
    • points — named lists of point coordinates (DataFrame with x, y, instance_id)
    • shapes — named lists of polygons or other geometries (DataFrame with sfc geometry column)
    • images — raster images (tissue photos, microscopy)
    • labels — segmentation masks (raster with instance IDs)
  3. Spatial mappings:
    • spatialMap — links assay columns to spatial element rows (assay, colname, element_type, region, instance_id)
    • imgData — links specimens to images (sample_id, image_id, scaleFactor, and optional raster metadata)

The following diagram shows how these components relate:

Entity-Relationship Diagram showing MASE relational schema

Entity-Relationship Diagram showing MASE relational schema

The ERD shows the actual class definition with all slots:

Inherited from MultiAssayExperiment (tan boxes): - ExperimentList — named list of assays (SE, SCE, matrices, etc.) - colData — specimen metadata (rownames = primary identifiers) - sampleMap — links assay columns to specimens via (assay, primary, colname)

MASE-specific slots (blue boxes): - pointsPointsLayerList: named list of DataFrames with coordinates - shapesShapesLayerList: named list of DataFrames with geometries
- imagesRasterLayerList: named list of SpatialImage/matrix (tissue photos, H&E, etc.) - labelsRasterLayerList: named list of SpatialImage/matrix (segmentation masks) - spatialMap — DataFrame linking assay columns to spatial elements via (assay, colname, element_type, region, instance_id) - imgData — DataFrame linking specimens to images via (sample_id, image_id)

Key relationships: - spatialMap$element_type specifies which spatial slot: "points" or "shapes" - spatialMap$region references a layer name within that slot (e.g., "coords", "cells") - spatialMap$instance_id references a row within that layer’s DataFrame - Foreign key: (element_type, region, instance_id)slot(mase, element_type)[[region]] - Each layer is accessed as a named element: points[[region]], shapes[[region]], images[[image_id]], labels[[image_id]]

Components

ExperimentList: assay data

The ExperimentList holds your experimental data. Each element is an assay (e.g., RNA-seq, proteomics, ATAC-seq). Assays can be of different classes:

  • matrix — base R matrix
  • SummarizedExperiment — single-assay container with rowData, colData
  • SingleCellExperiment — extends SummarizedExperiment with reducedDims, etc.
  • SpatialExperiment — extends SingleCellExperiment with spatialCoords
  • SpatialFeatureExperiment — extends SpatialExperiment with sf geometries

Requirements for classes in ExperimentList:

  • Must support [ subsetting
  • Must have colnames() (used for linking to specimens via sampleMap)
  • Row names are optional but useful for feature identification

colData: specimen metadata

The colData is a DataFrame with one row per specimen (biological unit). Row names are primary identifiers. Columns store metadata like patient ID, tissue type, treatment, or any other specimen-level attribute.

Terminology: a specimen is a colData row; an observation is an assay column (cell, spot, or bin). The sampleMap links observations to specimens.

sampleMap: assay-to-specimen mapping

The sampleMap is a three-column DataFrame:

Column Type Description
assay factor Name of the assay in ExperimentList
primary character Row name in colData (specimen ID)
colname character Column name in the assay

This table says: “column X of assay Y belongs to specimen Z.” Multiple assay columns can map to the same specimen (technical or biological replicates).

points: spatial coordinates

The points slot is a PointsLayerList (a named list of DataFrame objects). Each element represents a set of point coordinates (e.g., cells, spots, molecules). Required columns:

  • x, y — numeric coordinates
  • instance_id — character identifier for each point

Optional columns: z (for 3D), feature annotations, quality metrics.

shapes: spatial geometries

The shapes slot is a ShapesLayerList (a named list of DataFrame objects). Each element represents a set of polygons, lines, or other geometries. Required columns:

  • geometry — sfc (simple feature column from the sf package)
  • instance_id — character identifier for each shape

Shapes are commonly used for cell boundaries, tissue regions, or annotation areas.

images: raster images

The images slot is a RasterLayerList (a named list). Each element is a raster image (e.g., tissue H&E, fluorescence microscopy). Images can be:

  • Base R arrays
  • SpatRaster (from the terra package)
  • File paths to images on disk

labels: segmentation masks

The labels slot is a RasterLayerList (a named list). Each element is a raster where pixel values are instance IDs (e.g., cell ID 1, 2, 3, …). Labels are used for segmentation masks that define which pixels belong to which object.

spatialMap: assay-to-spatial mapping

The spatialMap 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 Spatial slot: "points" or "shapes"
region character Layer name within that slot (e.g., "coords", "cells")
instance_id character Row identifier in the spatial layer

This table says: “column X of assay Y is spatially located at row Z of element W.” The spatialMap is the key innovation: it allows multiple assays to share the same spatial elements, and it keeps spatial links consistent during subsetting.

Validity constraints:

  • Every row in spatialMap must correspond to a valid row in sampleMap (same assay + colname).
  • Every instance_id must exist in the corresponding points or shapes element.

imgData: specimen-to-image mapping

The imgData is a DataFrame with one row per image–specimen pair (Visium readers populate SPE-compatible columns). Typical columns include:

  • sample_id — row name in colData (specimen identifier)
  • image_id — name of the image in the images slot
  • scaleFactor — numeric scale factor for pixel ↔︎ coordinate conversion
  • data, width, height, path — raster payload or file reference (when loaded).

Quick Start: Building a MASE object

Here is a minimal example to get you started:

# Create a simple expression matrix
mat <- matrix(rnorm(20), nrow = 5, ncol = 4,
  dimnames = list(paste0("Gene", 1:5), paste0("Cell", 1:4)))

# Create point coordinates
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))

# Construct MASE
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

For more construction examples, including:

  • Multi-assay MASE with shapes
  • Building from matrices, SummarizedExperiment, SingleCellExperiment
  • Adding images and labels
  • Validation and common mistakes

See the Working with MultiAssaySpatialExperiment vignette.

Accessors

Inherited from MultiAssayExperiment

All MAE accessors work on MASE:

# Get experiments
experiments(mase)
#> ExperimentList class object of length 1:
#>  [1] rna: matrix with 5 rows and 4 columns

# Get specimen metadata
colData(mase)
#> DataFrame with 4 rows and 0 columns

# Get sampleMap
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

# Subset by assay
mase[, , "rna"]
#> 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

Spatial element accessors

MASE provides accessors for spatial elements:

# Get all spatial points
spatialPoints(mase)
#> PointsLayerList of length 1 
#> [1] coords: DFrame (4 x 3)

# Get names of point elements
spatialPointNames(mase)
#> [1] "coords"

# Get a specific point element
spatialPoint(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

# Get spatial images (empty in this example)
spatialImages(mase)
#> RasterLayerList of length 0

# Get spatial labels (empty in this example)
spatialLabels(mase)
#> RasterLayerList of length 0

Mapping accessors

# Get spatialMap
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

# Get imgData (empty in this example)
imgData(mase)
#> NULL

Subsetting

MASE supports all MAE subsetting operations, with propagation to spatial slots where applicable:

  • subsetByColData(x, y) — subset by specimen; updates spatialMap and imgData (point and shape rows are not trimmed unless removed via the map)
  • subsetByRow(x, y, ...) — subset assay features (spatial layers unchanged)
  • subsetByColumn(x, y) — subset assay columns; when y is a list, also trims linked points, shapes, images, and labels via spatialMap
  • subsetByAssay(x, y) — subset by assay name; keeps spatial layers referenced in spatialMap for the retained assays
  • x[i, j, k, ...] — combined subsetting (specimens, columns, assays, features)

Filter by observation metadata using the standard MAE list interface, for example subsetByColumn(mase, list(rna = colData(experiments(mase)[["rna"]])$tissue == "tumor")).

Spatial subsetting (geometry first, then assays via spatialMap):

  • subsetByBoundingBox(x, xmin, xmax, ymin, ymax, ...) — rectangular ROI
  • subsetByPolygon(x, polygon, ...) — polygon ROI

See the Working with MultiAssaySpatialExperiment vignette for detailed subsetting examples.

Spatial operations

MASE provides generics for spatial annotation and aggregation:

  • annotateWithRegions(x, points =, shapes =, ...) — annotate measurement points with the shapes (cells, segments) they fall in
  • aggregateByRegion(x, by =, FUN =, ...) — aggregate assay values by annotated region (e.g., sum transcript counts per cell)
  • spatialJoin(x, y, ...) — join two spatial layer tables (DataFrame × DataFrame via sf; lower-level primitive for custom annotation workflows)

These functions use the sf package for spatial joins (default: st_intersects). You can customize the join function (e.g., st_nearest_feature).

See the Working with MultiAssaySpatialExperiment vignette for detailed examples.

Coercion

MASE can convert to and from other Bioconductor spatial classes (when the relevant packages are installed):

  • as(spe, "MultiAssaySpatialExperiment") — wrap a SpatialExperiment as a single-assay MASE
  • as(mase, "SpatialExperiment") — requires exactly one SPE-compatible assay
  • as(sfe, "MultiAssaySpatialExperiment") — wrap a SpatialFeatureExperiment (requires SpatialFeatureExperiment)
  • as(mase, "SpatialFeatureExperiment") — requires exactly one SPE/SFE-compatible assay

See the MultiAssaySpatialExperiment use cases vignette for coercion examples.

Quick reference

The most common operations, grouped by task (see the man pages with ? and the other vignettes for details):

Task Functions
Construct MultiAssaySpatialExperiment(), prepMASE(), buildSpatialMap()
Read a platform readXeniumMASE(), readVisiumMASE(), readVisiumHDMASE(), readCosMxMASE(), readMERSCOPEMASE()
Spatial accessors spatialPoints(), spatialShapes(), spatialImages(), spatialLabels(), spatialMap()
MAE accessors experiments(), colData(), sampleMap(), subsetByAssay()
Subset mase[rows, cols, assays], by region (bounding box / polygon), by specimen
Annotate / aggregate annotateWithRegions(), aggregateByRegion() (points ↔︎ shapes joins)
Labels ↔︎ shapes rasterize (shapes → labels), vectorize (labels → shapes)
Coerce as(spe, "MultiAssaySpatialExperiment"), as(mase, "SpatialExperiment")

Next steps

After reading this introduction, proceed to:

  1. Working with MultiAssaySpatialExperiment: construction patterns, subsetting operations, spatial annotation and aggregation, and labels ↔︎ shapes conversion.
  2. MultiAssaySpatialExperiment use cases: complete workflows for reading Xenium, Visium, Visium HD, CosMx, and MERSCOPE (MERFISH) data; converting from SpatialExperiment / SpatialFeatureExperiment; multi-assay integration (Xenium + Visium); coordinate transformation and alignment; and multi-sample analysis.

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.2 MultiAssayExperiment_1.39.0       
#>  [3] SummarizedExperiment_1.43.0        Biobase_2.73.1                    
#>  [5] GenomicRanges_1.65.1               Seqinfo_1.3.0                     
#>  [7] IRanges_2.47.2                     S4Vectors_0.51.5                  
#>  [9] BiocGenerics_0.59.10               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-23               
#>  [3] SparseArray_1.13.2          KernSmooth_2.23-26         
#>  [5] lattice_0.22-9              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-5               
#> [13] e1071_1.7-17                DBI_1.3.0                  
#> [15] BiocManager_1.30.27         SingleCellExperiment_1.35.2
#> [17] codetools_0.2-20            jquerylib_0.1.4            
#> [19] abind_1.4-8                 cli_3.6.6                  
#> [21] rlang_1.3.0                 units_1.0-1                
#> [23] XVector_0.53.0              cachem_1.1.0               
#> [25] DelayedArray_0.39.3         yaml_2.3.12                
#> [27] BiocBaseUtils_1.15.1        otel_0.2.0                 
#> [29] S4Arrays_1.13.0             tools_4.6.1                
#> [31] SpatialExperiment_1.23.0    buildtools_1.0.0           
#> [33] R6_2.6.1                    proxy_0.4-29               
#> [35] lifecycle_1.0.5             classInt_0.4-11            
#> [37] magick_2.9.1                bslib_0.11.0               
#> [39] Rcpp_1.1.2                  sf_1.1-1                   
#> [41] xfun_0.60                   sys_3.4.3                  
#> [43] knitr_1.51                  rjson_0.2.23               
#> [45] htmltools_0.5.9             rmarkdown_2.31             
#> [47] maketools_1.3.2             compiler_4.6.1