Skip to content

escape.matrix(normalize = TRUE) fails on SpatialExperiment (and possibly all SCE) input — assay() called on NULL during normalization #180

Description

@ncborcherding

Summary

escape.matrix() errors during the normalization step when handed a SpatialExperiment object. Enrichment itself completes (all 167 chunks process, expressed-gene counts are computed) and the failure occurs only once normalization begins.

Reported by Cathal King, who has generously offered to share the SPE object so we can reproduce and test against real spatial data. Acknowledgment to be added to DESCRIPTION and package docs (see Documentation section).

Reproducing report

trial <- escape.matrix(spe,
                       method = "AUCell",
                       normalize = TRUE,
                       gene.sets = .gs,
                       min.size = NULL)

Output:

escape.matrix(): processing 167 chunk(s)...
Computing expressed-gene counts per cell...
Normalizing enrichment scores...
Error in h(simpleError(msg, call)) : 
  error in evaluating the argument 'x' in selecting a method for function 't': 
  unable to find an inherited method for function 'assay' for signature 'x = "NULL", i = "missing"'

spe is a SpatialExperiment (~167k spots given the chunk count, likely multi-section).

What the error tells us

The failing expression is t(assay(<NULL>)) — i.e. something upstream returned NULL and was passed straight into assay(). Two observations narrow this down:

  1. Chunked enrichment succeeded, so .cntEval() handled the SPE fine — counts extraction is not the problem.
  2. Computing expressed-gene counts per cell... printed, so the per-cell feature counts were also retrieved successfully from the SPE.
  3. The failure lands between Normalizing enrichment scores... and any output — i.e. while retrieving the enrichment matrix, not while computing anything.

Working hypothesis

When normalize = TRUE, escape.matrix() passes the freshly computed scores down to performNormalization(). If the normalization path branches on the class of input.data rather than on whether enrichment.data was supplied, then for any SummarizedExperiment-derived input it will ignore the in-memory scores and try to pull them back off the object via .pull.Enrich() / altExps(sc)[[assay]]. Since escape.matrix() never writes an assay to the object, that name lookup returns NULL (list [[ on a missing name returns NULL rather than erroring), and t(assay(NULL)) produces exactly the observed signature error.

Important corollary: if this is right, the bug is not spatial-specific — it should reproduce with a plain SingleCellExperiment too, and SpatialExperiment is simply the object class that surfaced it. Confirming or refuting that is step 1, because it changes the scope of the fix.

Secondary possibilities to rule out:

  • SPE-specific behavior of altExp() / altExps() (SPE extends SCE, so is() checks pass, but the int_colData machinery differs).
  • An assay-name assumption ("counts" vs "logcounts") — read10xVisium() objects ship with counts only. Less likely given the counts step succeeded, but cheap to check.

Tracking checklist

1. Diagnosis

  • Confirm whether a plain SingleCellExperiment reproduces the error — determines if this is an SPE bug or a general SE-input bug.
  • Test as(spe, "SingleCellExperiment") to isolate SPE-specific behavior.
  • Confirm normalize = FALSE completes cleanly on the same object.
  • Confirm the plain-matrix path (escape.matrix(as.matrix(counts(spe)), normalize = TRUE, ...)) works — establishes the workaround and confirms the branch.
  • debug(escape:::performNormalization) on Cathal's object; capture traceback() and identify the exact NULL return.

Minimal SPE for local testing without the shared data:

library(SpatialExperiment); library(SingleCellExperiment)
sce <- Seurat::as.SingleCellExperiment(SeuratObject::pbmc_small)
spe <- SpatialExperiment(
  assays        = list(counts = counts(sce), logcounts = logcounts(sce)),
  colData       = colData(sce),
  spatialCoords = matrix(runif(2 * ncol(sce)), ncol = 2,
                         dimnames = list(colnames(sce), c("x", "y")))
)
GS <- list(Bcells = c("MS4A1","CD79B","CD79A"), Tcells = c("CD3E","CD3D","CD3G"))
escape.matrix(spe, method = "AUCell", normalize = TRUE, gene.sets = GS, min.size = NULL)

2. Fix

  • In performNormalization(), give precedence to a non-NULL enrichment.data regardless of input.data class — never re-pull scores that were handed in.
  • Harden the enrichment accessor: validate the requested name against assayNames() / altExpNames() (Seurat: Assays()) and stop() with an actionable message naming what was requested and what is available, rather than propagating NULL.
  • Add an explicit guard so NULL can never reach assay().
  • Verify runEscape() on an SPE round-trips correctly: spatialCoords(), imgData(), and sample_id preserved, and colnames alignment maintained when the result is attached as an altExp.
  • Check the multi-sample case (several sample_ids in one SPE) — relevant to normalization groups and to Cathal's object specifically.

3. Tests

  • Unit test: escape.matrix(spe, normalize = TRUE) returns a matrix of the expected dimensions.
  • Unit test: same for plain SingleCellExperiment (regression guard for the general case).
  • Unit test: performNormalization() with an explicit enrichment.data and an SE input.data uses the supplied data.
  • Unit test: informative error when a nonexistent assay name is requested.
  • Add SpatialExperiment to Suggests and skip these tests if unavailable.

4. Documentation

  • Add Cathal King to DESCRIPTION Authors@R as ctb, with an acknowledgment for contributing the spatial test data.
  • NEWS.md entry for the fix, crediting the report.
  • State SpatialExperiment support explicitly in escape.matrix() / runEscape() / performNormalization() man pages.
  • Vignette section on spatial input (SPE construction, what runEscape() attaches, and the spatial-coordinate caveat).

Acceptance criteria

escape.matrix() and runEscape() complete with normalize = TRUE on a SpatialExperiment, results are numerically identical to the equivalent counts-matrix call, spatial metadata survives runEscape(), and any bad assay name produces a readable error instead of an S4 dispatch failure.

Interim workaround for users

scores <- escape.matrix(as.matrix(counts(spe)),
                        method    = "AUCell",
                        normalize = TRUE,
                        gene.sets = .gs,
                        min.size  = NULL)

Bypasses the SE dispatch path entirely; scores can be attached back to spe manually.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions