Author: Stijn Hawinkel

# Vignette of the smoppix package

#### Stijn Hawinkel

# 1 Introduction

This vignette demonstrates the use of the smoppix package: Single MOlecule sPatial omics data analysed through the Probabilistic IndeX , described in “Unified nonparametric analysis of single-molecule spatial omics data using probabilistic indices” by Hawinkel et al. (2025). It unifies methods for statistical inference on single-molecule spatial transcriptomics datasets with replication in a single framework using probabilistic indices (PIs). Tests provided include univariate testing for vicinity to fixed objects such as cell walls or nuclei, univariate aggregation tests as well as bivariate tests for co- or antilocalization. Two different null hypothesis can be tested against: complete spatial randomness (CSR, also known as homogeneous Poisson process), and the background distribution of molecules of all other genes. In addition, differences between groups in terms of any of these univariate and bivariate PIs can be tested for, while accounting for complex design structures using mixed models. smoppix is scalable to large numbers of molecules and genes thanks to an exact permutation null distribution in combination with a custom C++ implementation. An adequate variance weighting scheme harnesses the high-dimensionality of the data to account for heteroscedasticity. The smoppix package is also applicable to other types of single-molecule omics data such as spatial proteomics or metabolomics, or to cell type localization data.

# 2 Installation instructions

The package can be installed from Bioconductor as follows:

    library(Biocmanager)
    install("smoppix")

Alternatively, the latest version can be installed from github as:

    library(devtools)
    install_github("sthawinke/smoppix")

Once installed, we can load the package:

    library(smoppix)

# 3 Multithreading

The smoppix package can be computationally intensive, especially since the number transcript pairs grows quadratically with the number of transcripts. For this reason we provide multithreading through the BiocParallel package.

    library(BiocParallel)

All the user needs to do, is choose the number of cores:

    nCores <- 2 # The number of CPUs

and prepare the parallel backend. The code is different for windows or unix-based systems, but is taken care of by the following code snippet:

    param <- if (.Platform$OS.type == "unix") {
        # On unix-based systems (linux and macOS), use MulticoreParam
        MulticoreParam(nCores)
    } else {
        # On windows, build vignette without multithreading
        SerialParam()
    }
    register(param)

On windows, once the package has been built, for multithreading use

    register(SnowParam(workers = nCores, type = "SOCK")) #or doParallel

Wherever applicable, the registered backend will be used throughout the analysis. For serial calculations, for instance if memory is limiting, choose

    register(SerialParam())

# 4 Yang data: lycophyte roots

As example dataset we use the data from a publication by Yang et al. (2023) on Selaginella moellendorffii . Only a subset of the data, consisting of sections 1-5 of roots 1-3 is included for computational reasons. The data is loaded as follows.

    data(Yang)

A quick glimpse …

    head(Yang)

    ##      x    y    gene  day  root  section
    ## 1  357 1265 SmEIL1e day0 Root2 section3
    ## 2  425 1281 SmEIL1e day0 Root2 section3
    ## 3  381 1475 SmEIL1e day0 Root2 section3
    ## 4  317 1919 SmEIL1e day0 Root2 section3
    ## 5 1617 1801 SmEIL1e day0 Root2 section3
    ## 6  713  201 SmCASP1 day0 Root2 section3

It is a dataframe containing molecule locations (x- and y-variables), gene identity and covariates: day, root and section. At each day, 3 roots are included, with up to 5 sections for each root. The analysis will need to account for this nested design. The first step in the analysis is to build a hyperframe as provided in the spatstat package, which consists of all different point patterns and their covariates

    hypYang <- buildHyperFrame(Yang,
        coordVars = c("x", "y"),
        imageVars = c("day", "root", "section")
    )

    ## Found 29 unique images

As printed by the code, 29 images or point patterns were found. We take a closer look at the hyperframe

    head(hypYang)

    ## Hyperframe:
    ##                       ppp               image  tabObs  day  root  section
    ## day0_Root1_section1 (ppp) day0_Root1_section1 (table) day0 Root1 section1
    ## day0_Root1_section2 (ppp) day0_Root1_section2 (table) day0 Root1 section2
    ## day0_Root1_section3 (ppp) day0_Root1_section3 (table) day0 Root1 section3
    ## day0_Root1_section4 (ppp) day0_Root1_section4 (table) day0 Root1 section4
    ## day0_Root1_section5 (ppp) day0_Root1_section5 (table) day0 Root1 section5
    ## day0_Root2_section1 (ppp) day0_Root2_section1 (table) day0 Root2 section1

We see the following elements:

- ppp: the spatstat point patterns of class ppp
- image: The image id uniquely defining each point pattern. In our case, it is a concatenation of the day, root and section variables.
- tabObs: tabulation of the frequency of each transcript in each point pattern
- day, root and section: the covariates

Zooming in on the first point pattern:

    hypYang$ppp[[1]]

    ## Marked planar point pattern: 2279 points
    ## marks are of storage type  'character'
    ## window: rectangle = [2451, 6275] x [138, 1929] units

As a final confirmation of successfully reading in the data, we can make an exploratory plot:

    plotExplore(hypYang, Cex = 2)

A couple of most expressed genes are shown as coloured dots as shown in the legend, all other genes’ transcripts are shown in grey.

## 4.1 Univariate tests

### 4.1.1 Tests for univariate aggregation or regularity: nearest neighbour distances

    numFeats <- 10 #Limit number of genes in the analysis for computational reasons

The first round of tests concerns the aggregation or regularity of individual transcripts, for which we look at the distribution of nearest neighbour distances (pi = ‘nn’). Since the shape and size differ strongly between the roots, we choose the background of all other transcripts as null distribution, i.e. we look for transcripts aggregated with respect to all other transcripts. As this is computationally most efficient, we also fit the PI for the bivariate analysis in this step (‘nnPair’, see below). For computation reasons, we limit the analysis to 10 genes.

    nnObj <- estPis(hypYang,
        pis = c("nn", "nnPair"), null = "background", verbose = FALSE,
        features = c("SmVND2", "SmPINR", getFeatures(hypYang)[seq_len(numFeats)])
    )

A data-driven weighting function is now fitted to the estimates of all transcripts to share information on variances across features. This requires information on the design too: all variables that will be included in the final modelling should be provided to ‘designVars’. This excludes the lowest level of the design variable, ‘section’ in this case. The variance is estimated over the different sections of the same day and root. If there are no design variables, i.e. all point patterns are replicates of the exact same condition, no variable needs to be provided.

    nnObj <- addWeightFunction(nnObj, designVars = c("day", "root"))

The fitted trend can be plotted:

    plotWf(nnObj, pi = "nn")

As expected the weight increases with the number of observations; quickly at first but latter plateauing. This relation between number of observations and variance will provide weights for a linear (mixed) model to be fitted per feature. A built-in function prepares the dataframe for a specified gene (here SmAHK4e):

    dfUniNN <- buildDataFrame(nnObj, gene = "SmAHK4e", pi = "nn")

This dataframe can then be used for mixed modelling, either in R or in an other software package. We present an example analysis below using the lmerTest package, which enhances the lmer package by providing approximate p-values. We fit the following linear mixed model (LMM) for once manually, before automating it. The day variable enters the model as a fixed effect, the root as a random effect (a random intercept). We use sum coding for the day variable, meaning that two dummies are created, one for “day0” and one for “day3”, so without a reference level. Instead the corresponding parameters are constrained to sum to zero, i.e. to be each other’s opposite in this case. This maintains the interpretability of the intercept as the baseline pi, from which the different days can depart, and allows meaningful inference on the intercept.

    library(lmerTest, quietly = TRUE)

    ##
    ## Attaching package: 'lmerTest'

    ## The following object is masked from 'package:lme4':
    ##
    ##     lmer

    ## The following object is masked from 'package:stats':
    ##
    ##     step

    #Make sure to nest root within day, without including a random effect for day
    dfUniNN$dayRoot = paste0(dfUniNN$day, dfUniNN$root)
    lmeMod <- lmerTest::lmer(pi - 0.5 ~ day + (1 |dayRoot),
        data = dfUniNN, na.action = na.omit,
        weights = weight, contrasts = list("day" = "contr.sum")
    )
    summary(lmeMod)

    ## Linear mixed model fit by REML. t-tests use Satterthwaite's method [
    ## lmerModLmerTest]
    ## Formula: pi - 0.5 ~ day + (1 | dayRoot)
    ##    Data: dfUniNN
    ## Weights: weight
    ##
    ## REML criterion at convergence: -49
    ##
    ## Scaled residuals:
    ##     Min      1Q  Median      3Q     Max
    ## -2.2001 -0.4075  0.1610  0.8333  1.9271
    ##
    ## Random effects:
    ##  Groups   Name        Variance  Std.Dev.
    ##  dayRoot  (Intercept) 0.0028886 0.05375
    ##  Residual             0.0001368 0.01169
    ## Number of obs: 28, groups:  dayRoot, 6
    ##
    ## Fixed effects:
    ##             Estimate Std. Error       df t value Pr(>|t|)
    ## (Intercept) -0.10222    0.02515  3.23188  -4.065   0.0233 *
    ## day1         0.01753    0.02515  3.23188   0.697   0.5325
    ## ---
    ## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    ##
    ## Correlation of Fixed Effects:
    ##      (Intr)
    ## day1 0.044

Alternatively, a wrapper function is available that fits such LMMs for every transcript. A simple random intercept is assumed for every random effect, for more complex designs it is better to supply your own formula through the Formula argument.

Various errors may occur while fitting, e.g. insufficient non-missing observations. The function finishes silently but returns the error messages in the output. The results can now be viewed as follows using the getResults function, with the genes sorted by p-value.

    head(getResults(allModsNN, "nn", "Intercept"), 4)

    ##         Estimate          SE         pVal         pAdj
    ## SmPINR 0.3590816 0.008063135 3.016997e-16 3.620396e-15
    ## SmAPL  0.3803474 0.011384813 7.452704e-11 4.471623e-10
    ## SmARF2 0.3562729 0.020373960 5.816213e-07 2.326485e-06
    ## SmVND2 0.1352421 0.008119909 1.338453e-06 4.015358e-06

    head(getResults(allModsNN, "nn", "day"), 4)

    ##             dayday0     dayday3       pVal      pAdj
    ## SmARF2   0.04645412 -0.04645412 0.03315517 0.3978621
    ## SmAPL    0.02006893 -0.02006893 0.08969013 0.4272782
    ## SmPINR  -0.01345151  0.01345151 0.10681955 0.4272782
    ## SmAHK4c  0.04930028 -0.04930028 0.28719100 0.6418468

We see many genes with significant aggregation (intercept estimate smaller than 0.5), but no significant differences between day 0 and 3.

## 4.2 Bivariate tests

### 4.2.1 Tests for colocalization or antilocalization: interfeature nearest neighbour distances

Tests for co/antilocalization are requested by supplying “nnPair” as PI. By default, all combinations of features into pairs are fitted. Again we plot the weighting function, which is now a bivariate spline as a function of the minority and majority gene, i.e. the gene in the gene pair with least respectively most events:

    plotWf(nnObj, pi = "nnPair")

The least expressed gene or minority gene has the strongest influence on the weight. The dataframe is built in a similar way as before, separating the gene pair through a double hyphen:

    dfBiNN <- buildDataFrame(nnObj, gene = "SmVND2--SmPINR", pi = "nnPair")

Finally for the mixed model:

    #Make sure to nest root within day, without including a random effect for day
    dfBiNN$dayRoot = paste0(dfBiNN$day, dfBiNN$root)
    lmeModNN <- lmerTest::lmer(pi - 0.5 ~ day + (1 | dayRoot),
        data = dfBiNN, na.action = na.omit,
        weights = weight, contrasts = list("day" = "contr.sum")
    )
    summary(lmeModNN)

    ## Linear mixed model fit by REML. t-tests use Satterthwaite's method [
    ## lmerModLmerTest]
    ## Formula: pi - 0.5 ~ day + (1 | dayRoot)
    ##    Data: dfBiNN
    ## Weights: weight
    ##
    ## REML criterion at convergence: -34.4
    ##
    ## Scaled residuals:
    ##      Min       1Q   Median       3Q      Max
    ## -1.66475 -0.68749 -0.00616  0.50652  1.42231
    ##
    ## Random effects:
    ##  Groups   Name        Variance  Std.Dev.
    ##  dayRoot  (Intercept) 0.0063618 0.07976
    ##  Residual             0.0003087 0.01757
    ## Number of obs: 27, groups:  dayRoot, 6
    ##
    ## Fixed effects:
    ##             Estimate Std. Error      df t value Pr(>|t|)
    ## (Intercept)  0.31932    0.03704 3.86398   8.621  0.00116 **
    ## day1         0.02803    0.03704 3.86398   0.757  0.49264
    ## ---
    ## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    ##
    ## Correlation of Fixed Effects:
    ##      (Intr)
    ## day1 -0.015

We find a significant positive intercept 0.319, indicating strong antilocalization (PI = 0.819). We investigate this gene pair visually:

    plotExplore(hypYang, features = "SmVND2--SmPINR")

Shows clear antilocalization indeed. And similarly, this process can be wrapped using _fitLMMs()_as before:

    head(getResults(allModsNN, "nnPair", "Intercept"))

    ##                   Estimate         SE         pVal        pAdj
    ## SmAFB3--SmAHP6b  0.4085719 0.02029633 0.0001463627 0.009659935
    ## SmPINR--SmVND2   0.8193170 0.03703954 0.0011600521 0.032096693
    ## SmAFB3--SmAHK4e  0.4229910 0.02142665 0.0014589406 0.032096693
    ## SmAFB2--SmVND2   0.5732943 0.02315283 0.0040415758 0.066686001
    ## SmAHK4e--SmAHP6b 0.4428683 0.01398762 0.0135652153 0.168490010
    ## SmAHK4c--SmAHK4e 0.5663627 0.02549390 0.0153172737 0.168490010

The results can also be written to a spreadsheet for consultation outside R:

    writeToXlsx(allModsNN, file = "myfile.xlsx")

# 5 Eng2019: Mouse brain cells

A second dataset was published in a paper by Eng et al. (2019) on mouse fibroblast cells, which will serve to illustrate methods given knowledge of cell boundaries. The dataset was subset to the eight most expressed genes for memory reasons. The hyperframe is built as before:

    data(Eng)
    hypEng <- buildHyperFrame(Eng, coordVars = c("x", "y"),
        imageVars = c("fov", "experiment"))

    ## Found 17 unique images

Plotting an overview

    plotExplore(hypEng)

Next, the cell boundaries are added, as a list with names corresponding to the rownames of the hyperframe. Different formats are allowed to provide windows in case the correct packages are installed, see ?addCell. Based on these cells, every event is assigned to a cell. Although available for this dataset, also nuclei can be added analogously via the addNuclei functions.

    # Add cell identifiers
    hypEng <- addCell(hypEng, EngRois, verbose = FALSE)

Now plot the complete hyperframe with cell boundaries:

    plotExplore(hypEng)

## 5.1 Vicinity to cell edge or centroid, and other within-cell localization patterns

First, we test for vicinity of the RNA molecules to cell boundaries or centroids, and for localization patterns within the cell. For this we pass different PI arguments to the estPis() function. As null hypothesis, we choose complete spatial randomness (CSR) within the cell, which seems most natural, although the “background” option is also available. The suffix “Cell” indicates that aggregation and colocalization is tested within the same cell only, so not across the tissue.

    engPis <- estPis(hypEng, pis = c("edge", "centroid", "nnCell", "nnPairCell"),
        null = "CSR", verbose = FALSE)

As before, the weight function needs to be added for the nearest neighbour PIs. The lowest level variable defaults to “cell” in this case, so there is no need to supply it:

    engPis <- addWeightFunction(engPis)

We the bivariate weight function as an example:

    plotWf(engPis, "nnPairCell")

As before, the minority gene of the pair is the main driver of variance. Next, we fit linear mixed models on the obtained PIs. The experiment was conducted twice, so we add the experiment variable as fixed effect. The image and cell variables are added automatically where appropriate, as printed in the output:

    allModsNNcell <- fitLMMs(engPis, fixedVars = "experiment")

    ## Fitted formula for pi edge:
    ## pi - 0.5 ~ 1 + experiment + (1 | image/cell)

    ## Fitted formula for pi centroid:
    ## pi - 0.5 ~ 1 + experiment + (1 | image/cell)

    ## Fitted formula for pi nnCell:
    ## pi - 0.5 ~ 1 + experiment + (1 | image)

    ## Fitted formula for pi nnPairCell:
    ## pi - 0.5 ~ 1 + experiment + (1 | image)

For the “edge” and “centroid” options, each individual RNA-molecule is an observation in the model, and random effects are added for both cell and image. For “nnCell” and “nnPairCell”, the observations are averaged per cell, and so only a random effect common to all cells of a single image remains. Now we plot the genes with the strongest remoteness from the cell edge:

    plotTopResults(hypEng, results = allModsNNcell, pi = "edge", what = "far",
        numFeats = 1)

Note that the cells are selected for high expression of the gene and are not shown in their original position! Now plot the gene with strongest intracellular aggregation:

    plotTopResults(hypEng, results = allModsNNcell, pi = "nnCell", numFeats = 1)

And finally the gene pair with the strongest antilocalization:

    plotTopResults(hypEng,
        results = allModsNNcell, pi = "nnPairCell", numFeats = 1,
        what = "anti"
    )

We can see that the molecule distribution within the cell is all but uniform! A side question that may arise is whether the measures of intracellular aggregation have a global spatial patterning throughout the tissue. We can explore this graphically for the gene most significantly remote from the cell edge:

    plotExplore(hypEng,
        piEsts = engPis, piColourCell = "edge", features =
            remEdge <- rownames(getResults(allModsNNcell, "edge", "Intercept"))[1]
    )

The molecules are shown in red, while the yellow-blue scale indicates the PI estimate.

## 5.2 Gradients

Another biologically interesting spatial pattern are gradients. Again, they can be estimated image-wide or within cells. Parametric model fitting of Poisson point processes is provided in the spatstat.model package by Baddeley et al. (2015), we wrap it here for convenience. Detecting presence of gradients over different point patterns is done by fitting a model including interaction terms between x and y coordinates and image identifiers. If this interactions are significant, this implies existence of gradients in the different point patterns, albeit with different directions. The fitting process is quite computation intensive, so we limit ourselves to a subset of the data. By default both overall gradients and within-cell gradients are fitted.

    engGrads <- estGradients(hypEng[seq_len(2), ], features = getFeatures(hypEng)[seq_len(3)])
    pValsGrads <- getPvaluesGradient(engGrads, "cell")

Keep in mind though that a gradient that is significant for a computer may look very different from the human perspective; many types of spatial patterns can be captured by a gradient to some extent without providing a satisfactory fit.

# 6 Predicting tumor types with probabilistic indices as predictor

Probabilistic indices are features extracted from point patterns that can serve purposes other than hypothesis testing. Here we demonstrate their use as interpretable predictors for predicting tumor type from cell type locations on a breast cancer dataset from a publication by Keren et al. (2018), previously analysed by VanderDoes et al. (2023). Make sure to install the funkycells package to access the data.

    if(reqFunky <- require(funkycells)){
        data("TNBC_pheno")
        data("TNBC_meta")
        TNBC_pheno$Age <- TNBC_meta$Age[match(TNBC_pheno$Person, TNBC_meta$Person)]
        TNBC_pheno$Class <- ifelse(TNBC_pheno$Class=="0", "mixed", "compartentalised")
        hypTNBC <- buildHyperFrame(TNBC_pheno, coordVars = c("cellx", "celly"),
                                   imageVars = c("Person", "Class", "Age"), featureName = "Phenotype")
        hypTNBC <- hypTNBC[order(hypTNBC$Class),] #Order by class for plots
    }

    ## Loading required package: funkycells

    ## Found 33 unique images

    if(reqFunky){
        plotExplore(hypTNBC, Cex.main = 0.7, features = c("Tumor", "Macrophage", "CD8T"),
                    CexLegend = 0.85)
    }

Explorative plot of triple negative breast cancer dataset.

Fit a penalised regression model, and estimate its predictive accuracy through cross-validation.

    if(reqFunky){
        #Select feature through hypothesis tests
        pisTNBC = estPis(hypTNBC, pis = c("nn", "nnPair"), null = "background", verbose = FALSE)
        pisTNBC = addWeightFunction(pisTNBC)
        TNBClmms = fitLMMs(pisTNBC, fixedVars = c("Age", "Class"), verbose = TRUE)
        #Extract PIs of significant features
        nnRes = getResults(TNBClmms, "nn", "Class")
        sigLevel = 0.05
        nnSig = rownames(nnRes)[nnRes[, "pAdj"] < sigLevel]
        nnPairRes = getResults(TNBClmms, "nnPair", "Class")
        nnPairSig = rownames(nnPairRes)[nnPairRes[, "pAdj"] < sigLevel & !is.na(nnPairRes[, "pAdj"])]
        nnPis = as.matrix(vapply(pisTNBC$hypFrame$pimRes, FUN.VALUE = double(nnn <- (length(nnSig)+ length(nnPairSig))), function(x){
            c(if(length(nnSig)) x$pointDists$nn[nnSig],
              if(length(nnPairSig)) vapply(nnPairSig, FUN.VALUE = 1, function(y) {
                  getGp(gp = y, x$pointDists$nnPair, notFoundReturn = NA)
              }))
        }))
        if(nnn!=1){nnPis = t(nnPis)}
        colnames(nnPis) = c(nnSig, nnPairSig)
        nnPis[is.na(nnPis)] = 0.5 #Replace missing values by 0.5
        if(require(glmnet, quietly = TRUE)){
            set.seed(20062024)
            lassoModel = cv.glmnet(factor(hypTNBC$Class), x = cbind("Age" = hypTNBC$Age, nnPis), family = "binomial", alpha = 1, nfolds = 10, type.measure = "class")
            lassoModel #In-sample misclassification error is quite low
        }
    }

    ## Fitted formula for pi nn:
    ## pi - 0.5 ~ 1 + Age + Class

    ## Fitted formula for pi nnPair:
    ## pi - 0.5 ~ 1 + Age + Class

    ## Loaded glmnet 5.0

    ##
    ## Call:  cv.glmnet(x = cbind(Age = hypTNBC$Age, nnPis), y = factor(hypTNBC$Class),      type.measure = "class", nfolds = 10, family = "binomial",      alpha = 1)
    ##
    ## Measure: Misclassification Error
    ##
    ##       Lambda Index Measure SE Nonzero
    ## min 0.002508    56       0  0       6
    ## 1se 0.002508    56       0  0       6

# 7 Schematic workflow

A schematic overview of the smoppix pipeline, starting from a hyperframe, with corresponding functions is shown below:

# 8 Troubleshooting

An error like

> Error in reducer$value.cache[[as.character(idx)]] <- values : wrong args for environment subassignment In addition: Warning message: In parallel::mccollect(wait = FALSE, timeout = 1) : 1 parallel job did not deliver a result

often indicates insufficient memory. Try reducing the number of cores requested with MultiCoreParam(), or switch to serial processing with register(SerialParam()).

# 9 Session info

    sessionInfo()

    ## R version 4.6.0 Patched (2026-05-01 r89994)
    ## Platform: aarch64-apple-darwin23
    ## Running under: macOS Tahoe 26.3.1
    ##
    ## Matrix products: default
    ## BLAS:   /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib
    ## LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
    ##
    ## locale:
    ## [1] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
    ##
    ## time zone: America/New_York
    ## tzcode source: internal
    ##
    ## attached base packages:
    ## [1] stats     graphics  grDevices utils     datasets  methods   base
    ##
    ## other attached packages:
    ## [1] glmnet_5.0          funkycells_1.1.1    lmerTest_3.2-1
    ## [4] lme4_2.0-1          Matrix_1.7-5        BiocParallel_1.47.0
    ## [7] smoppix_1.5.2
    ##
    ## loaded via a namespace (and not attached):
    ##  [1] tidyselect_1.2.1            dplyr_1.2.1
    ##  [3] farver_2.1.2                S7_0.2.2
    ##  [5] fastmap_1.2.0               SingleCellExperiment_1.35.1
    ##  [7] spatstat.explore_3.8-0      spatstat.geom_3.7-3
    ##  [9] rpart_4.1.27                digest_0.6.39
    ## [11] mime_0.13                   lifecycle_1.0.5
    ## [13] survival_3.8-6              spatstat.data_3.1-9
    ## [15] magrittr_2.0.5              compiler_4.6.0
    ## [17] rlang_1.2.0                 sass_0.4.10
    ## [19] tools_4.6.0                 yaml_2.3.12
    ## [21] knitr_1.51                  labeling_0.4.3
    ## [23] S4Arrays_1.13.0             DelayedArray_0.39.2
    ## [25] RColorBrewer_1.1-3          abind_1.4-8
    ## [27] withr_3.0.2                 numDeriv_2016.8-1.1
    ## [29] BiocGenerics_0.59.3         grid_4.6.0
    ## [31] polyclip_1.10-7             stats4_4.6.0
    ## [33] ggplot2_4.0.3               iterators_1.0.14
    ## [35] scales_1.4.0                MASS_7.3-65
    ## [37] spatstat.utils_3.2-3        dichromat_2.0-0.1
    ## [39] SummarizedExperiment_1.43.0 cli_3.6.6
    ## [41] rmarkdown_2.31              reformulas_0.4.4
    ## [43] generics_0.1.4              otel_0.2.0
    ## [45] RcppParallel_5.1.11-2       rjson_0.2.23
    ## [47] minqa_1.2.8                 cachem_1.1.0
    ## [49] splines_4.6.0               spatstat.model_3.7-0
    ## [51] parallel_4.6.0              XVector_0.53.0
    ## [53] matrixStats_1.5.0           vctrs_0.7.3
    ## [55] boot_1.3-32                 jsonlite_2.0.0
    ## [57] IRanges_2.47.1              S4Vectors_0.51.2
    ## [59] tensor_1.5.1                magick_2.9.1
    ## [61] foreach_1.5.2               spatstat.univar_3.2-0
    ## [63] jquerylib_0.1.4             goftest_1.2-3
    ## [65] spatstat.random_3.4-5       glue_1.8.1
    ## [67] nloptr_2.2.1                codetools_0.2-20
    ## [69] shape_1.4.6.1               stringi_1.8.7
    ## [71] gtable_0.3.6                deldir_2.0-4
    ## [73] GenomicRanges_1.65.0        tibble_3.3.1
    ## [75] pillar_1.11.1               htmltools_0.5.9
    ## [77] Seqinfo_1.3.0               R6_2.6.1
    ## [79] zigg_0.0.2                  Rdpack_2.6.6
    ## [81] evaluate_1.0.5              lattice_0.22-9
    ## [83] Biobase_2.73.1              rbibutils_2.4.1
    ## [85] SpatialExperiment_1.23.0    openxlsx_4.2.8.1
    ## [87] Rfast_2.1.5.2               bslib_0.11.0
    ## [89] Rcpp_1.1.1-1.1              zip_2.3.3
    ## [91] spatstat.sparse_3.2-0       SparseArray_1.13.2
    ## [93] nlme_3.1-169                mgcv_1.9-4
    ## [95] xfun_0.57                   MatrixGenerics_1.25.0
    ## [97] pkgconfig_2.0.3

# 10 Bibliography

Baddeley, A., E. Rubak, and R. Turner. 2015. Spatial point patterns: Methodology and applications with R. CRC press.
Eng, C.-H. L., M. Lawson, Q. Zhu, et al. 2019. “Transcriptome-scale super-resolved imaging in tissues by RNA seqFISH+.” Nature 568 (7751): 235–39. https://doi.org/10.1038/s41586-019-1049-y.
Hawinkel, S., X. Yang, W. Poelmans, H. Motte, T. Beeckman, and S. Maere. 2025. “Unified nonparametric analysis of single-molecule spatial omics data using probabilistic indices.” bioRxiv, ahead of print. https://doi.org/10.1101/2025.05.20.654270.
Keren, L., M. Bosse, D. Marquez, et al. 2018. “A Structured Tumor-Immune Microenvironment in Triple Negative Breast Cancer Revealed by Multiplexed Ion Beam Imaging.” Cell 174 (6): 1373 - 1387 - e19. https://doi.org/10.1016/j.cell.2018.08.039.
VanderDoes, J., C. Marceaux, K. Yokote, M.-L. Asselin-Labat, G. Rice, and J. D. Hywood. 2023. “Using random forests to uncover the predictive power of distancevarying cell interactions in tumor microenvironments.” bioRxiv, ahead of print. https://doi.org/10.1101/2023.07.18.549619.
Yang, X., W. Poelmans, C. Grones, et al. 2023. “Spatial transcriptomics of a lycophyte root sheds light on root evolution.” Curr. Biol. 33 (19): 4069–84. https://doi.org/10.1016/j.cub.2023.08.030.