This vignette demonstrates realistic workflows using
MultiAssaySpatialExperiment (MASE) for multi-assay spatial
transcriptomics analysis. It is divided into two parts:
Part 1: Data Import — Reading spatial omics data from vendor outputs (Xenium, Visium, Visium HD, CosMx, MERSCOPE) and converting from other Bioconductor spatial formats.
Part 2: Real-World Workflows — Complete analysis examples integrating multiple technologies, spatial transformations, and deconvolution.
Scientific question: How do cell-level and tissue-level spatial patterns complement each other? Can we leverage high-resolution Xenium data to deconvolve Visium spots and improve our understanding of tissue architecture?
MASE provides readers for major spatial omics platforms and coercion methods for interoperability with other Bioconductor spatial classes.
10x Genomics Xenium provides single-cell resolution subcellular imaging. The Xenium Explorer output directory contains:
cell_feature_matrix.h5: Cell × gene countscells.parquet or cells.csv.gz: Cell
coordinates and metadatanucleus_boundaries.parquet: Cell segmentation polygons
(optional)transcripts.parquet: Transcript-level coordinates
(optional, large)# Read Xenium data from output directory
mase_xenium <- readXeniumMASE(
data_dir = "path/to/xenium_output",
sample_id = "sample1",
segmentations = "cell",
add_transcripts = FALSE # Skip transcripts for faster loading
)
# The MASE object contains:
# - experiments$rna: SingleCellExperiment with cell × gene matrix
# - spatialPoints(): cell centroid coordinates (x, y, z)
# - spatialShapes(): cell or nucleus segmentation polygons (when segmentations loaded)
# - spatialMap(): links assay columns to spatial elements| Parameter | Default | Purpose |
|---|---|---|
data_dir |
required | Path to Xenium output directory |
sample_id |
NULL |
Sample identifier (uses directory name if NULL) |
segmentations |
"cell" |
Segmentation to load: "cell", "nucleus",
or "both" |
add_transcripts |
FALSE |
Load transcript-level coordinates (large!) |
images |
TRUE |
Load image metadata |
load_images |
FALSE |
Load image raster data |
Transcript-level data (millions of points) is useful for:
Skip transcripts (add_transcripts = FALSE) for faster
loading and smaller memory footprint when working with cell-level data
only.
10x Genomics Visium provides spot-based spatial transcriptomics. Space Ranger output contains:
filtered_feature_bc_matrix.h5: Spot × gene countsspatial/tissue_positions.csv: Spot coordinatesspatial/scalefactors_json.json: Image scale
factorsspatial/tissue_hires_image.png: H&E image
(optional)# Read Visium data
mase_visium <- readVisiumMASE(
data_dir = "path/to/spaceranger_output",
sample_id = "sample1",
images = TRUE,
data = "filtered"
)
# The MASE object contains:
# - experiments: spot × gene matrix (SummarizedExperiment or SCE)
# - spatialPoints(): spot coordinates (x, y in pixels or microns)
# - spatialImages(): H&E image metadata (if images = TRUE)
# - imgData(): links specimens to images
# - spatialMap(): links assay columns to spatial elementsVisium HD provides higher resolution with multiple binning levels (2
µm, 8 µm, 16 µm). Use the dedicated HD reader and specify bin size codes
"002", "008", or "016":
NanoString CosMx provides subcellular resolution with compartment-specific segmentation (nucleus, membrane, cytoplasm). Output files include:
*_exprMat_file.csv: Expression matrix*_metadata_file.csv: Cell metadata*_tx_file.csv: Transcript coordinatesCellComposite/: Composite images# Read CosMx data
mase_cosmx <- readCosMxMASE(
data_dir = "path/to/cosmx_output",
sample_id = "cosmx_sample",
fov_ids = NULL # NULL loads all FOVs; or c("1", "3", "5")
)
# The MASE object contains:
# - experiments: cell × gene matrix
# - spatialPoints(): cell coordinates
# - spatialShapes(): cell segmentation (includes compartments when available)
# - spatialMap(): links assay columns to spatial elementsCosMx data is organized by FOVs. Options:
fov_ids = NULL: Load all FOVs (combines into single
MASE)fov_ids = c("1", "3", "5"): Load specific FOVsfov_ids = "1": Load a single FOVVizgen MERSCOPE provides high-throughput spatial transcriptomics (often referred to as MERFISH). Output directory contains:
cell_by_gene.csv: Cell × gene matrixcell_metadata.csv: Cell coordinates and metadatadetected_transcripts.csv: Transcript coordinatescell_boundaries/: Cell segmentation polygons
(Parquet)# Read MERSCOPE data
mase_merscope <- readMERSCOPEMASE(
data_dir = "path/to/vizgen_output",
sample_id = "merscope_sample",
segmentation = "cellpose",
load_transcripts = FALSE
)
# The MASE object contains:
# - experiments: cell × gene matrix
# - spatialPoints(): cell coordinates (x, y, z when present)
# - spatialShapes(): cell segmentation polygons
# - spatialMap(): links assay columns to spatial elementsSelect the reader that matches your platform output directory:
| Platform | Reader |
|---|---|
| 10x Xenium | readXeniumMASE() |
| 10x Visium | readVisiumMASE() |
| 10x Visium HD | readVisiumHDMASE() |
| NanoString CosMx | readCosMxMASE() |
| Vizgen MERSCOPE | readMERSCOPEMASE() |
library(SpatialExperiment)
# Load or create a SpatialExperiment
# spe <- read10xVisium("path/to/visium")
# Convert to MASE
mase_from_spe <- as(spe, "MultiAssaySpatialExperiment")
# The conversion:
# - Places SPE assay(s) into experiments
# - Copies colData to specimen metadata
# - Extracts spatialCoords to points layer
# - Creates appropriate sampleMap and spatialMaplibrary(SpatialFeatureExperiment)
# Load or create a SpatialFeatureExperiment
# sfe <- read10xVisiumSFE("path/to/visium")
# Convert to MASE
mase_from_sfe <- as(sfe, "MultiAssaySpatialExperiment")
# The conversion:
# - Preserves all assays
# - Converts colGeometries to points or shapes
# - Preserves annotGeometries
# - Creates spatialMap linking geometries to assay columnsSee Part 2 below and the Working with MultiAssaySpatialExperiment vignette for coercion details and limitations (e.g., multi-assay objects require manual assembly).
For multi-sample experiments, read each sample separately and combine:
# Read sample 1 (Xenium)
mase1 <- readXeniumMASE(data_dir = "path/to/sample1", sample_id = "P001_xenium")
# Read sample 2 (Xenium)
mase2 <- readXeniumMASE(data_dir = "path/to/sample2", sample_id = "P002_xenium")
# Combine samples
mase_multi <- c(mase1, mase2)
# The combined MASE has:
# - All experiments from both samples
# - Updated colData with sample identifiers
# - Preserved spatial elements from both samples
# - Updated sampleMap and spatialMapA common experimental design combines:
By integrating both, we can: 1. Use Xenium to identify cell types and their spatial organization 2. Use Visium to capture broader transcriptomic programs 3. Transfer cell type labels from Xenium to Visium spots 4. Deconvolve Visium spots using Xenium cell proportions
We will create synthetic data that mimics a real experiment. In practice, you would load your own data from: - Xenium: Cell-by-gene matrix + cell centroids - Visium: Spot-by-gene matrix + spot centroids
# Xenium data: single-cell resolution
# Simulate 500 cells, 50 genes (subset of full panel)
n_cells <- 500
n_genes_xenium <- 50
xenium_counts <- matrix(
rpois(n_cells * n_genes_xenium, lambda = 5),
nrow = n_genes_xenium,
ncol = n_cells,
dimnames = list(
paste0("Gene", 1:n_genes_xenium),
paste0("Cell", 1:n_cells)
)
)
# Cell centroids (tissue coordinates in micrometers)
xenium_coords <- DataFrame(
x = runif(n_cells, 0, 1000),
y = runif(n_cells, 0, 1000),
instance_id = paste0("Cell", 1:n_cells)
)
# Cell type annotations (from clustering or known markers)
cell_types <- sample(
c("Neuron", "Astrocyte", "Microglia", "Oligodendrocyte"),
n_cells,
replace = TRUE,
prob = c(0.5, 0.2, 0.15, 0.15)
)
# Create SingleCellExperiment
xenium_sce <- SingleCellExperiment(
assays = list(counts = xenium_counts),
colData = DataFrame(
cell_type = cell_types,
tech = "xenium"
)
)# Visium data: spot-level resolution
# Simulate 100 spots, 1000 genes (whole transcriptome, downsampled)
n_spots <- 100
n_genes_visium <- 1000
# Create gene names: first 50 overlap with Xenium, rest are Visium-only
visium_gene_names <- c(
paste0("Gene", 1:n_genes_xenium), # Overlapping genes (50)
paste0("GeneV", 1:(n_genes_visium - n_genes_xenium)) # Visium-only (950)
)
visium_counts <- matrix(
rpois(n_spots * n_genes_visium, lambda = 10),
nrow = n_genes_visium,
ncol = n_spots,
dimnames = list(
visium_gene_names,
paste0("Spot", 1:n_spots)
)
)
# Spot centroids (same coordinate system as Xenium)
visium_coords <- DataFrame(
x = runif(n_spots, 0, 1000),
y = runif(n_spots, 0, 1000),
instance_id = paste0("Spot", 1:n_spots)
)
# Create SummarizedExperiment
visium_se <- SummarizedExperiment(
assays = list(counts = visium_counts),
colData = DataFrame(
tech = rep("visium", n_spots),
row.names = paste0("Spot", 1:n_spots)
)
)Combine Xenium and Visium from the same tissue section into one MASE
object. Both assays share a single specimen (colData row);
individual cells and spots are observations linked through
sampleMap and spatialMap.
# Specimen metadata: one row per biological unit (here, one tissue section)
specimen_id <- "section_01"
specimens <- DataFrame(
patient_id = "P001",
tissue = "mouse_brain",
section_id = specimen_id,
row.names = specimen_id
)
n_total <- n_cells + n_spots
# Sample map: link each assay column (cell or spot) to the shared specimen
sample_map <- DataFrame(
assay = factor(
c(rep("xenium", n_cells), rep("visium", n_spots)),
c("xenium", "visium")
),
primary = rep(specimen_id, n_total),
colname = c(paste0("Cell", 1:n_cells), paste0("Spot", 1:n_spots))
)
# Spatial map: link assay columns to spatial coordinates
spatial_map <- DataFrame(
assay = factor(
c(rep("xenium", n_cells), rep("visium", n_spots)),
c("xenium", "visium")
),
colname = c(paste0("Cell", 1:n_cells), paste0("Spot", 1:n_spots)),
element_type = rep("points", n_total),
region = factor(
c(rep("xenium_coords", n_cells), rep("visium_coords", n_spots)),
c("xenium_coords", "visium_coords")
),
instance_id = c(paste0("Cell", 1:n_cells), paste0("Spot", 1:n_spots))
)
# Construct MASE
mase <- MultiAssaySpatialExperiment(
experiments = ExperimentList(
xenium = xenium_sce,
visium = visium_se
),
colData = specimens,
sampleMap = sample_map,
points = PointsLayerList(
xenium_coords = xenium_coords,
visium_coords = visium_coords
),
spatialMap = spatial_map
)
mase
#> A MultiAssaySpatialExperiment object of 2 listed
#> experiments with user-defined names and respective classes.
#> Containing an ExperimentList class object of length 2:
#> [1] xenium: SingleCellExperiment with 50 rows and 500 columns
#> [2] visium: SummarizedExperiment with 1000 rows and 100 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: 2 elements
#> spatialShapes: 0 elements
#> imgData: NULL
#> spatialMap: present# Extract coordinates
xenium_pts <- spatialPoint(mase, "xenium_coords")
visium_pts <- spatialPoint(mase, "visium_coords")
# Plot both assays
par(mfrow = c(1, 2))
# Xenium cells colored by type
plot(xenium_pts$x, xenium_pts$y,
col = as.factor(cell_types),
pch = 16, cex = 0.5,
main = "Xenium: Single cells",
xlab = "x (µm)", ylab = "y (µm)")
legend("topright", legend = unique(cell_types),
col = 1:length(unique(cell_types)), pch = 16, cex = 0.7)
# Visium spots
plot(visium_pts$x, visium_pts$y,
pch = 21, bg = "lightblue", cex = 2,
main = "Visium: Spots (55 µm)",
xlab = "x (µm)", ylab = "y (µm)")Check which genes are shared between assays:
xenium_genes <- rownames(experiments(mase)$xenium)
visium_genes <- rownames(experiments(mase)$visium)
shared_genes <- intersect(xenium_genes, visium_genes)
cat("Shared genes:", length(shared_genes), "\n")
#> Shared genes: 50
cat("Xenium-only genes:", length(setdiff(xenium_genes, visium_genes)), "\n")
#> Xenium-only genes: 0
cat("Visium-only genes:", length(setdiff(visium_genes, xenium_genes)), "\n")
#> Visium-only genes: 950Use Xenium cell types to annotate Visium spots. For each spot, find
which cells fall within it and calculate cell type proportions. The loop
below is explicit for teaching; when spot polygons are stored in
spatialShapes(), you can also use
annotateWithRegions() (see Working with
MultiAssaySpatialExperiment).
# Create circular regions for Visium spots (55 µm diameter = 27.5 µm radius)
visium_circles <- st_buffer(
st_sf(visium_pts,
geometry = st_as_sfc(paste0("POINT(", visium_pts$x, " ", visium_pts$y, ")"))),
dist = 27.5
)
visium_shapes <- DataFrame(
geometry = st_geometry(visium_circles),
instance_id = visium_pts$instance_id
)
# Add shapes to MASE
spatialShapes(mase) <- ShapesLayerList(visium_spots = visium_shapes)
# For each Visium spot, find which Xenium cells are inside
xenium_sf <- st_sf(xenium_pts,
geometry = st_as_sfc(paste0("POINT(", xenium_pts$x, " ", xenium_pts$y, ")")))
visium_sf <- st_sf(
instance_id = visium_shapes$instance_id,
geometry = visium_shapes$geometry
)
# Spatial intersection
intersections <- st_intersects(visium_sf, xenium_sf)
# Calculate cell type proportions per spot
spot_cell_types <- lapply(seq_along(intersections), function(i) {
cell_indices <- intersections[[i]]
if (length(cell_indices) == 0) {
return(data.frame(
spot = visium_pts$instance_id[i],
Neuron = 0, Astrocyte = 0, Microglia = 0, Oligodendrocyte = 0,
n_cells = 0
))
}
types <- cell_types[cell_indices]
props <- base::table(types) / length(types)
data.frame(
spot = visium_pts$instance_id[i],
Neuron = as.numeric(props["Neuron"]),
Astrocyte = as.numeric(props["Astrocyte"]),
Microglia = as.numeric(props["Microglia"]),
Oligodendrocyte = as.numeric(props["Oligodendrocyte"]),
n_cells = length(cell_indices),
stringsAsFactors = FALSE
)
})
spot_annotations <- do.call(rbind, spot_cell_types)
spot_annotations[is.na(spot_annotations)] <- 0
head(spot_annotations)
#> spot Neuron Astrocyte Microglia Oligodendrocyte n_cells
#> 1 Spot1 1.0 0 0.0 0 1
#> 2 Spot2 0.0 0 0.0 0 0
#> 3 Spot3 0.0 1 0.0 0 1
#> 4 Spot4 0.0 0 0.0 0 0
#> 5 Spot5 0.0 0 0.0 0 0
#> 6 Spot6 0.5 0 0.5 0 2# Plot cell type proportions per spot
par(mfrow = c(2, 2))
for (cell_type in c("Neuron", "Astrocyte", "Microglia", "Oligodendrocyte")) {
plot(visium_pts$x, visium_pts$y,
pch = 21, cex = 3,
bg = rgb(spot_annotations[[cell_type]], 0, 0, alpha = 0.7),
main = paste(cell_type, "proportion"),
xlab = "x (µm)", ylab = "y (µm)")
}Aggregate Xenium gene expression by tissue regions (e.g., cortical
layers). First, define regions, then use
aggregateByRegion().
# Define two regions: "cortex" (y > 500) and "subcortex" (y <= 500)
# In practice, these would be anatomical annotations
region_polygons <- list(
cortex = st_polygon(list(cbind(
c(0, 1000, 1000, 0, 0),
c(500, 500, 1000, 1000, 500)
))),
subcortex = st_polygon(list(cbind(
c(0, 1000, 1000, 0, 0),
c(0, 0, 500, 500, 0)
)))
)
regions_sf <- st_sf(
region_id = c("cortex", "subcortex"),
geometry = st_sfc(region_polygons)
)
# Convert to DataFrame for MASE
regions_df <- DataFrame(
geometry = st_geometry(regions_sf),
instance_id = c("cortex", "subcortex")
)
# Add to MASE
spatialShapes(mase) <- ShapesLayerList(
visium_spots = visium_shapes,
regions = regions_df
)# Annotate Xenium centroids with anatomical regions, then aggregate expression.
# Run on the full MASE so the regions layer is retained (subsetByAssay drops
# shapes not referenced in spatialMap).
mase <- annotateWithRegions(mase,
points = "xenium_coords", shapes = "regions")
xenium_by_region <- aggregateByRegion(mase, by = "regions",
assays = "xenium", FUN = "sum")
xenium_by_region[["xenium"]][1:5, ]
#> cortex subcortex
#> Gene1 1256 1285
#> Gene2 1215 1220
#> Gene3 1311 1248
#> Gene4 1194 1276
#> Gene5 1171 1286Compare gene expression patterns between Xenium and Visium for shared genes.
# Extract counts for shared genes
xenium_shared <- assay(experiments(mase)$xenium, "counts")[shared_genes, ]
visium_shared <- assay(experiments(mase)$visium, "counts")[shared_genes, ]
# Calculate mean expression per gene
xenium_means <- rowMeans(xenium_shared)
visium_means <- rowMeans(visium_shared)
# Plot correlation
plot(log1p(xenium_means), log1p(visium_means),
pch = 16, col = rgb(0, 0, 0, 0.5),
xlab = "log1p(mean Xenium counts)",
ylab = "log1p(mean Visium counts)",
main = "Gene expression correlation")
abline(0, 1, col = "red", lty = 2)
# Correlation
cor_val <- cor(xenium_means, visium_means, method = "spearman")
text(0.5, max(log1p(visium_means)) * 0.9,
paste("Spearman rho =", round(cor_val, 3)),
pos = 4)Key findings from this analysis:
Spatial resolution: Xenium provides single-cell resolution, while Visium captures tissue-level patterns with multiple cells per spot.
Gene overlap: Shared genes allow direct comparison and validation between technologies.
Cell type deconvolution: Xenium cell type information can be transferred to Visium spots, revealing cellular composition.
Regional patterns: Both assays can be aggregated by anatomical regions for comparative analysis.
Complementary data: Xenium’s high resolution complements Visium’s broad transcriptomic coverage.
When integrating data from different spatial platforms, coordinate systems often differ. This workflow demonstrates how to align Xenium and Visium data using spatial transformations.
Xenium and Visium may use different coordinate systems:
To overlay them, you need to:
Visium spots are arranged in a hexagonal grid. Each spot is ~55 µm diameter, with center-to-center spacing of ~100 µm.
# Visium spot coordinates (grid indices from Space Ranger)
visium_grid <- DataFrame(
row_idx = c(0, 0, 1, 1, 2, 2),
col_idx = c(0, 1, 0, 1, 0, 1),
instance_id = paste0("Spot", 1:6)
)
# Convert to microns (approximate)
# Spot spacing: ~100 µm horizontally, ~87 µm vertically (hexagonal)
visium_microns <- DataFrame(
x = visium_grid$col_idx * 100, # Column → x
y = visium_grid$row_idx * 87, # Row → y (hexagonal offset)
instance_id = visium_grid$instance_id
)
visium_microns
#> DataFrame with 6 rows and 3 columns
#> x y instance_id
#> <numeric> <numeric> <character>
#> 1 0 0 Spot1
#> 2 100 0 Spot2
#> 3 0 87 Spot3
#> 4 100 87 Spot4
#> 5 0 174 Spot5
#> 6 100 174 Spot6For precise alignment, use affine transformations (translation, rotation, scaling):
# Define affine matrix: translate by (50, 30), scale by 1.2, no rotation
# Format: 2x3 matrix [a b c; d e f] where:
# x' = a*x + b*y + c
# y' = d*x + e*y + f
affine_matrix <- matrix(c(
1.2, 0.0, 50, # x' = 1.2*x + 0*y + 50 (scale + translate)
0.0, 1.2, 30 # y' = 0*x + 1.2*y + 30 (scale + translate)
), nrow = 2, byrow = TRUE)
# Apply transformation
apply_affine <- function(pts, mat) {
x_new <- mat[1, 1] * pts$x + mat[1, 2] * pts$y + mat[1, 3]
y_new <- mat[2, 1] * pts$x + mat[2, 2] * pts$y + mat[2, 3]
DataFrame(x = x_new, y = y_new, instance_id = pts$instance_id)
}
# Simulate Xenium coordinates
xenium_original <- DataFrame(
x = c(100, 150, 200, 250, 300),
y = c(100, 150, 200, 150, 100),
instance_id = paste0("Cell", 1:5)
)
# Transform Xenium to align with Visium
xenium_aligned <- apply_affine(xenium_original, affine_matrix)
xenium_aligned
#> DataFrame with 5 rows and 3 columns
#> x y instance_id
#> <numeric> <numeric> <character>
#> 1 170 150 Cell1
#> 2 230 210 Cell2
#> 3 290 270 Cell3
#> 4 350 210 Cell4
#> 5 410 150 Cell5When you have known landmarks (tissue features visible in both modalities), use them to compute an optimal transformation:
# Example: three landmarks in both Xenium and Visium
# In practice, these would be manually identified features (blood vessels, etc.)
xenium_landmarks <- matrix(c(
100, 100,
200, 150,
300, 200
), ncol = 2, byrow = TRUE)
visium_landmarks <- matrix(c(
50, 50,
150, 100,
250, 150
), ncol = 2, byrow = TRUE)
cat("Landmark-based alignment workflow:\n")
#> Landmark-based alignment workflow:
cat("1. Identify 3+ corresponding landmarks in both modalities\n")
#> 1. Identify 3+ corresponding landmarks in both modalities
cat("2. Compute affine/rigid transformation using:\n")
#> 2. Compute affine/rigid transformation using:
cat(" - Procrustes analysis (for rigid transformations)\n")
#> - Procrustes analysis (for rigid transformations)
cat(" - Least squares fitting (for affine transformations)\n")
#> - Least squares fitting (for affine transformations)
cat(" - Or use packages like RNiftyReg, imager, or EBImage\n")
#> - Or use packages like RNiftyReg, imager, or EBImage
cat("3. Apply transformation to all points\n")
#> 3. Apply transformation to all points
cat("4. Validate alignment visually\n")
#> 4. Validate alignment visually
cat("5. Store transformed coordinates in MASE\n")
#> 5. Store transformed coordinates in MASEStore both original and aligned coordinates as separate layers for flexibility:
# Create MASE with both coordinate systems
# Original Xenium coordinates
pts_original <- PointsLayerList(xenium_original = xenium_original)
# Aligned Xenium coordinates
pts_aligned <- PointsLayerList(xenium_aligned = xenium_aligned)
# Visium coordinates
pts_visium <- PointsLayerList(visium_coords = visium_microns)
# You can store all in the same MASE object
# This allows:
# - Comparing original vs aligned coordinates
# - Refining alignment iteratively
# - Switching between coordinate systems as neededCheck alignment quality visually:
par(mfrow = c(1, 2))
# Before alignment
plot(xenium_original$x, xenium_original$y,
pch = 16, col = "red", cex = 1.5,
main = "Before alignment",
xlab = "x (µm)", ylab = "y (µm)",
xlim = c(0, 400), ylim = c(0, 350))
points(visium_microns$x, visium_microns$y,
pch = 21, bg = "blue", cex = 3)
legend("topright", legend = c("Xenium (cells)", "Visium (spots)"),
col = c("red", "blue"), pch = c(16, 21), pt.cex = c(1.5, 2))
# After alignment
plot(xenium_aligned$x, xenium_aligned$y,
pch = 16, col = "red", cex = 1.5,
main = "After alignment",
xlab = "x (µm)", ylab = "y (µm)",
xlim = c(0, 400), ylim = c(0, 350))
points(visium_microns$x, visium_microns$y,
pch = 21, bg = "blue", cex = 3)
legend("topright", legend = c("Xenium (aligned)", "Visium (spots)"),
col = c("red", "blue"), pch = c(16, 21), pt.cex = c(1.5, 2))MASE excels at managing multiple tissue sections or patients. Here’s a brief example:
# Suppose we have data from 3 patients
# Each with Xenium + Visium
# Specimen metadata
specimens_multi <- DataFrame(
patient_id = c(rep("P001", 150), rep("P002", 120), rep("P003", 180)),
tissue = "mouse_brain",
condition = c(rep("control", 150), rep("treated", 120), rep("treated", 180)),
row.names = paste0("Sample", 1:450)
)
# Build sampleMap linking all assays to specimens
# ... (similar to single-sample example)
# With MASE, you can:
# - Subset by patient: mase[, colData(mase)$patient_id == "P001", ]
# - Subset by condition: mase[, colData(mase)$condition == "control", ]
# - Subset one assay: subsetByAssay(mase, "xenium")
# - Analyze spatial patterns per patient
# - Compare conditions across patientsA typical analysis workflow using MASE generics:
# 1. Load data with the appropriate reader
mase <- readXeniumMASE(data_dir = "path/to/xenium_output", sample_id = "sample1")
# 2. Quality control: subset specimens
mase_qc <- mase[, colData(mase)$qc_pass, ]
# 3. Spatial filtering: focus on region of interest
library(sf)
roi <- st_polygon(list(matrix(c(100, 200, 500, 200, 500, 600, 100, 600, 100, 200),
ncol = 2, byrow = TRUE)))
mase_roi <- subsetByPolygon(mase_qc, roi)
# 4. Annotate points with cell boundaries
mase_annotated <- annotateWithRegions(mase_roi,
points = "centroids", shapes = "cells")
# 5. Aggregate expression per cell for downstream analysis
cell_expr <- aggregateByRegion(mase_annotated, by = "cells", FUN = "sum")
# 6. Differential expression, clustering, etc. on cell_expr matricesMultiAssaySpatialExperiment provides a unified framework
for:
sampleMap and spatialMapThe relational database design ensures: - Referential integrity: All links stay consistent during subsetting - Flexibility: Add or remove assays/spatial elements as needed - Interoperability: Convert to/from other Bioconductor spatial classes
For more examples, see:
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] ggplot2_4.0.3 sf_1.1-1
#> [3] SingleCellExperiment_1.35.2 MultiAssaySpatialExperiment_0.99.2
#> [5] MultiAssayExperiment_1.39.0 SummarizedExperiment_1.43.0
#> [7] Biobase_2.73.1 GenomicRanges_1.65.1
#> [9] Seqinfo_1.3.0 IRanges_2.47.2
#> [11] S4Vectors_0.51.5 BiocGenerics_0.59.10
#> [13] generics_0.1.4 MatrixGenerics_1.25.0
#> [15] matrixStats_1.5.0 BiocStyle_2.41.0
#>
#> loaded via a namespace (and not attached):
#> [1] gtable_0.3.6 rjson_0.2.23 xfun_0.60
#> [4] bslib_0.11.0 lattice_0.22-9 vctrs_0.7.3
#> [7] tools_4.6.1 tibble_3.3.1 proxy_0.4-29
#> [10] pkgconfig_2.0.3 BiocBaseUtils_1.15.1 Matrix_1.7-5
#> [13] KernSmooth_2.23-26 RColorBrewer_1.1-3 S7_0.2.2
#> [16] lifecycle_1.0.5 compiler_4.6.1 farver_2.1.2
#> [19] codetools_0.2-20 htmltools_0.5.9 sys_3.4.3
#> [22] buildtools_1.0.0 class_7.3-23 sass_0.4.10
#> [25] yaml_2.3.12 pillar_1.11.1 jquerylib_0.1.4
#> [28] classInt_0.4-11 DelayedArray_0.39.3 cachem_1.1.0
#> [31] magick_2.9.1 abind_1.4-8 tidyselect_1.2.1
#> [34] digest_0.6.39 dplyr_1.2.1 maketools_1.3.2
#> [37] fastmap_1.2.0 grid_4.6.1 cli_3.6.6
#> [40] SparseArray_1.13.2 magrittr_2.0.5 S4Arrays_1.13.0
#> [43] e1071_1.7-17 withr_3.0.3 scales_1.4.0
#> [46] rmarkdown_2.31 XVector_0.53.0 otel_0.2.0
#> [49] SpatialExperiment_1.23.0 evaluate_1.0.5 knitr_1.51
#> [52] rlang_1.3.0 Rcpp_1.1.2 glue_1.8.1
#> [55] DBI_1.3.0 BiocManager_1.30.27 jsonlite_2.0.0
#> [58] R6_2.6.1 units_1.0-1