The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
detect_TLS()
scan_clustering() (Optional)calc_icat()detect_tic()summarize_TLS()plot_TLS()Tertiary lymphoid structures (TLS) are ectopic lymphoid organs that form in non-lymphoid tissues — most notably in tumours — and are associated with improved patient outcomes and immunotherapy response. tlsR provides a fast, reproducible pipeline for detecting TLS and characterising their spatial organisation in multiplexed tissue imaging data (e.g. mIHC, CODEX, IMC).
The core pipeline is:
Raw ldata list
│
▼
detect_TLS() ← KNN-based B+T co-localisation
│
├──► scan_clustering() ← Optional: local Ripley's L
│
├──► calc_icat() ← ICAT linearity score per TLS
│
├──► detect_tic() ← T-cell clusters outside TLS
│
├──► summarize_TLS() ← Tidy summary table
│
└──► plot_TLS() ← Publication-ready spatial plot
tlsR expects a named list of data
frames (ldata), one element per tissue sample.
Each data frame must contain at minimum:
| Column | Type | Description |
|---|---|---|
x |
numeric | X coordinate in microns |
y |
numeric | Y coordinate in microns |
phenotype |
character | Cell label; must contain "B cell" /
"T cell" |
Additional columns (e.g. cell area, marker intensities) are silently ignored.
library(tlsR)
data(toy_ldata)
# Structure of the built-in example dataset
str(toy_ldata)
#> List of 1
#> $ ToySample:'data.frame': 2130 obs. of 5 variables:
#> ..$ x : num [1:2130] 863 2365 1227 2649 2821 ...
#> ..$ y : num [1:2130] 479 434 448 1543 1478 ...
#> ..$ phenotype: chr [1:2130] "Other" "Other" "Other" "Other" ...
#> ..$ row_index: int [1:2130] 1 2 3 4 5 6 7 8 9 10 ...
#> ..$ cflag : int [1:2130] 0 0 0 0 0 0 0 0 0 0 ...
table(toy_ldata[["ToySample"]]$phenotype)
#>
#> B cells Other T cells
#> 143 1850 137detect_TLS()detect_TLS() identifies B-cell-rich regions with
sufficient T-cell co-localisation using a KNN density approach.
# Ensure toy data has expected columns for the new validation
data(toy_ldata)
if (!"phenotype" %in% names(toy_ldata[["ToySample"]])) {
toy_ldata[["ToySample"]]$phenotype <- toy_ldata[["ToySample"]]$coarse_phen_vec # or whatever the correct mapping is
}
ldata <- detect_TLS(
LSP = "ToySample",
k = 30, # neighbours for density estimation
bcell_density_threshold = 15, # min avg 1/k-distance (um)
min_B_cells = 50, # min B cells per candidate TLS
min_T_cells_nearby = 30, # min T cells within max_distance_T
max_distance_T = 50, # search radius (um)
ldata = toy_ldata
)
#> Sample 'ToySample': 1 TLS detected.
table(ldata[["ToySample"]]$tls_id_knn)
#>
#> 0 1
#> 2050 80The new column tls_id_knn is 0 for non-TLS
cells and a positive integer for cells assigned to TLS 1, 2, 3, … .
df <- ldata[["ToySample"]]
col <- ifelse(df$tls_id_knn == 0, "grey80",
c("#0072B2", "#009E73", "#CC79A7")[df$tls_id_knn])
plot(df$x, df$y,
col = col, pch = 19, cex = 0.3,
xlab = "x (um)", ylab = "y (um)",
main = "Detected TLS — ToySample")
legend("topright",
legend = c("Background", paste0("TLS ", sort(unique(df$tls_id_knn[df$tls_id_knn > 0])))),
col = c("grey80", "#0072B2", "#009E73", "#CC79A7"),
pch = 19, pt.cex = 1.2, bty = "n")scan_clustering()
(Optional)scan_clustering() slides a square window across the
tissue and tests for statistically significant immune cell clustering
using Ripley’s L with a Monte Carlo CSR envelope.
# eval=FALSE because this step can take ~10–30 s on real data
windows <- scan_clustering(
ws = 500, # window side (um)
sample = "ToySample",
phenotype = "B cells",
nsim = 39, # Monte Carlo simulations (39 → p < 0.05)
plot = FALSE,
ldata = ldata
)
cat("Significant windows:", length(windows), "\n")
# Access the first window's centre and cell count:
if (length(windows) > 0) {
cat("Centre:", windows[[1]]$window_center, "\n")
cat("Cells: ", windows[[1]]$n_cells, "\n")
}calc_icat()The ICAT (Immune Cell Arrangement Trace) index quantifies how linearly organised cells are within a TLS. A higher value indicates a more structured (germinal-centre-like) arrangement.
n_tls <- max(ldata[["ToySample"]]$tls_id_knn, na.rm = TRUE)
if (n_tls >= 1) {
icat_scores <- vapply(
seq_len(n_tls),
function(id) calc_icat("ToySample", tlsID = id, ldata = ldata),
numeric(1)
)
names(icat_scores) <- paste0("TLS", seq_len(n_tls))
print(icat_scores)
}
#> TLS1
#> -2.401757calc_icat() returns NA (with a message) if
a TLS has too few cells or if FastICA fails to converge — no errors are
thrown.
detect_tic()T-cell clusters (TIC) that lie outside TLS are identified
with HDBSCAN. The min_pts and min_cluster_size
arguments let you control sensitivity.
ldata <- detect_tic(
sample = "ToySample",
min_pts = 10, # HDBSCAN minPts
min_cluster_size = 10, # drop clusters smaller than this
ldata = ldata
)
#> detect_tic: 3 T-cell cluster(s) detected in 'ToySample'.
table(ldata[["ToySample"]]$tcell_cluster_hdbscan, useNA = "ifany")
#>
#> 0 1 2 3 <NA>
#> 22 50 41 24 1993summarize_TLS()summarize_TLS() produces a tidy one-row-per-sample
summary — convenient for downstream statistical analysis.
sumtbl <- summarize_TLS(ldata, calc_icat_scores = FALSE)
print(sumtbl)
#> sample n_TLS total_cells TLS_cells TLS_fraction mean_TLS_size n_TIC
#> 1 ToySample 1 2130 80 0.03755869 80 3With calc_icat_scores = TRUE a list-column
icat_scores is appended containing named numeric vectors of
per-TLS ICAT values.
plot_TLS()plot_TLS() produces a ggplot2 scatter plot with TLS and
TIC coloured distinctly using a colourblind-friendly palette.
p <- plot_TLS(
sample = "ToySample",
ldata = ldata,
show_tic = TRUE,
point_size = 0.5,
alpha = 0.7
)The returned ggplot object can be further customised
with standard ggplot2 functions:
tlsR is designed to scale naturally to many samples.
Simply pass your full ldata list and iterate:
samples <- names(ldata)
ldata <- Reduce(function(ld, s) detect_TLS(s, ldata = ld), samples, ldata)
ldata <- Reduce(function(ld, s) detect_tic(s, ldata = ld), samples, ldata)
summary_all <- summarize_TLS(ldata)
print(summary_all)sessionInfo()
#> R version 4.5.2 (2025-10-31)
#> Platform: aarch64-apple-darwin20
#> Running under: macOS Tahoe 26.3.1
#>
#> Matrix products: default
#> BLAS: /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/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] ggplot2_4.0.2 tlsR_0.2.0
#>
#> loaded via a namespace (and not attached):
#> [1] sass_0.4.10 generics_0.1.4 spatstat.explore_3.8-0
#> [4] tensor_1.5.1 spatstat.data_3.1-9 lattice_0.22-9
#> [7] digest_0.6.39 magrittr_2.0.4 spatstat.utils_3.2-2
#> [10] evaluate_1.0.5 grid_4.5.2 RColorBrewer_1.1-3
#> [13] fastmap_1.2.0 jsonlite_2.0.0 Matrix_1.7-5
#> [16] spatstat.sparse_3.1-0 scales_1.4.0 jquerylib_0.1.4
#> [19] abind_1.4-8 cli_3.6.5 rlang_1.1.7
#> [22] polyclip_1.10-7 fastICA_1.2-7 withr_3.0.2
#> [25] cachem_1.1.0 yaml_2.3.12 otel_0.2.0
#> [28] spatstat.univar_3.1-7 FNN_1.1.4.1 tools_4.5.2
#> [31] deldir_2.0-4 dplyr_1.2.0 spatstat.geom_3.7-3
#> [34] vctrs_0.7.2 R6_2.6.1 lifecycle_1.0.5
#> [37] dbscan_1.2.4 pkgconfig_2.0.3 pillar_1.11.1
#> [40] bslib_0.10.0 gtable_0.3.6 glue_1.8.0
#> [43] Rcpp_1.1.1 xfun_0.57 tibble_3.3.1
#> [46] tidyselect_1.2.1 rstudioapi_0.18.0 knitr_1.51
#> [49] dichromat_2.0-0.1 goftest_1.2-3 farver_2.1.2
#> [52] nlme_3.1-169 htmltools_0.5.9 spatstat.random_3.4-5
#> [55] labeling_0.4.3 rmarkdown_2.31 compiler_4.5.2
#> [58] S7_0.2.1These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.