--- title: "Design of MultiAssaySpatialExperiment" author: - name: Patrick Aboyoun email: aboyounp@gene.com affiliation: Genentech, Inc. date: "Compiled: `r BiocStyle::doc_date()`" package: MultiAssaySpatialExperiment output: BiocStyle::html_document: toc: true number_sections: true toc_depth: 3 toc_float: collapsed: true vignette: > %\VignetteIndexEntry{4. Design of MultiAssaySpatialExperiment} %\VignetteEncoding{UTF-8} %\VignetteEngine{knitr::rmarkdown} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r libs, message = FALSE} 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. ```{r construct_minimal} 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 ``` ## 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`. ```{r anatomy_inherited} experiments(mase) colData(mase) sampleMap(mase) ``` `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 | ```{r anatomy_spatial} spatialPoints(mase) spatialPoints(mase)[["coords"]] # empty in this minimal object, but the accessors always exist spatialShapes(mase) spatialImages(mase) ``` ## 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 | ```{r anatomy_spatialmap} spatialMap(mase) ``` 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: ```{r mase_erd, echo = FALSE, out.width = "100%", fig.cap = "Entity-relationship diagram of the MASE schema. Tan entities are inherited from MultiAssayExperiment; blue entities are added by MASE."} knitr::include_graphics("mase_erd.png") ``` 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 ```{r sessionInfo} sessionInfo() ```