OSCR

Spatially resolved transcriptomics identifies intercellular signaling post-ischemic stroke that controls neural stem cell proliferation.

Code ↔ Paper

2 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 2 matches
  1. [1] § Results › Intercellular communication inference from spatial transcriptomic profiling ↔ R/modeling.R, lines 2–63 · score 0.56 · interaction strength, infer cell, spatial transcriptomics, model, spots, ranked
  2. [2] § STAR★Methods › Method details › Analysis of scRNA-sequencing data ↔ R/CellChat_class.R, lines 2–76 · score 0.55 · single cell RNA, Seurat, UMAP, Seq, genes

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

R · 1,297 lines · 65 KB · GPL-3.0 · 1 match

  1. #' Compute the communication probability/strength between any interacting cell groups
  2. #'
  3. #' To further speed up on large-scale datasets, USER can downsample the data using the function 'subset' from Seurat package (e.g., pbmc.small <- subset(pbmc, downsample = 500)), or using the function `sketchData` from CellChat, in particular for the large cell clusters;
  4. #'
  5. #'
  6. #' @param object CellChat object
  7. #' @param type Methods for computing the average gene expression per cell group. By default = "triMean", producing fewer but stronger interactions;
  8. #' When setting `type = "truncatedMean"`, a value should be assigned to 'trim', producing more interactions.
  9. #' @param trim the fraction (0 to 0.25) of observations to be trimmed from each end of x before the mean is computed
  10. #' @param LR.use A subset of ligand-receptor interactions used in inferring communication network
  11. #' @param raw.use Whether use the raw data (i.e., `[email hidden]`) or the smoothed data (i.e., `[email hidden]`).
  12. #' Set raw.use = FALSE to use the projected data when analyzing single-cell data with shallow sequencing depth because the projected data could help to reduce the dropout effects of signaling genes, in particular for possible zero expression of subunits of ligands/receptors.
  13. #' @param population.size Whether consider the proportion of cells in each group across all sequenced cells.
  14. #' Set population.size = FALSE if analyzing sorting-enriched single cells, to remove the potential artifact of population size.
  15. #' Set population.size = TRUE if analyzing unsorted single-cell transcriptomes, with the reason that abundant cell populations tend to send collectively stronger signals than the rare cell populations.
  16. #'
  17. #' Parameters for spatial data analysis:
  18. #' @param distance.use Whether to use distance constraints to compute communication probability. Setting `distance.use = TRUE` indicates that the cell-cell communication probability is inversely proportional to the computed distance.
  19. #' Setting `distance.use = FALSE` will only filter out interactions between spatially distant regions, but not add distance constraints.
  20. #' @param interaction.range The maximum interaction/diffusion length of ligands (Unit: microns). This hard threshold is used to filter out the connections between spatially distant regions
  21. #' @param scale.distance A scale or normalization factor for the spatial distances when setting `distance.use = TRUE`. For example, scale.distance equals 1, 0.1, 0.01, 0.001, 0.11, or 0.011. We choose this values such that the minimum value of the scaled distances is in [1,2]. This value is not necessary when setting `distance.use = FALSE`.
  22. #'
  23. #' When comparing communication across different CellChat objects, the same scale factor should be used. For a single CellChat analysis, different scale factors will not affect the ranking of the signaling based on their interaction strength.
  24. #'
  25. #' @param k.min The minimum number of interacting cell pairs required for defining spatially proximal cell groups.
  26. #' @param contact.dependent Whether using the `contact-dependent` manner for inference signaling, that is determining interacting cell pairs by requiring cells to be in direct membrane-membrane contact. By default `contact.dependent = TRUE` when inferring contact-dependent and juxtacrine signaling (that is "Cell-Cell Contact" signaling classified in CellChatDB$interaction$annotation).
  27. #' If only focusing on `Secreted Signaling`, the `contact-dependent` manner will be not used except for setting `contact.dependent.forced = TRUE`.
  28. #' @param contact.range The interaction range (Unit: microns) to restrict the contact-dependent signaling when `contact.dependent = TRUE`.
  29. #' For spatial transcriptomics in a single-cell resolution, `contact.range` is approximately equal to the estimated cell diameter (i.e., the cell center-to-center distance), which means that contact-dependent and juxtacrine signaling can only happens when the two cells are contact to each other.
  30. #'
  31. #' Typically, `contact.range = 10`, which is a typical human cell size. However, for low-resolution spatial data such as 10X visium, it should be the cell center-to-center distance (i.e., `contact.range = 100` for visium data). The function `computeCellDistance` can compute the center-to-center distance.
  32. #'
  33. #' @param contact.knn.k Number of neighbors to restrict the contact-dependent signaling within the neatest neighbors when `contact.dependent = TRUE`. By default, CellChat uses `contact.range` to restrict the contact-dependent signaling; however, users can also provide a value of `contact.knn.k`, in order to determine interacting cell pairs based on the k-nearest neighbors (knn).
  34. #' For 10X visium, contact.knn.k = 6. For other spatial technologies, this value may be hard to determine because the sequenced cells/spots are usually not regularly arranged.
  35. #' @param do.symmetric Whether converting the adjacent matrix into symmetric one when determining spatially proximal cell groups. Default is TRUE, indicating that if adj(i,j) or adj(j,i) is zero, then both are zeros.
  36. #'
  37. #' @param contact.dependent.forced Whether forcing to use the `contact-dependent` manner for inference signaling for all L-R pairs including secreted signaling. Users can set `contact.dependent.forced = TRUE` if also preferring interactions within a contact manner for `Secreted Signaling`.
  38. #'
  39. #' @param nboot Threshold of p-values
  40. #' @param seed.use Set a random seed. By default, set the seed to 1.
  41. #' @param Kh Parameter in Hill function
  42. #' @param n Parameter in Hill function
  43. #'
  44. #'
  45. #' @importFrom future nbrOfWorkers
  46. #' @importFrom future.apply future_sapply
  47. #' @importFrom pbapply pbsapply
  48. #' @importFrom stats aggregate
  49. #' @importFrom Matrix crossprod
  50. #' @importFrom utils txtProgressBar setTxtProgressBar
  51. #'
  52. #' @return A CellChat object with updated slot 'net':
  53. #'
  54. #' object@net$prob is the inferred communication probability (strength) array, where the first, second and third dimensions represent a source, target and ligand-receptor pair, respectively.
  55. #'
  56. #' USER can access all the inferred cell-cell communications using the function 'subsetCommunication(object)', which returns a data frame.
  57. #'
  58. #' object@net$pval is the corresponding p-values of each interaction
  59. #'
  60. #' @export
  61. #'
  62. computeCommunProb <- function(object, type = c("triMean", "truncatedMean","thresholdedMean", "median"), trim = 0.1, LR.use = NULL, raw.use = TRUE, population.size = FALSE,
  63. distance.use = TRUE, interaction.range = 250, scale.distance = 0.01, k.min = 10, contact.dependent = TRUE, contact.range = NULL, contact.knn.k = NULL, contact.dependent.forced = FALSE, do.symmetric = TRUE,
  64. nboot = 100, seed.use = 1L, Kh = 0.5, n = 1) {
  65. type <- match.arg(type)
  66. cat(type, "is used for calculating the average gene expression per cell group.", "\n")
  67. FunMean <- switch(type,
  68. triMean = triMean,
  69. truncatedMean = function(x) mean(x, trim = trim, na.rm = TRUE),
  70. thresholdedMean = function(x) thresholdedMean(x, trim = trim, na.rm = TRUE),
  71. median = function(x) median(x, na.rm = TRUE))
  72. if (raw.use) {
  73. data <- as.matrix([email hidden])
  74. } else {
  75. data <- as.matrix([email hidden])
  76. }
  77. if (is.null(LR.use)) {
  78. pairLR.use <- object@LR$LRsig
  79. } else {
  80. if (length(unique(LR.use$annotation)) > 1) {
  81. LR.use$annotation <- factor(LR.use$annotation, levels = c("Secreted Signaling","ECM-Receptor", "Non-protein Signaling", "Cell-Cell Contact"))
  82. LR.use <- LR.use[order(LR.use$annotation), , drop = FALSE]
  83. LR.use$annotation <- as.character(LR.use$annotation)
  84. }
  85. pairLR.use <- LR.use
  86. }
  87. complex_input <- object@DB$complex
  88. cofactor_input <- object@DB$cofactor
  89. my.sapply <- ifelse(
  90. test = future::nbrOfWorkers() == 1,
  91. yes = sapply,
  92. no = future.apply::future_sapply
  93. )
  94. ptm = Sys.time()
  95. pairLRsig <- pairLR.use
  96. group <- object@idents
  97. geneL <- as.character(pairLRsig$ligand)
  98. geneR <- as.character(pairLRsig$receptor)
  99. nLR <- nrow(pairLRsig)
  100. numCluster <- nlevels(group)
  101. if (numCluster != length(unique(group))) {
  102. stop("Please check `unique(object@idents)` and ensure that the factor levels are correct!
  103. You may need to drop unused levels using 'droplevels' function. e.g.,
  104. `meta$labels = droplevels(meta$labels, exclude = setdiff(levels(meta$labels),unique(meta$labels)))`")
  105. }
  106. data.use <- data/max(data)
  107. nC <- ncol(data.use)
  108. # compute the average expression per group
  109. data.use.avg <- aggregate(t(data.use), list(group), FUN = FunMean)
  110. data.use.avg <- t(data.use.avg[,-1])
  111. colnames(data.use.avg) <- levels(group)
  112. # compute the expression of ligand or receptor
  113. dataLavg <- computeExpr_LR(geneL, data.use.avg, complex_input)
  114. dataRavg <- computeExpr_LR(geneR, data.use.avg, complex_input)
  115. # take account into the effect of co-activation and co-inhibition receptors
  116. dataRavg.co.A.receptor <- computeExpr_coreceptor(cofactor_input, data.use.avg, pairLRsig, type = "A")
  117. dataRavg.co.I.receptor <- computeExpr_coreceptor(cofactor_input, data.use.avg, pairLRsig, type = "I")
  118. dataRavg <- dataRavg * dataRavg.co.A.receptor/dataRavg.co.I.receptor
  119. dataLavg2 <- t(replicate(nrow(dataLavg), as.numeric(table(group))/nC))
  120. dataRavg2 <- dataLavg2
  121. # compute the expression of agonist and antagonist
  122. index.agonist <- which(!is.na(pairLRsig$agonist) & pairLRsig$agonist != "")
  123. index.antagonist <- which(!is.na(pairLRsig$antagonist) & pairLRsig$antagonist != "")
  124. # quantify the communication probability
  125. # compute the spatial constraint
  126. if (object@options$datatype != "RNA") {
  127. data.spatial <- object@images$coordinates
  128. if ("spatial.factors" %in% names(object@images)) {
  129. ratio <- object@images$spatial.factors$ratio
  130. tol <- object@images$spatial.factors$tol
  131. } else {
  132. stop("`object@images$spatial.factors` is missing. Please update the object via `updateCellChat`! \n")
  133. }
  134. meta.t = data.frame(group = group, samples = object@meta$samples, row.names = rownames(object@meta))
  135. res <- computeRegionDistance(coordinates = data.spatial, meta = meta.t, interaction.range = interaction.range, ratio = ratio, tol = tol, k.min = k.min, contact.dependent = contact.dependent, contact.range = contact.range, contact.knn.k = contact.knn.k)
  136. d.spatial <- res$d.spatial # NaN if no nearby cell pairs
  137. adj.contact <- res$adj.contact # zeros if no nearby cell pairs
  138. if (distance.use) {
  139. print(paste0('>>> Run CellChat on spatial transcriptomics data using distances as constraints of the computed communication probability <<< [', Sys.time(),']'))
  140. d.spatial <- d.spatial * scale.distance
  141. diag(d.spatial) <- NaN
  142. d.min <- min(d.spatial, na.rm = TRUE)
  143. if (d.min < 1) {
  144. cat("The suggested minimum value of scaled distances is in [1,2], and the calculated value here is ", d.min,"\n")
  145. stop("Please increase the value of `scale.distance` and use a value that is slighly smaller than ", format(1/d.min, digits = 2) ,"\n")
  146. }
  147. P.spatial <- 1/d.spatial
  148. P.spatial[is.na(d.spatial)] <- 0
  149. diag(P.spatial) <- max(P.spatial) # if this value is 1, the self-connections will have more larger weight.
  150. d.spatial <- d.spatial/scale.distance # This is only for saving the data
  151. } else {
  152. print(paste0('>>> Run CellChat on spatial transcriptomics data without distance values as constraints of the computed communication probability <<< [', Sys.time(),']'))
  153. P.spatial <- matrix(1, nrow = numCluster, ncol = numCluster)
  154. P.spatial[is.na(d.spatial)] <- 0 # diagonal is 1
  155. }
  156. } else {
  157. print(paste0('>>> Run CellChat on sc/snRNA-seq data <<< [', Sys.time(),']'))
  158. d.spatial <- matrix(NaN, nrow = numCluster, ncol = numCluster)
  159. P.spatial <- matrix(1, nrow = numCluster, ncol = numCluster)
  160. adj.contact <- matrix(1, nrow = numCluster, ncol = numCluster)
  161. contact.dependent = FALSE; contact.dependent.forced = FALSE; contact.range = NULL; contact.knn.k = NULL;
  162. distance.use = NULL; interaction.range = NULL; ratio = NULL; tol = NULL; k.min = NULL;
  163. }
  164. if (object@options$datatype == "RNA") {
  165. nLR1 <- nLR
  166. } else {
  167. if (contact.dependent.forced == TRUE) {
  168. cat("Force to run CellChat in a `contact-dependent` manner for all L-R pairs including secreted signaling.\n")
  169. P.spatial <- P.spatial * adj.contact
  170. nLR1 <- nLR
  171. } else { # contact.dependent.forced == F
  172. if (contact.dependent == TRUE && length(unique(pairLRsig$annotation)) > 0) {
  173. if (all(unique(pairLRsig$annotation) %in% c("Cell-Cell Contact"))) {
  174. cat("All the input L-R pairs are `Cell-Cell Contact` signaling. Run CellChat in a contact-dependent manner. \n")
  175. P.spatial <- P.spatial * adj.contact
  176. nLR1 <- nLR
  177. } else if (all(unique(pairLRsig$annotation) %in% c("Secreted Signaling", "ECM-Receptor", "Non-protein Signaling"))) {
  178. cat("Molecules of the input L-R pairs are diffusible. Run CellChat in a diffusion manner based on the `interaction.range`.\n")
  179. nLR1 <- nLR
  180. } else {
  181. cat("The input L-R pairs have both secreted signaling and contact-dependent signaling. Run CellChat in a contact-dependent manner for `Cell-Cell Contact` signaling, and in a diffusion manner based on the `interaction.range` for other L-R pairs. \n")
  182. nLR1 <- max(which(pairLRsig$annotation %in% c("Secreted Signaling", "ECM-Receptor", "Non-protein Signaling")))
  183. }
  184. } else { # contact.dependent == F or there is no `annotation` column in the database
  185. cat("Run CellChat in a diffusion manner based on the `interaction.range` for all L-R pairs. Setting `contact.dependent = TRUE` if preferring a contact-dependent manner for `Cell-Cell Contact` signaling. \n")
  186. nLR1 <- nLR
  187. }
  188. }
  189. }
  190. Prob <- array(0, dim = c(numCluster,numCluster,nLR))
  191. Pval <- array(0, dim = c(numCluster,numCluster,nLR))
  192. set.seed(seed.use)
  193. permutation <- replicate(nboot, sample.int(nC, size = nC))
  194. data.use.avg.boot <- my.sapply(
  195. X = 1:nboot,
  196. FUN = function(nE) {
  197. groupboot <- group[permutation[, nE]]
  198. data.use.avgB <- aggregate(t(data.use), list(groupboot), FUN = FunMean)
  199. data.use.avgB <- t(data.use.avgB[,-1])
  200. return(data.use.avgB)
  201. },
  202. simplify = FALSE
  203. )
  204. pb <- txtProgressBar(min = 0, max = nLR, style = 3, file = stderr())
  205. for (i in 1:nLR) {
  206. # ligand/receptor
  207. dataLR <- Matrix::crossprod(matrix(dataLavg[i,], nrow = 1), matrix(dataRavg[i,], nrow = 1))
  208. P1 <- dataLR^n/(Kh^n + dataLR^n)
  209. P1_Pspatial <- P1*P.spatial
  210. if (sum(P1_Pspatial) == 0) {
  211. Pnull = P1_Pspatial
  212. Prob[ , , i] <- Pnull
  213. p = 1
  214. Pval[, , i] <- matrix(p, nrow = numCluster, ncol = numCluster, byrow = FALSE)
  215. } else {
  216. if (i > nLR1) {
  217. P.spatial <- P.spatial * adj.contact
  218. }
  219. # agonist and antagonist
  220. if (is.element(i, index.agonist)) {
  221. data.agonist <- computeExpr_agonist(data.use = data.use.avg, pairLRsig, cofactor_input, index.agonist = i, Kh = Kh, n = n)
  222. P2 <- Matrix::crossprod(matrix(data.agonist, nrow = 1))
  223. } else {
  224. P2 <- matrix(1, nrow = numCluster, ncol = numCluster)
  225. }
  226. if (is.element(i, index.antagonist)) {
  227. data.antagonist <- computeExpr_antagonist(data.use = data.use.avg, pairLRsig, cofactor_input, index.antagonist = i, Kh = Kh, n = n)
  228. P3 <- Matrix::crossprod(matrix(data.antagonist, nrow = 1))
  229. } else {
  230. P3 <- matrix(1, nrow = numCluster, ncol = numCluster)
  231. }
  232. # number of cells
  233. if (population.size) {
  234. P4 <- Matrix::crossprod(matrix(dataLavg2[i,], nrow = 1), matrix(dataRavg2[i,], nrow = 1))
  235. } else {
  236. P4 <- matrix(1, nrow = numCluster, ncol = numCluster)
  237. }
  238. # Pnull = P1*P2*P3*P4
  239. Pnull = P1*P2*P3*P4*P.spatial
  240. Prob[ , , i] <- Pnull
  241. Pnull <- as.vector(Pnull)
  242. #Pboot <- foreach(nE = 1:nboot) %dopar% {
  243. Pboot <- my.sapply(
  244. X = 1:nboot,
  245. FUN = function(nE) {
  246. data.use.avgB <- data.use.avg.boot[[nE]]
  247. dataLavgB <- computeExpr_LR(geneL[i], data.use.avgB, complex_input)
  248. dataRavgB <- computeExpr_LR(geneR[i], data.use.avgB, complex_input)
  249. # take account into the effect of co-activation and co-inhibition receptors
  250. dataRavgB.co.A.receptor <- computeExpr_coreceptor(cofactor_input, data.use.avgB, pairLRsig[i, , drop = FALSE], type = "A")
  251. dataRavgB.co.I.receptor <- computeExpr_coreceptor(cofactor_input, data.use.avgB, pairLRsig[i, , drop = FALSE], type = "I")
  252. dataRavgB <- dataRavgB * dataRavgB.co.A.receptor/dataRavgB.co.I.receptor
  253. dataLRB = Matrix::crossprod(dataLavgB, dataRavgB)
  254. P1.boot <- dataLRB^n/(Kh^n + dataLRB^n)
  255. # agonist and antagonist
  256. if (is.element(i, index.agonist)) {
  257. data.agonist <- computeExpr_agonist(data.use = data.use.avgB, pairLRsig, cofactor_input, index.agonist = i, Kh = Kh, n = n)
  258. P2.boot <- Matrix::crossprod(matrix(data.agonist, nrow = 1))
  259. } else {
  260. P2.boot <- matrix(1, nrow = numCluster, ncol = numCluster)
  261. }
  262. if (is.element(i, index.antagonist)) {
  263. data.antagonist <- computeExpr_antagonist(data.use = data.use.avgB, pairLRsig, cofactor_input, index.antagonist = i, Kh = Kh, n= n)
  264. P3.boot <- Matrix::crossprod(matrix(data.antagonist, nrow = 1))
  265. } else {
  266. P3.boot <- matrix(1, nrow = numCluster, ncol = numCluster)
  267. }
  268. if (population.size) {
  269. groupboot <- group[permutation[, nE]]
  270. dataLavg2B <- as.numeric(table(groupboot))/nC
  271. dataLavg2B <- matrix(dataLavg2B, nrow = 1)
  272. dataRavg2B <- dataLavg2B
  273. P4.boot = Matrix::crossprod(dataLavg2B, dataRavg2B)
  274. } else {
  275. P4.boot = matrix(1, nrow = numCluster, ncol = numCluster)
  276. }
  277. # Pboot = P1.boot*P2.boot*P3.boot*P4.boot
  278. Pboot = P1.boot*P2.boot*P3.boot*P4.boot*P.spatial
  279. return(as.vector(Pboot))
  280. }
  281. )
  282. Pboot <- matrix(unlist(Pboot), nrow=length(Pnull), ncol = nboot, byrow = FALSE)
  283. nReject <- rowSums(Pboot - Pnull > 0)
  284. p = nReject/nboot
  285. Pval[, , i] <- matrix(p, nrow = numCluster, ncol = numCluster, byrow = FALSE)
  286. }
  287. setTxtProgressBar(pb = pb, value = i)
  288. }
  289. close(con = pb)
  290. Pval[Prob == 0] <- 1
  291. dimnames(Prob) <- list(levels(group), levels(group), rownames(pairLRsig))
  292. dimnames(Pval) <- dimnames(Prob)
  293. net <- list("prob" = Prob, "pval" = Pval)
  294. execution.time = Sys.time() - ptm
  295. object@options$run.time <- as.numeric(execution.time, units = "secs")
  296. object@options$parameter <- list(type.mean = type, trim = trim, raw.use = raw.use, population.size = population.size, nboot = nboot, seed.use = seed.use, Kh = Kh, n = n,
  297. distance.use = distance.use, interaction.range = interaction.range, ratio = ratio, tol = tol, k.min = k.min,
  298. contact.dependent = contact.dependent, contact.range = contact.range, contact.knn.k = contact.knn.k, contact.dependent.forced = contact.dependent.forced
  299. )
  300. if (object@options$datatype != "RNA") {
  301. object@images$distance <- d.spatial
  302. }
  303. object@net <- net
  304. print(paste0('>>> CellChat inference is done. Parameter values are stored in `object@options$parameter` <<< [', Sys.time(),']'))
  305. return(object)
  306. }
  307. #' Compute the communication probability on signaling pathway level by summarizing all related ligands/receptors
  308. #'
  309. #' @param object CellChat object
  310. #' @param net A list from object@net; If net = NULL, net = object@net
  311. #' @param pairLR.use A dataframe giving the ligand-receptor interactions; If pairLR.use = NULL, pairLR.use = object@LR$LRsig
  312. #' @param thresh threshold of the p-value for determining significant interaction
  313. #'
  314. #' @return A CellChat object with updated slot 'netP':
  315. #'
  316. #' object@netP$prob is the communication probability array on signaling pathway level; USER can convert this array to a data frame using the function 'reshape2::melt()',
  317. #'
  318. #' e.g., `df.netP <- reshape2::melt(object@netP$prob, value.name = "prob"); colnames(df.netP)[1:3] <- c("source","target","pathway_name")` or access all significant interactions using the function \code{\link{subsetCommunication}}
  319. #'
  320. #' object@netP$pathways list all the signaling pathways with significant communications.
  321. #'
  322. #' From version >= 1.1.0, pathways are ordered based on the total communication probabilities. NB: pathways with small total communication probabilities might be also very important since they might be specifically activated between only few cell types.
  323. #'
  324. #' @export
  325. #'
  326. computeCommunProbPathway <- function(object = NULL, net = NULL, pairLR.use = NULL, thresh = 0.05) {
  327. if (is.null(net)) {
  328. net <- object@net
  329. }
  330. if (is.null(pairLR.use)) {
  331. pairLR.use <- object@LR$LRsig
  332. }
  333. prob <- net$prob
  334. prob[net$pval > thresh] <- 0
  335. LR <- dimnames(prob)[[3]]
  336. LR.sig <- LR[apply(prob, 3, sum) != 0]
  337. pathways <- unique(pairLR.use$pathway_name)
  338. group <- factor(pairLR.use$pathway_name, levels = pathways)
  339. prob.pathways <- aperm(apply(prob, c(1, 2), by, group, sum), c(2, 3, 1))
  340. pathways.sig <- pathways[apply(prob.pathways, 3, sum) != 0]
  341. prob.pathways.sig <- prob.pathways[,,pathways.sig, drop = FALSE]
  342. idx <- sort(apply(prob.pathways.sig, 3, sum), decreasing=TRUE, index.return = TRUE)$ix
  343. pathways.sig <- pathways.sig[idx]
  344. prob.pathways.sig <- prob.pathways.sig[, , idx]
  345. if (is.null(object)) {
  346. netP = list(pathways = pathways.sig, prob = prob.pathways.sig)
  347. return(netP)
  348. } else {
  349. object@net$LRs <- LR.sig
  350. object@netP$pathways <- pathways.sig
  351. object@netP$prob <- prob.pathways.sig
  352. return(object)
  353. }
  354. }
  355. #' Calculate the aggregated network by counting the number of links or summarizing the communication probability
  356. #'
  357. #' @param object CellChat object
  358. #' @param sources.use,targets.use,signaling,pairLR.use Please check the description in function \code{\link{subsetCommunication}}
  359. #' @param remove.isolate whether removing the isolate cell groups without any interactions when applying \code{\link{subsetCommunication}}
  360. #' @param thresh threshold of the p-value for determining significant interaction
  361. #' @param return.object whether return an updated CellChat object
  362. #' @importFrom dplyr group_by summarize groups
  363. #' @importFrom stringr str_split
  364. #'
  365. #' @return Return an updated CellChat object:
  366. #'
  367. #' `object@net$count` is a matrix: rows and columns are sources and targets respectively, and elements are the number of interactions between any two cell groups. USER can convert a matrix to a data frame using the function `reshape2::melt()`
  368. #'
  369. #' `object@net$weight` is also a matrix containing the interaction weights between any two cell groups
  370. #'
  371. #' `object@net$sum` is deprecated. Use `object@net$weight`
  372. #'
  373. #' @export
  374. #'
  375. aggregateNet <- function(object, sources.use = NULL, targets.use = NULL, signaling = NULL, pairLR.use = NULL, remove.isolate = TRUE, thresh = 0.05, return.object = TRUE) {
  376. net <- object@net
  377. if (is.null(sources.use) & is.null(targets.use) & is.null(signaling) & is.null(pairLR.use)) {
  378. prob <- net$prob
  379. pval <- net$pval
  380. pval[prob == 0] <- 1
  381. prob[pval >= thresh] <- 0
  382. net$count <- apply(prob > 0, c(1,2), sum)
  383. net$weight <- apply(prob, c(1,2), sum)
  384. net$weight[is.na(net$weight)] <- 0
  385. net$count[is.na(net$count)] <- 0
  386. } else {
  387. df.net <- subsetCommunication(object, slot.name = "net",
  388. sources.use = sources.use, targets.use = targets.use,
  389. signaling = signaling,
  390. pairLR.use = pairLR.use,
  391. thresh = thresh)
  392. df.net$source_target <- paste(df.net$source, df.net$target, sep = "|")
  393. df.net2 <- df.net %>% group_by(source_target) %>% summarize(count = n(), .groups = 'drop')
  394. df.net3 <- df.net %>% group_by(source_target) %>% summarize(prob = sum(prob), .groups = 'drop')
  395. df.net2$prob <- df.net3$prob
  396. a <- stringr::str_split(df.net2$source_target, "|", simplify = T)
  397. df.net2$source <- as.character(a[, 1])
  398. df.net2$target <- as.character(a[, 2])
  399. cells.level <- levels(object@idents)
  400. if (remove.isolate) {
  401. message("Isolate cell groups without any interactions are removed. To block it, set `remove.isolate = FALSE`")
  402. df.net2$source <- factor(df.net2$source, levels = cells.level[cells.level %in% unique(df.net2$source)])
  403. df.net2$target <- factor(df.net2$target, levels = cells.level[cells.level %in% unique(df.net2$target)])
  404. } else {
  405. df.net2$source <- factor(df.net2$source, levels = cells.level)
  406. df.net2$target <- factor(df.net2$target, levels = cells.level)
  407. }
  408. count <- tapply(df.net2[["count"]], list(df.net2[["source"]], df.net2[["target"]]), sum)
  409. prob <- tapply(df.net2[["prob"]], list(df.net2[["source"]], df.net2[["target"]]), sum)
  410. net$count <- count
  411. net$weight <- prob
  412. net$weight[is.na(net$weight)] <- 0
  413. net$count[is.na(net$count)] <- 0
  414. }
  415. if (return.object) {
  416. object@net <- net
  417. return(object)
  418. } else {
  419. return(net)
  420. }
  421. }
  422. #' Compute averaged expression values for each cell group
  423. #'
  424. #' @param object CellChat object
  425. #' @param features a char vector giving the used features. default use all features
  426. #' @param group.by cell group information; default is `object@idents` when input is a single object and `object@idents$joint` when input is a merged object; otherwise it should be one of the column names of the meta slot
  427. #' @param type methods for computing the average gene expression per cell group.
  428. #'
  429. #' By default = "triMean", defined as a weighted average of the distribution's median and its two quartiles (https://en.wikipedia.org/wiki/Trimean);
  430. #'
  431. #' When setting `type = "truncatedMean"`, a value should be assigned to 'trim'. See the function `base::mean`.
  432. #'
  433. #' @param trim the fraction (0 to 0.25) of observations to be trimmed from each end of x before the mean is computed.
  434. #' @param slot.name the data in the slot.name to use
  435. #' @param data.use a customed data matrix. Default: data.use = NULL and the expression matrix in the 'slot.name' is used
  436. #'
  437. #' @return Returns a matrix with genes as rows, cell groups as columns.
  438. #' @export
  439. #'
  440. computeAveExpr <- function(object, features = NULL, group.by = NULL, type = c("triMean", "truncatedMean", "median"), trim = NULL,
  441. slot.name = c("data.signaling", "data"), data.use = NULL) {
  442. type <- match.arg(type)
  443. slot.name <- match.arg(slot.name)
  444. FunMean <- switch(type,
  445. triMean = triMean,
  446. truncatedMean = function(x) mean(x, trim = trim, na.rm = TRUE),
  447. median = function(x) median(x, na.rm = TRUE))
  448. if (is.null(data.use)) {
  449. data.use <- slot(object, slot.name)
  450. }
  451. if (is.null(features)) {
  452. features.use <- row.names(data.use)
  453. } else {
  454. features.use <- intersect(features, row.names(data.use))
  455. }
  456. data.use <- data.use[features.use, , drop = FALSE]
  457. data.use <- as.matrix(data.use)
  458. if (is.null(group.by)) {
  459. labels <- object@idents
  460. if (!is.factor(labels)) {
  461. message("Use the joint cell labels from the merged CellChat object")
  462. labels <- object@idents$joint
  463. }
  464. } else {
  465. labels <- object@meta[[group.by]]
  466. }
  467. if (!is.factor(labels)) {
  468. labels <- factor(labels)
  469. }
  470. # compute the average expression per group
  471. data.use.avg <- aggregate(t(data.use), list(labels), FUN = FunMean)
  472. data.use.avg <- t(data.use.avg[,-1])
  473. rownames(data.use.avg) <- features.use
  474. colnames(data.use.avg) <- levels(labels)
  475. return(data.use.avg)
  476. }
  477. #' Compute the expression of complex in individual cells using geometric mean
  478. #' @param complex_input the complex_input from CellChatDB
  479. #' @param data.use data matrix (row are genes and columns are cells or cell groups)
  480. #' @param complex the names of complex
  481. #' @return
  482. #' @importFrom dplyr select starts_with
  483. #' @importFrom future nbrOfWorkers
  484. #' @importFrom future.apply future_sapply
  485. #' @importFrom pbapply pbsapply
  486. #' @export
  487. computeExpr_complex <- function(complex_input, data.use, complex) {
  488. Rsubunits <- complex_input[complex,] %>% dplyr::select(starts_with("subunit"))
  489. my.sapply <- ifelse(
  490. test = future::nbrOfWorkers() == 1,
  491. yes = sapply,
  492. no = future.apply::future_sapply
  493. )
  494. data.complex = my.sapply(
  495. X = 1:nrow(Rsubunits),
  496. FUN = function(x) {
  497. RsubunitsV <- unlist(Rsubunits[x,], use.names = F)
  498. RsubunitsV <- RsubunitsV[RsubunitsV != ""]
  499. return(geometricMean(data.use[RsubunitsV, , drop = FALSE]))
  500. }
  501. )
  502. data.complex <- t(data.complex)
  503. return(data.complex)
  504. }
  505. # Compute the average expression of complex per cell group using geometric mean
  506. # @param complex_input the complex_input from CellChatDB
  507. # @param data.use data matrix (rows are genes and columns are cells)
  508. # @param complex the names of complex
  509. # @param group a factor defining the cell groups
  510. # @param FunMean the function for computing mean expression per group
  511. # @return
  512. # @importFrom dplyr select starts_with
  513. # @importFrom future nbrOfWorkers
  514. # @importFrom future.apply future_sapply
  515. # @importFrom pbapply pbsapply
  516. # #' @export
  517. .computeExprGroup_complex <- function(complex_input, data.use, complex, group, FunMean) {
  518. Rsubunits <- complex_input[complex,] %>% dplyr::select(starts_with("subunit"))
  519. my.sapply <- ifelse(
  520. test = future::nbrOfWorkers() == 1,
  521. yes = pbapply::pbsapply,
  522. no = future.apply::future_sapply
  523. )
  524. data.complex = my.sapply(
  525. X = 1:nrow(Rsubunits),
  526. FUN = function(x) {
  527. RsubunitsV <- unlist(Rsubunits[x,], use.names = F)
  528. RsubunitsV <- RsubunitsV[RsubunitsV != ""]
  529. RsubunitsV <- intersect(RsubunitsV, rownames(data.use))
  530. if (length(RsubunitsV) > 1) {
  531. data.avg <- aggregate(t(data.use[RsubunitsV, ,drop = FALSE]), list(group), FUN = FunMean)
  532. data.avg <- t(data.avg[,-1])
  533. } else if (length(RsubunitsV) == 1) {
  534. data.avg <- aggregate(matrix(data.use[RsubunitsV,], ncol = 1), list(group), FUN = FunMean)
  535. data.avg <- t(data.avg[,-1])
  536. } else {
  537. data.avg = matrix(0, nrow = 1, ncol = length(unique(group)))
  538. }
  539. return(geometricMean(data.avg))
  540. }
  541. )
  542. data.complex <- t(data.complex)
  543. return(data.complex)
  544. }
  545. #' Compute the expression of ligands or receptors using geometric mean
  546. #' @param geneLR a char vector giving a set of ligands or receptors
  547. #' @param data.use data matrix (row are genes and columns are cells or cell groups)
  548. #' @param complex_input the complex_input from CellChatDB
  549. # #' @param group a factor defining the cell groups; If NULL, compute the expression of ligands or receptors in individual cells; otherwise, compute the average expression of ligands or receptors per cell group
  550. # #' @param FunMean the function for computing average expression per cell group
  551. #' @return
  552. #' @export
  553. computeExpr_LR <- function(geneLR, data.use, complex_input){
  554. nLR <- length(geneLR)
  555. numCluster <- ncol(data.use)
  556. index.singleL <- which(geneLR %in% rownames(data.use))
  557. dataL1avg <- data.use[geneLR[index.singleL],]
  558. dataLavg <- matrix(nrow = nLR, ncol = numCluster)
  559. dataLavg[index.singleL,] <- dataL1avg
  560. index.complexL <- setdiff(1:nLR, index.singleL)
  561. if (length(index.complexL) > 0) {
  562. complex <- geneLR[index.complexL]
  563. data.complex <- computeExpr_complex(complex_input, data.use, complex)
  564. dataLavg[index.complexL,] <- data.complex
  565. }
  566. return(dataLavg)
  567. }
  568. #' Modeling the effect of coreceptor on the ligand-receptor interaction
  569. #'
  570. #' @param data.use data matrix
  571. #' @param cofactor_input the cofactor_input from CellChatDB
  572. #' @param pairLRsig a data frame giving ligand-receptor interactions
  573. #' @param type when type == "A", computing expression of co-activation receptor; when type == "I", computing expression of co-inhibition receptor.
  574. #' @return
  575. #' @importFrom future nbrOfWorkers
  576. #' @importFrom future.apply future_sapply
  577. #' @importFrom pbapply pbsapply
  578. #' @export
  579. computeExpr_coreceptor <- function(cofactor_input, data.use, pairLRsig, type = c("A", "I")) {
  580. type <- match.arg(type)
  581. if (type == "A") {
  582. coreceptor.all = pairLRsig$co_A_receptor
  583. } else if (type == "I"){
  584. coreceptor.all = pairLRsig$co_I_receptor
  585. }
  586. index.coreceptor <- which(!is.na(coreceptor.all) & coreceptor.all != "")
  587. if (length(index.coreceptor) > 0) {
  588. my.sapply <- ifelse(
  589. test = future::nbrOfWorkers() == 1,
  590. yes = sapply,
  591. no = future.apply::future_sapply
  592. )
  593. coreceptor <- coreceptor.all[index.coreceptor]
  594. coreceptor.ind <- cofactor_input[coreceptor, grepl("cofactor" , colnames(cofactor_input) )]
  595. data.coreceptor.ind = my.sapply(
  596. X = 1:nrow(coreceptor.ind),
  597. FUN = function(x) {
  598. coreceptor.indV <- unlist(coreceptor.ind[x,], use.names = F)
  599. coreceptor.indV <- coreceptor.indV[coreceptor.indV != ""]
  600. coreceptor.indV <- intersect(coreceptor.indV, rownames(data.use))
  601. if (length(coreceptor.indV) == 1) {
  602. return(1 + data.use[coreceptor.indV, ])
  603. } else if (length(coreceptor.indV) > 1) {
  604. return(apply(1 + data.use[coreceptor.indV, ], 2, prod))
  605. } else {
  606. return(matrix(1, nrow = 1, ncol = ncol(data.use)))
  607. }
  608. }
  609. )
  610. data.coreceptor.ind <- t(data.coreceptor.ind)
  611. data.coreceptor <- matrix(1, nrow = length(coreceptor.all), ncol = ncol(data.use))
  612. data.coreceptor[index.coreceptor,] <- data.coreceptor.ind
  613. } else {
  614. data.coreceptor <- matrix(1, nrow = length(coreceptor.all), ncol = ncol(data.use))
  615. }
  616. return(data.coreceptor)
  617. }
  618. # Modeling the effect of coreceptor on the ligand-receptor interaction
  619. #
  620. # @param data.use data matrix
  621. # @param cofactor_input the cofactor_input from CellChatDB
  622. # @param pairLRsig a data frame giving ligand-receptor interactions
  623. # @param type when type == "A", computing expression of co-activation receptor; when type == "I", computing expression of co-inhibition receptor.
  624. # @param group a factor defining the cell groups
  625. # @param FunMean the function for computing mean expression per group
  626. # @return
  627. # @importFrom future nbrOfWorkers
  628. # @importFrom future.apply future_sapply
  629. # @importFrom pbapply pbsapply
  630. # #' @export
  631. .computeExprGroup_coreceptor <- function(cofactor_input, data.use, pairLRsig, type = c("A", "I"), group, FunMean) {
  632. type <- match.arg(type)
  633. if (type == "A") {
  634. coreceptor.all = pairLRsig$co_A_receptor
  635. } else if (type == "I"){
  636. coreceptor.all = pairLRsig$co_I_receptor
  637. }
  638. index.coreceptor <- which(!is.na(coreceptor.all) & coreceptor.all != "")
  639. if (length(index.coreceptor) > 0) {
  640. my.sapply <- ifelse(
  641. test = future::nbrOfWorkers() == 1,
  642. yes = pbapply::pbsapply,
  643. no = future.apply::future_sapply
  644. )
  645. coreceptor <- coreceptor.all[index.coreceptor]
  646. coreceptor.ind <- cofactor_input[coreceptor, grepl("cofactor" , colnames(cofactor_input) )]
  647. data.coreceptor.ind = my.sapply(
  648. X = 1:nrow(coreceptor.ind),
  649. FUN = function(x) {
  650. coreceptor.indV <- unlist(coreceptor.ind[x,], use.names = F)
  651. coreceptor.indV <- coreceptor.indV[coreceptor.indV != ""]
  652. coreceptor.indV <- intersect(coreceptor.indV, rownames(data.use))
  653. if (length(coreceptor.indV) > 1) {
  654. data.avg <- aggregate(t(data.use[coreceptor.indV,]), list(group), FUN = FunMean)
  655. data.avg <- t(data.avg[,-1])
  656. return(apply(1 + data.avg, 2, prod))
  657. # return(1 + apply(data.avg, 2, mean))
  658. } else if (length(coreceptor.indV) == 1) {
  659. data.avg <- aggregate(matrix(data.use[coreceptor.indV,], ncol = 1), list(group), FUN = FunMean)
  660. data.avg <- t(data.avg[,-1])
  661. return(1 + data.avg)
  662. } else {
  663. return(matrix(1, nrow = 1, ncol = length(unique(group))))
  664. }
  665. }
  666. )
  667. data.coreceptor.ind <- t(data.coreceptor.ind)
  668. data.coreceptor <- matrix(1, nrow = length(coreceptor.all), ncol = length(unique(group)))
  669. data.coreceptor[index.coreceptor,] <- data.coreceptor.ind
  670. } else {
  671. data.coreceptor <- matrix(1, nrow = length(coreceptor.all), ncol = length(unique(group)))
  672. }
  673. return(data.coreceptor)
  674. }
  675. #' Modeling the effect of agonist on the ligand-receptor interaction
  676. #' @param data.use data matrix
  677. #' @param cofactor_input the cofactor_input from CellChatDB
  678. #' @param pairLRsig the L-R interactions
  679. #' @param group a factor defining the cell groups
  680. #' @param index.agonist the index of agonist in the database
  681. #' @param Kh a parameter in Hill function
  682. #' @param FunMean the function for computing mean expression per group
  683. #' @param n Hill coefficient
  684. #' @return
  685. #' @export
  686. #' @importFrom stats aggregate
  687. computeExprGroup_agonist <- function(data.use, pairLRsig, cofactor_input, group, index.agonist, Kh, FunMean, n) {
  688. agonist <- pairLRsig$agonist[index.agonist]
  689. agonist.ind <- cofactor_input[agonist, grepl("cofactor" , colnames(cofactor_input))]
  690. agonist.indV <- unlist(agonist.ind, use.names = F)
  691. agonist.indV <- agonist.indV[agonist.indV != ""]
  692. agonist.indV <- intersect(agonist.indV, rownames(data.use))
  693. if (length(agonist.indV) == 1) {
  694. data.avg <- aggregate(matrix(data.use[agonist.indV,], ncol = 1), list(group), FUN = FunMean)
  695. data.avg <- t(data.avg[,-1])
  696. data.agonist <- 1 + data.avg^n/(Kh^n + data.avg^n)
  697. } else if (length(agonist.indV) > 1) {
  698. data.avg <- aggregate(t(data.use[agonist.indV,]), list(group), FUN = FunMean)
  699. data.avg <- t(data.avg[,-1])
  700. data.agonist <- apply(1 + data.avg^n/(Kh^n + data.avg^n), 2, prod)
  701. } else {
  702. data.agonist = matrix(1, nrow = 1, ncol = length(unique(group)))
  703. }
  704. return(data.agonist)
  705. }
  706. #' Modeling the effect of antagonist on the ligand-receptor interaction
  707. #'
  708. #' @param data.use data matrix
  709. #' @param cofactor_input the cofactor_input from CellChatDB
  710. #' @param pairLRsig the L-R interactions
  711. #' @param group a factor defining the cell groups
  712. #' @param index.antagonist the index of antagonist in the database
  713. #' @param Kh a parameter in Hill function
  714. #' @param n Hill coefficient
  715. #' @param FunMean the function for computing mean expression per group
  716. #' @return
  717. #' @export
  718. #' @importFrom stats aggregate
  719. computeExprGroup_antagonist <- function(data.use, pairLRsig, cofactor_input, group, index.antagonist, Kh, FunMean, n) {
  720. antagonist <- pairLRsig$antagonist[index.antagonist]
  721. antagonist.ind <- cofactor_input[antagonist, grepl( "cofactor" , colnames(cofactor_input) )]
  722. antagonist.indV <- unlist(antagonist.ind, use.names = F)
  723. antagonist.indV <- antagonist.indV[antagonist.indV != ""]
  724. antagonist.indV <- intersect(antagonist.indV, rownames(data.use))
  725. if (length(antagonist.indV) == 1) {
  726. data.avg <- aggregate(matrix(data.use[antagonist.indV,], ncol = 1), list(group), FUN = FunMean)
  727. data.avg <- t(data.avg[,-1])
  728. data.antagonist <- Kh^n/(Kh^n + data.avg^n)
  729. } else if (length(antagonist.indV) > 1) {
  730. data.avg <- aggregate(t(data.use[antagonist.indV,]), list(group), FUN = FunMean)
  731. data.avg <- t(data.avg[,-1])
  732. data.antagonist <- apply(Kh^n/(Kh^n + data.avg^n), 2, prod)
  733. } else {
  734. data.antagonist = matrix(1, nrow = 1, ncol = length(unique(group)))
  735. }
  736. return(data.antagonist)
  737. }
  738. #' Modeling the effect of agonist on the ligand-receptor interaction
  739. #' @param data.use data matrix
  740. #' @param cofactor_input the cofactor_input from CellChatDB
  741. #' @param pairLRsig the L-R interactions
  742. # #' @param group a factor defining the cell groups
  743. #' @param index.agonist the index of agonist in the database
  744. #' @param Kh a parameter in Hill function
  745. # #' @param FunMean the function for computing mean expression per group
  746. #' @param n Hill coefficient
  747. #' @return
  748. #' @export
  749. #' @importFrom stats aggregate
  750. computeExpr_agonist <- function(data.use, pairLRsig, cofactor_input, index.agonist, Kh, n) {
  751. agonist <- pairLRsig$agonist[index.agonist]
  752. agonist.ind <- cofactor_input[agonist, grepl("cofactor" , colnames(cofactor_input))]
  753. agonist.indV <- unlist(agonist.ind, use.names = F)
  754. agonist.indV <- agonist.indV[agonist.indV != ""]
  755. agonist.indV <- intersect(agonist.indV, rownames(data.use))
  756. if (length(agonist.indV) == 1) {
  757. # data.avg <- aggregate(matrix(data.use[agonist.indV,], ncol = 1), list(group), FUN = FunMean)
  758. # data.avg <- t(data.avg[,-1])
  759. data.avg <- data.use[agonist.indV,, drop = FALSE]
  760. data.agonist <- 1 + data.avg^n/(Kh^n + data.avg^n)
  761. } else if (length(agonist.indV) > 1) {
  762. # data.avg <- aggregate(t(data.use[agonist.indV,]), list(group), FUN = FunMean)
  763. # data.avg <- t(data.avg[,-1])
  764. data.avg <- data.use[agonist.indV,, drop = FALSE]
  765. data.agonist <- apply(1 + data.avg^n/(Kh^n + data.avg^n), 2, prod)
  766. } else {
  767. # data.agonist = matrix(1, nrow = 1, ncol = length(unique(group)))
  768. data.agonist = matrix(1, nrow = 1, ncol = ncol(data.use))
  769. }
  770. return(data.agonist)
  771. }
  772. #' Modeling the effect of antagonist on the ligand-receptor interaction
  773. #'
  774. #' @param data.use data matrix
  775. #' @param cofactor_input the cofactor_input from CellChatDB
  776. #' @param pairLRsig the L-R interactions
  777. # #' @param group a factor defining the cell groups
  778. #' @param index.antagonist the index of antagonist in the database
  779. #' @param Kh a parameter in Hill function
  780. #' @param n Hill coefficient
  781. # #' @param FunMean the function for computing mean expression per group
  782. #' @return
  783. #' @export
  784. #' @importFrom stats aggregate
  785. computeExpr_antagonist <- function(data.use, pairLRsig, cofactor_input, index.antagonist, Kh, n) {
  786. antagonist <- pairLRsig$antagonist[index.antagonist]
  787. antagonist.ind <- cofactor_input[antagonist, grepl( "cofactor" , colnames(cofactor_input) )]
  788. antagonist.indV <- unlist(antagonist.ind, use.names = F)
  789. antagonist.indV <- antagonist.indV[antagonist.indV != ""]
  790. antagonist.indV <- intersect(antagonist.indV, rownames(data.use))
  791. if (length(antagonist.indV) == 1) {
  792. # data.avg <- aggregate(matrix(data.use[antagonist.indV,], ncol = 1), list(group), FUN = FunMean)
  793. # data.avg <- t(data.avg[,-1])
  794. data.avg <- data.use[antagonist.indV,, drop = FALSE]
  795. data.antagonist <- Kh^n/(Kh^n + data.avg^n)
  796. } else if (length(antagonist.indV) > 1) {
  797. # data.avg <- aggregate(t(data.use[antagonist.indV,]), list(group), FUN = FunMean)
  798. # data.avg <- t(data.avg[,-1])
  799. data.avg <- data.use[antagonist.indV,, drop = FALSE]
  800. data.antagonist <- apply(Kh^n/(Kh^n + data.avg^n), 2, prod)
  801. } else {
  802. # data.antagonist = matrix(1, nrow = 1, ncol = length(unique(group)))
  803. data.antagonist = matrix(1, nrow = 1, ncol = ncol(data.use))
  804. }
  805. return(data.antagonist)
  806. }
  807. #' Compute the geometric mean
  808. #' @param x a numeric vector
  809. #' @param na.rm whether remove na
  810. #' @return
  811. #' @export
  812. geometricMean <- function(x,na.rm=TRUE){
  813. if (is.null(nrow(x))) {
  814. exp(mean(log(x),na.rm=na.rm))
  815. } else {
  816. exp(apply(log(x),2,mean,na.rm=na.rm))
  817. }
  818. }
  819. #' Compute the Tukey's trimean
  820. #' @param x a numeric vector
  821. #' @param na.rm whether remove na
  822. #' @return
  823. #' @importFrom collapse fquantile
  824. #' @export
  825. triMean <- function(x, na.rm = TRUE) {
  826. mean(collapse::fquantile(x, probs = c(0.25, 0.50, 0.50, 0.75), na.rm = na.rm))
  827. }
  828. #' Compute the average expression per cell group when the percent of expressing cells per cell group larger than a threshold
  829. #' @param x a numeric vector
  830. #' @param trim the percent of expressing cells per cell group to be considered as zero
  831. #' @param na.rm whether remove na
  832. #' @return
  833. #' @importFrom Matrix nnzero
  834. # #' @export
  835. thresholdedMean <- function(x, trim = 0.1, na.rm = TRUE) {
  836. percent <- Matrix::nnzero(x)/length(x)
  837. if (percent < trim) {
  838. return(0)
  839. } else {
  840. return(mean(x, na.rm = na.rm))
  841. }
  842. }
  843. #' Filter cell-cell communication if there are only few number of cells in certain cell groups or inconsistent cell-cell communication across samples
  844. #'
  845. #' @param object CellChat object
  846. #' @param min.cells The minmum number of cells required in each cell group for cell-cell communication
  847. #' @param min.samples The minmum number of samples required for consistent cell-cell communication across samples (that is an interaction present in at least `min.samples` samples) when mutiple samples/replicates/batches are merged as an input for CellChat analysis.
  848. #' @param rare.keep Whether to keep the interactions associated with the rare populations when min.samples >= 2. When a rare population is identified in the merged samples (say 15 cells in this rare population from two samples), it is likely to filter out the interactions associated with this rare population when setting min.samples >= 2. Setting `rare.keep = TRUE` to retain the identified interactions associated with this rare population.
  849. #' @param nonFilter.keep Whether to keep the non-filtered cell-cell communication in the CellChat object. This is useful for avoiding re-running `computeCommunProb` if you want to adjust the parameters when running `filterCommunication`.
  850. #' @return CellChat object with an updated slot net
  851. #' @export
  852. #'
  853. filterCommunication <- function(object, min.cells = 10, min.samples = NULL, rare.keep = FALSE, nonFilter.keep = FALSE) {
  854. net <- object@net
  855. if (nonFilter.keep == TRUE) {
  856. cat("The non-filtered cell-cell communication is stored in `object@net$prob.nonFilter` and `object@net$pval.nonFilter`. \n")
  857. object@net$prob.nonFilter <- net$prob
  858. object@net$pval.nonFilter <- net$pval
  859. }
  860. num.interaction0 <- sum(net$prob > 0)
  861. cell.excludes <- which(as.numeric(table(object@idents)) <= min.cells)
  862. if (length(cell.excludes) > 0) {
  863. cat("The cell-cell communication related with the following cell groups are excluded due to the few number of cells: ", toString(levels(object@idents)[cell.excludes]), "!",'\t')
  864. net$prob[cell.excludes,,] <- 0
  865. net$prob[,cell.excludes,] <- 0
  866. num.interaction1 <- sum(net$prob > 0)
  867. pct.dicrease <- scales::percent((num.interaction0-num.interaction1)/num.interaction0, accuracy = .1)
  868. cat(paste0(pct.dicrease, " interactions are removed!",'\n'))
  869. } else {
  870. num.interaction1 <- num.interaction0
  871. }
  872. sample.info <- object@meta$samples
  873. sample.id <- levels(sample.info)
  874. if (is.null(min.samples)) {
  875. min.samples <- 1
  876. } else if (min.samples > length(sample.id)) {
  877. stop(paste0("There are only ", length(sample.id), " samples in the data. Please change the value of `min.samples`! "))
  878. }
  879. if (length(sample.id) >= 2 & min.samples >= 2) {
  880. if (object@options$parameter$raw.use == TRUE) {
  881. data <- as.matrix([email hidden])
  882. } else {
  883. if ("data.smooth" %in% methods::slotNames(object) == FALSE) {
  884. stop("`[email hidden]` is missing. Please update the CellChat object via `updateCellChat`! \n")
  885. }
  886. data <- as.matrix([email hidden])
  887. }
  888. data.use <- data/max(data)
  889. group <- object@idents
  890. type <- object@options$parameter$type.mean
  891. trim <- object@options$parameter$trim
  892. FunMean <- switch(type,
  893. triMean = triMean,
  894. truncatedMean = function(x) mean(x, trim = trim, na.rm = TRUE),
  895. thresholdedMean = function(x) thresholdedMean(x, trim = trim, na.rm = TRUE),
  896. median = function(x) median(x, na.rm = TRUE))
  897. LR <- dimnames(net$prob)[[3]]
  898. idx.nonzero <- which(apply(net$prob, 3, sum) != 0)
  899. LR.nonzero <- LR[idx.nonzero] # only examine the L-R pairs with nonzero communication probabilities.
  900. interaction_input <- object@DB$interaction
  901. complex_input <- object@DB$complex
  902. geneIfo <- object@DB$geneInfo
  903. idx <- match(LR.nonzero, interaction_input$interaction_name)
  904. geneL <- as.character(interaction_input$ligand[idx])
  905. geneR <- as.character(interaction_input$receptor[idx])
  906. geneLR <- c(unique(geneL), unique(geneR))
  907. geneLR <- extractGeneSubset(geneLR, complex_input, geneIfo)
  908. data.use <- data.use[rownames(data.use) %in% geneLR, ]
  909. score.LR <- array(0, dim = c(nlevels(group),nlevels(group),length(LR.nonzero), length(sample.id)))
  910. LR.nonzero.all <- c()
  911. cell.excludes.sample <- c()
  912. for (i in 1:length(sample.id)) {
  913. cell.use <- which(sample.info == sample.id[i])
  914. group.use <- group[cell.use]
  915. group.use <- droplevels(group.use)
  916. # get the rare populations with few cells in each sample
  917. cell.excludes.sample.i <- which(as.numeric(table(object@idents[cell.use])) <= min.cells)
  918. cell.excludes.sample <- c(cell.excludes.sample, cell.excludes.sample.i)
  919. # compute average expression per cell group
  920. data.use.i <- data.use[, cell.use]
  921. data.use.avg <- aggregate(t(data.use.i), list(group.use), FUN = FunMean)
  922. data.use.avg <- t(data.use.avg[,-1])
  923. group.exist <- which(levels(group) %in% unique(group.use))
  924. if (length(group.exist) < nlevels(group)) {
  925. data.use.avg.temp <- matrix(0, nrow = nrow(data.use), ncol = nlevels(group))
  926. data.use.avg.temp[ , group.exist] <- data.use.avg
  927. rownames(data.use.avg.temp) <- rownames(data.use.avg)
  928. data.use.avg <- data.use.avg.temp
  929. }
  930. colnames(data.use.avg) <- levels(group)
  931. # compute the average expression of ligand or receptor in each cell group
  932. dataLavg <- computeExpr_LR(geneL, data.use.avg, complex_input)
  933. dataRavg <- computeExpr_LR(geneR, data.use.avg, complex_input)
  934. # compute the interaction scores for each ligand-receptor pair based on their expression
  935. for (jj in 1:length(LR.nonzero)) { # It is not good to use parallel here because it will change the order of LR
  936. score.LR[,,jj,i] <- Matrix::crossprod(matrix(dataLavg[jj, ], nrow = 1), matrix(dataRavg[jj, ], nrow = 1))
  937. }
  938. if (length(cell.excludes.sample.i) > 0) {
  939. cat(paste0("The number of cells of the following cell groups in ", sample.id[i], " sample are less than ", min.cells, " cells: ",toString(levels(object@idents)[cell.excludes.sample.i]), "!",'\n'))
  940. score.LR[cell.excludes.sample.i, , , i] <- 0
  941. score.LR[ ,cell.excludes.sample.i, , i] <- 0
  942. }
  943. #LR.nonzero.all <- c(LR.nonzero.all, LR.nonzero[apply(score.LR[ , , , i], 3, sum) != 0])
  944. }
  945. #LR.nonzero.jointOnly <- setdiff(LR.nonzero, unique(LR.nonzero.all))
  946. # get the excluded cell groups that are not observed in the merged data, which is very possible for rare populations
  947. cell.excludes.sample <- unique(cell.excludes.sample)
  948. if (length(cell.excludes.sample) > 0) {
  949. cell.excludes.sample <- setdiff(cell.excludes.sample, cell.excludes)
  950. }
  951. score.LR[score.LR > 0] <- 1 # binarize the interaction score
  952. score.LR.consitent <- array(0, dim = c(nlevels(group),nlevels(group),length(LR.nonzero)))
  953. LR.inconsitent <- c()
  954. for (jj in 1:length(LR.nonzero)) {
  955. score.LR.sum <- apply(score.LR[ , , jj, ], c(1,2), sum) # elements 2 and 1 means consistent and inconsistent interactions across samples, respectively.
  956. # set communication probability to be zero for inconsistent interactions across samples
  957. if (sum((score.LR.sum > 0) * (score.LR.sum < min.samples)) > 0) {
  958. #LR.inconsitent <- c(LR.inconsitent, LR.nonzero[jj])
  959. score.LR.consitent <- (score.LR.sum >= min.samples) * 1
  960. if (rare.keep == TRUE & length(cell.excludes.sample) > 0) {
  961. score.LR.consitent[cell.excludes.sample, ] <- 1
  962. score.LR.consitent[ ,cell.excludes.sample] <- 1
  963. }
  964. net$prob[ , , LR.nonzero[jj]] <- net$prob[ , , LR.nonzero[jj]] * score.LR.consitent
  965. }
  966. }
  967. num.interaction2 <- sum(net$prob > 0)
  968. pct.dicrease <- scales::percent((num.interaction1-num.interaction2)/num.interaction1, accuracy = .1)
  969. cat(paste0(pct.dicrease, " interactions are removed due to their inconsistence across ", min.samples, " samples!",'\n'))
  970. }
  971. object@net <- net
  972. return(object)
  973. }
  974. #' Identify all the significant interactions (L-R pairs) from some cell groups to other cell groups
  975. #'
  976. #' @param object CellChat object
  977. #' @param from a vector giving the index or the name of source cell groups
  978. #' @param to a corresponding vector giving the index or the name of target cell groups. Note: The length of 'from' and 'to' must be the same, giving the corresponding pair of cell groups for communication.
  979. #' @param bidirection whether show the bidirectional communication, i.e., both 'from'->'to' and 'to'->'from'.
  980. #' @param pair.only whether only return ligand-receptor pairs without pathway names and communication strength
  981. #' @param pairLR.use0 ligand-receptor pairs to use; default is all the significant interactions
  982. #' @param thresh threshold of the p-value for determining significant interaction
  983. #'
  984. #' @return
  985. #' @export
  986. #'
  987. identifyEnrichedInteractions <- function(object, from, to, bidirection = FALSE, pair.only = TRUE, pairLR.use0 = NULL, thresh = 0.05){
  988. pairwiseLR <- object@net$pairwiseRank
  989. if (is.null(pairwiseLR)) {
  990. stop("The interactions between pairwise cell groups have not been extracted!
  991. Please first run `object <- rankNetPairwise(object)`")
  992. }
  993. group.names.all <- names(pairwiseLR)
  994. if (!is.numeric(from)) {
  995. from <- match(from, group.names.all)
  996. if (sum(is.na(from)) > 0) {
  997. message("Some input cell group names in 'from' do not exist!")
  998. from <- from[!is.na(from)]
  999. }
  1000. }
  1001. if (!is.numeric(to)) {
  1002. to <- match(to, group.names.all)
  1003. if (sum(is.na(to)) > 0) {
  1004. message("Some input cell group names in 'to' do not exist!")
  1005. to <- to[!is.na(to)]
  1006. }
  1007. }
  1008. if (length(from) != length(to)) {
  1009. stop("The length of 'from' and 'to' must be the same!")
  1010. }
  1011. if (bidirection) {
  1012. from2 <- c(from, to)
  1013. to <- c(to, from)
  1014. from <- from2
  1015. }
  1016. if (is.null(pairLR.use0)) {
  1017. k <- 0
  1018. pairLR.use0 <- list()
  1019. for (i in 1:length(from)){
  1020. pairwiseLR_ij <- pairwiseLR[[from[i]]][[to[i]]]
  1021. idx <- pairwiseLR_ij$pval < thresh
  1022. if (length(idx) > 0) {
  1023. k <- k +1
  1024. pairLR.use0[[k]] <- pairwiseLR_ij[idx,]
  1025. }
  1026. }
  1027. pairLR.use0 <- do.call(rbind, pairLR.use0)
  1028. }
  1029. k <- 0
  1030. pval <- matrix(nrow = length(rownames(pairLR.use0)), ncol = length(from))
  1031. prob <- pval
  1032. group.names <- c()
  1033. for (i in 1:length(from)) {
  1034. k <- k+1
  1035. pairwiseLR_ij <- pairwiseLR[[from[i]]][[to[i]]]
  1036. pairwiseLR_ij <- pairwiseLR_ij[rownames(pairLR.use0),]
  1037. pval_ij <- pairwiseLR_ij$pval
  1038. prob_ij <- pairwiseLR_ij$prob
  1039. pval_ij[pval_ij > 0.05] = 1
  1040. pval_ij[pval_ij > 0.01 & pval_ij <= 0.05] = 2
  1041. pval_ij[pval_ij <= 0.01] = 3
  1042. prob_ij[pval_ij ==1] <- 0
  1043. pval[,k] <- pval_ij
  1044. prob[,k] <- prob_ij
  1045. group.names <- c(group.names, paste(group.names.all[from[i]], group.names.all[to[i]], sep = " - "))
  1046. }
  1047. prob[which(prob == 0)] <- NA
  1048. # remove rows that are entirely NA
  1049. pval <- pval[rowSums(is.na(prob)) != ncol(prob), ,drop = FALSE]
  1050. pairLR.use0 <- pairLR.use0[rowSums(is.na(prob)) != ncol(prob), ,drop = FALSE]
  1051. prob <- prob[rowSums(is.na(prob)) != ncol(prob), ,drop = FALSE]
  1052. if (pair.only) {
  1053. pairLR.use0 <- dplyr::select(pairLR.use0, ligand, receptor)
  1054. }
  1055. return(pairLR.use0)
  1056. }
  1057. #' Compute the region distance based on the spatial locations of each splot/cell of the spatial transcriptomics
  1058. #'
  1059. #' @param coordinates a data matrix in which each row gives the spatial locations/coordinates of each cell/spot
  1060. #' @param meta a data frame including at least two columns named `group` and `samples`. `meta$group` is a factor vector defining the regions/labels of each cell/spot. `meta$samples` is a factor vector defining the sample labels of each dataset.
  1061. #' @param interaction.range The maximum interaction/diffusion range of ligands. This hard threshold is used to filter out the connections between spatially distant regions
  1062. #' @param ratio a numerical vector giving the conversion factor when converting spatial coordinates from Pixels or other units to Micrometers (i.e.,Microns).
  1063. #'
  1064. #' For example, setting `ratio = 0.18` indicates that 1 pixel equals 0.18um in the coordinates.
  1065. #' For 10X visium, it is the ratio of the theoretical spot size (i.e., 65um) over the number of pixels that span the diameter of a theoretical spot size in the full-resolution image (i.e., 'spot.size.fullres' in the 'scalefactors_json.json' file).
  1066. #' @param tol a numerical vector giving the tolerance factor to increase the robustness when comparing the center-to-center distance against the `interaction.range`. This can be the half value of cell/spot size in the unit of um.
  1067. #'
  1068. #' For example, for 10X visium, `tol` can be set as `65/2`; for slide-seq, `tol` can be set as `10/2`.
  1069. #' If the cell/spot size is not known, we provide a function `computeCellDistance` to compute the center-to-center distance. `tol` can be the the half value of the minimum center-to-center distance.
  1070. #' @param k.min the minimum number of interacting cell pairs required for defining adjacent cell groups
  1071. #' @param contact.dependent Whether determining spatially proximal cell groups based on either the contact.range or the k-nearest neighbors (knn). By default `contact.dependent = TRUE` when inferring contact-dependent and juxtacrine signaling (including ECM-Receptor and Cell-Cell Contact signaling classified in CellChatDB$interaction$annotation).
  1072. #' If only focusing on `Secreted Signaling`, the `contact.dependent` will be automatically set as FALSE except for `contact.dependent.forced = TRUE`.
  1073. #' @param contact.range The interaction range (Unit: microns) to restrict the contact-dependent signaling.
  1074. #' For spatial transcriptomics in a single-cell resolution, `contact.range` is approximately equal to the estimated cell diameter (i.e., the cell center-to-center distance), which means that contact-dependent and juxtacrine signaling can only happens when the two cells are contact to each other.
  1075. #'
  1076. #' Typically, `contact.range = 10`, which is a typical human cell size. However, for low-resolution spatial data such as 10X visium, it should be the cell center-to-center distance (i.e., `contact.range = 100` for visium data). The function `computeCellDistance` can compute the center-to-center distance.
  1077. #'
  1078. #' @param contact.knn.k Number of neighbors to restrict the contact-dependent signaling within the neatest neighbors. By default, CellChat uses `contact.range` to restrict the contact-dependent signaling; however, users can also provide a value of `contact.knn.k`, in order to determine spatially proximal cell groups based on the k-nearest neighbors (knn).
  1079. #' For 10X visium, contact.knn.k = 6. For other spatial technologies, this value may be hard to determine because the sequenced cells/spots are usually not regularly arranged.
  1080. #' @param do.symmetric Whether converting the adjacent matrix into symmetric one when determining spatially proximal cell groups. Default is TRUE, indicating that if adj(i,j) or adj(j,i) is zero, then both are zeros.
  1081. #'
  1082. #' @importFrom BiocNeighbors queryKNN AnnoyParam
  1083. #' @return A list including a square matrix giving the pairwise region distances and an adjacent matrix indicating physically contacting cell groups based on either the contact.range or the k-nearest neighbors
  1084. #'
  1085. #' @export
  1086. computeRegionDistance <- function(coordinates, meta,
  1087. interaction.range = NULL, ratio = NULL, tol = NULL, k.min = 10,
  1088. contact.dependent = TRUE, contact.range = NULL, contact.knn.k = NULL, do.symmetric = TRUE
  1089. ) {
  1090. trim <- 0.1
  1091. FunMean <- function(x) mean(x, trim = trim, na.rm = TRUE) # This is used for computing the average distance between two cell groups
  1092. group <- meta$group
  1093. numCluster <- nlevels(group)
  1094. level.use <- levels(group)
  1095. level.use <- level.use[level.use %in% unique(group)]
  1096. samples <- meta$samples
  1097. samples.use <- levels(samples)
  1098. d.spatial <- array(NaN, dim = c(numCluster,numCluster,length(samples.use)))
  1099. adj.spatial <- array(0, dim = c(numCluster,numCluster,length(samples.use)))
  1100. adj.contact <- array(0, dim = c(numCluster,numCluster,length(samples.use)))
  1101. adj.contact.knn <- array(0, dim = c(numCluster,numCluster,length(samples.use)))
  1102. if (contact.dependent == TRUE & !is.null(contact.knn.k)) {
  1103. ## find the k-nearest neighbors for each single cell
  1104. # my.knn <- FNN::get.knn(coordinates, k = contact.knn.k)
  1105. # nn.ranked <- my.knn$nn.index # this is a matrix with the size of nCell * contact.knn.k
  1106. nn.ranked <- matrix(NA, nrow = nrow(coordinates), ncol = contact.knn.k)
  1107. for (k in 1:length(samples.use)) {
  1108. idx.k <- which(samples == samples.use[k])
  1109. my.knn <- suppressWarnings(BiocNeighbors::findKNN(coordinates[idx.k, ], k = contact.knn.k, BNPARAM = BiocNeighbors::AnnoyParam(), get.index = TRUE))
  1110. nn.ranked[idx.k, ] <- my.knn$index # this is a matrix with the size of nCell * contact.knn.k
  1111. }
  1112. k.min.contact <- k.min
  1113. } else {
  1114. nn.ranked <- matrix(1, nrow = nrow(coordinates), ncol = 1)
  1115. k.min.contact <- -1 # this produces adj.contact.knn with all elements being 1
  1116. }
  1117. if (contact.dependent == TRUE) {
  1118. if (is.null(contact.range) & is.null(contact.knn.k)) {
  1119. stop("Please check the documentation of `computeCommunProb` and provide the value of either `contact.range` or `contact.knn.k`")
  1120. }
  1121. } else {
  1122. contact.range <- 10000 # this produces adj.contact with all elements being 1
  1123. }
  1124. for (k in 1:length(samples.use)) {
  1125. idx.k <- samples == samples.use[k]
  1126. for (i in 1:numCluster) {
  1127. for (j in 1:numCluster) {
  1128. idx.i <- which((group == level.use[i]) & idx.k)
  1129. idx.j <- which((group == level.use[j]) & idx.k)
  1130. if (length(idx.i) == 0 | length(idx.j) == 0) {
  1131. next # if one cell group is missing in one sample, just goes to next loop
  1132. }
  1133. data.spatial.i <- coordinates[idx.i, , drop = FALSE]
  1134. data.spatial.j <- coordinates[idx.j, , drop = FALSE]
  1135. # for each point in the i-th cell group, find its 1-nearest neighbor in the j-th cell group
  1136. #qout <- suppressWarnings(BiocNeighbors::queryKNN(data.spatial.j, data.spatial.i, k = 1, BNPARAM = BiocNeighbors::KmknnParam(), get.index = TRUE))
  1137. qout <- suppressWarnings(BiocNeighbors::queryKNN(data.spatial.j, data.spatial.i, k = 1, BNPARAM = BiocNeighbors::AnnoyParam(), get.index = TRUE))
  1138. # qout$index is an one column matrix with length being `length(idx.i)`, which is the index of the 1-nearest neighbor in the j-th cell group defined by `idx.j`
  1139. # qout$distance is an one column matrix with length being `length(idx.i)`, which is the distance to the 1-nearest neighbor in the j-th cell group defined by `idx.j`
  1140. # conver the calculated distance into the distance in micrometers
  1141. qout$distance <- qout$distance*ratio[k]
  1142. # long-range distance
  1143. idx <- qout$distance - interaction.range < tol[k]
  1144. adj.spatial[i,j,k] <- (length(unique(qout$index[idx])) >= k.min) * 1
  1145. # short-range distance based on contact.range
  1146. idx2 <- qout$distance - contact.range < tol[k]
  1147. adj.contact[i,j,k] <- (length(unique(qout$index[idx2])) >= k.min) * 1
  1148. # short-range distance based on knn
  1149. knn.i <- unique(as.vector(nn.ranked[idx.i, ]))
  1150. #adj.contact.knn[i,j,k] <- (length(intersect(knn.i, idx.j)) >= k.min.contact) * 1
  1151. adj.contact.knn[i,j,k] <- (length(intersect(knn.i, unique(qout$index[idx]))) >= k.min.contact) * 1 # knn within the long-range distance
  1152. # computing the average distance between two cell groups
  1153. d.spatial[i,j,k] <- FunMean(qout$distance) # since distances are positive values, different ways for computing the mean have little effects.
  1154. }
  1155. }
  1156. }
  1157. # merged spatial information from different samples
  1158. d.spatial <- apply(d.spatial, c(1,2), function(x) mean(x, na.rm = TRUE))
  1159. adj.spatial <- apply(adj.spatial, c(1,2), mean)
  1160. adj.contact <- apply(adj.contact, c(1,2), mean)
  1161. adj.contact.knn <- apply(adj.contact.knn, c(1,2), mean)
  1162. # for multi-samples analysis, the following is needed
  1163. adj.spatial[adj.spatial > 0] <- 1
  1164. adj.contact[adj.contact > 0] <- 1
  1165. adj.contact.knn[adj.contact.knn > 0] <- 1
  1166. # make these adjacent matrix as symmetric
  1167. if (do.symmetric) {
  1168. adj.spatial <- adj.spatial * t(adj.spatial) # if one is zero, then both are zeros.
  1169. adj.contact <- adj.contact * t(adj.contact) # if one is zero, then both are zeros.
  1170. adj.contact.knn <- adj.contact.knn * t(adj.contact.knn) # if one is zero, then both are zeros.
  1171. }
  1172. d.spatial <- (d.spatial + t(d.spatial))/2
  1173. # filter out the spatially distant cell groups
  1174. adj.spatial[adj.spatial == 0] <- NaN
  1175. d.spatial <- d.spatial * adj.spatial
  1176. rownames(d.spatial) <- levels(group); colnames(d.spatial) <- levels(group)
  1177. if (length(contact.knn.k) > 0) {
  1178. adj.contact = adj.contact.knn
  1179. }
  1180. res <- list(d.spatial = d.spatial, adj.contact = adj.contact)
  1181. return(res)
  1182. }
  1183. #' Compute cell-cell distance based on the spatial coordinates
  1184. #'
  1185. #' @param coordinates a data matrix in which each row gives the spatial locations/coordinates of each cell/spot
  1186. #' @param interaction.range The maximum interaction/diffusion range of ligands. This hard threshold is used to filter out the connections between spatially distant cells
  1187. #' @param ratio The conversion factor when converting spatial coordinates from Pixels or other units to Micrometers (i.e.,Microns).
  1188. #'
  1189. #' For example, setting `ratio = 0.18` indicates that 1 pixel equals 0.18um in the coordinates.
  1190. #' For 10X visium, it is the ratio of the theoretical spot size (i.e., 65um) over the number of pixels that span the diameter of a theoretical spot size in the full-resolution image (i.e., 'spot.size.fullres' in the 'scalefactors_json.json' file).
  1191. #' @param tol The tolerance factor to increase the robustness when comparing the center-to-center distance against the `interaction.range`. This can be the half value of cell/spot size in the unit of um.
  1192. #'
  1193. #' For example, for 10X visium, `tol` can be set as `65/2`; for slide-seq, `tol` can be set as `10/2`.
  1194. #' If the cell/spot size is not known, we provide a function `computeCellDistance` to compute the center-to-center distance. `tol` can be the the half value of the minimum center-to-center distance.
  1195. #'
  1196. #' @return an object of class "dist" giving the pairwise cell-cell distance
  1197. #' @export
  1198. #'
  1199. computeCellDistance <- function(coordinates, interaction.range = NULL, ratio = NULL, tol = NULL){
  1200. if (ncol(coordinates) == 2) {
  1201. colnames(coordinates) <- c("x_cent","y_cent")
  1202. } else {
  1203. stop("Please check the input 'coordinates' and make sure it is a two column matrix.")
  1204. }
  1205. d.spatial <- collapse::fdist(coordinates)
  1206. if (!is.null(ratio)) {
  1207. d.spatial <- d.spatial*ratio
  1208. }
  1209. if(!is.null(interaction.range) & !is.null(tol)){
  1210. message("\n Apply a predefined spatial distance threshold based on the interaction length...")
  1211. d.spatial[d.spatial > (interaction.range + tol)] <- NaN
  1212. }
  1213. return(d.spatial)
  1214. }

modeling.R at commit 75253cd, under GPL-3.0 · at the source

Overview

Authors: He Huang1, Emerson Daniele2, Wing Chung Jessie Lam1, Teodora Tockovska1, Daniela Lozano Casasbuenas1,2, Hathairat Chanphao3, Bebhinn Treanor3,4,5, Maryam Faiz1,2, Scott A Yuzwa1
ORCID iDs: Scott A Yuzwa
  1. Department of Laboratory Medicine and Pathobiology, Temerty Faculty of Medicine, University of Toronto, 1 King’s College Circle, Toronto, ON M5S 1A8, Canada
  2. Division of Anatomy, Department of Surgery, Temerty Faculty of Medicine, University of Toronto, 1 King’s College Circle, Toronto, ON M5S 1A8, Canada
  3. Department of Biological Sciences, University of Toronto Scarborough, 1265 Military Trail, Toronto, ON M1C 1A4, Canada
  4. Department of Immunology, University of Toronto, 1 King’s College Circle, Toronto, ON M5S 1A8, Canada
  5. Department of Cell and Systems Biology, University of Toronto, 24 Harbord Street, Toronto, ON M5S 3G5, Canada
Journal: Stem cell reports, volume 21, issue 6, article 102922
Dates: received 15 September 2025; accepted 11 April 2026; published online 14 May 2026; in print June 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1016/j.stemcr.2026.102922 · PMID 42140198 · PMCID PMC13261890 · OpenAlex W4413975245
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), mouse (organism), stroke (population)
Methods: Smoothing, state filtering, decompositions, Evoked potentials, fMRI & imaging, Statistics
Keywords: neural stem cells, proliferation, spatial transcriptomics, galectin-9, Lgals9
MeSH: Ischemic Stroke*, Neural Stem Cells*, Signal Transduction*, Transcriptome*, Animals, Cell Communication, Cell Proliferation, Galectins, Gene Expression Profiling, Mice, Spatial Transcriptomics, Stem Cell Niche (* major topic)
Topic: Neuroinflammation and Neurodegeneration Mechanisms (Neurology, Neuroscience), according to OpenAlex
Funding: Canada Foundation for Innovation; Ontario Research Foundation; University of Toronto; Canadian Institutes of Health Research (PJT-175137)
Citations: not cited yet (Europe PMC); 66 references in the paper
Research resources: rabbit anti-TLR4 RRID:AB_10638446, mouse anti-FOXJ1 RRID:AB_1548836, chicken anti-GFAP RRID:AB_177521, goat anti-mouse galectin-9 RRID:AB_2137240, rabbit anti-SOX2 RRID:AB_2194037, RRID:AB_2336933, Cy™3 Streptavidin RRID:AB_2337244, Donkey serum RRID:AB_2337258, RRID:AB_2340375, RRID:AB_2340813, RRID:AB_2492288, biotin anti-mouse CD366 (Tim-3) RRID:AB_2571936, rabbit anti-Iba1/AIF-1 RRID:AB_2820254, goat anti-SOX2 RRID:AB_355110, mouse anti-Ki-67 RRID:AB_393778, rabbit anti-doublecortin (DCX) RRID:AB_561007, mouse anti-GFAP RRID:AB_561049, C57BL/6NCrl inbred mouse RRID:IMSR_CRL:027

Abstract

The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.

Repository

Its files are read in the Code ↔ Paper reader above, with 2 matches between paragraphs and lines of code.

jinworks/CellChat

License: GPL-3.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 75253cd0c9e68410e6e721a6d3a0419a1d7e358f, 4 March 2026
Languages: R (19), C++ (2)
Size: 178 files, 21 scripts
Software Heritage: not archived
Found in: the resources table
Holds: README, license file, environment (DESCRIPTION), documentation, 9 notebooks
Not found: CITATION.cff, tests, continuous integration
Tools: tidyverse (9 files), patchwork (6 files), Seurat (4 files), ComplexHeatmap (3 files), cowplot (3 files), ggplot2 (3 files), SingleCellExperiment (3 files), igraph (2 files), reshape2 (2 files), reticulate (2 files), circlize (1 file), Plotly (1 file), UMAP (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
23 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 21 scripts, each with its path and the digest of its content;
  • 2 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Code and data availability statement

The paper has a code and data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

  • no repository, dataset or request procedure was recognized in it

Read it in the paper: doi.org/10.1016/j.stemcr.2026.102922.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 9 authors, 5 keywords, 12 MeSH terms, 4 funders, 64 references, 18 RRIDs.

Cite

This paper

Huang, H., Daniele, E., Lam, W. C. J., Tockovska, T., Lozano Casasbuenas, D., Chanphao, H., Treanor, B., Faiz, M., & Yuzwa, S. A. (2026). Spatially resolved transcriptomics identifies intercellular signaling post-ischemic stroke that controls neural stem cell proliferation. Stem cell reports, 21(6), 102922. https://doi.org/10.1016/j.stemcr.2026.102922

BibTeX

@article{huang2026spatially,
author = {Huang, He and Daniele, Emerson and Lam, Wing Chung Jessie and Tockovska, Teodora and Lozano Casasbuenas, Daniela and Chanphao, Hathairat and Treanor, Bebhinn and Faiz, Maryam and Yuzwa, Scott A},
title = {{Spatially resolved transcriptomics identifies intercellular signaling post-ischemic stroke that controls neural stem cell proliferation}},
journal = {Stem cell reports},
year = {2026},
month = may,
volume = {21},
number = {6},
pages = {102922},
publisher = {Elsevier},
issn = {2213-6711},
doi = {10.1016/j.stemcr.2026.102922},
url = {https://doi.org/10.1016/j.stemcr.2026.102922},
pmid = {42140198},
pmcid = {PMC13261890}
}

RIS

TY - JOUR
AU - Huang, He
AU - Daniele, Emerson
AU - Lam, Wing Chung Jessie
AU - Tockovska, Teodora
AU - Lozano Casasbuenas, Daniela
AU - Chanphao, Hathairat
AU - Treanor, Bebhinn
AU - Faiz, Maryam
AU - Yuzwa, Scott A
TI - Spatially resolved transcriptomics identifies intercellular signaling post-ischemic stroke that controls neural stem cell proliferation
T2 - Stem cell reports
J2 - Stem Cell Reports
PY - 2026
DA - 2026/05/14
VL - 21
IS - 6
SP - 102922
SN - 2213-6711
PB - Elsevier
DO - 10.1016/j.stemcr.2026.102922
UR - https://doi.org/10.1016/j.stemcr.2026.102922
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.stemcr.2026.102922",
"type": "article-journal",
"title": "Spatially resolved transcriptomics identifies intercellular signaling post-ischemic stroke that controls neural stem cell proliferation",
"container-title": "Stem cell reports",
"author": [
{
"family": "Huang",
"given": "He"
},
{
"family": "Daniele",
"given": "Emerson"
},
{
"family": "Lam",
"given": "Wing Chung Jessie"
},
{
"family": "Tockovska",
"given": "Teodora"
},
{
"family": "Lozano Casasbuenas",
"given": "Daniela"
},
{
"family": "Chanphao",
"given": "Hathairat"
},
{
"family": "Treanor",
"given": "Bebhinn"
},
{
"family": "Faiz",
"given": "Maryam"
},
{
"family": "Yuzwa",
"given": "Scott A"
}
],
"container-title-short": "Stem Cell Reports",
"volume": "21",
"issue": "6",
"page": "102922",
"DOI": "10.1016/j.stemcr.2026.102922",
"PMID": "42140198",
"PMCID": "PMC13261890",
"ISSN": "2213-6711",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.stemcr.2026.102922",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
14
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1016/j.stemcr.2026.102997 [code]
BDNF regulates pituitary stem cell engagement toward precursor state.
Journal: Stem cell reports
In common: SingleCellExperiment, reticulate, UMAP, 10 other tools, mouse, 2 references
[2] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: SingleCellExperiment, UMAP, igraph, 9 other tools, genetics / omics, 3 references
[3] doi:10.1038/s41467-026-76232-w [code]
Th17 effector cytokines induce shared and distinct microglial and endothelial cell responses in a mouse model for post-streptococcal encephalitis.
Journal: Nature communications
In common: SingleCellExperiment, reticulate, UMAP, 8 other tools, genetics / omics, mouse, 3 references
[4] doi:10.1038/s41380-026-03585-5 [code]
Multiomics analysis identifies VPA-induced changes in neural progenitor cells, ventricular-like regions, and cellular microenvironment in dorsal forebrain organoids.
Journal: Molecular psychiatry
In common: reticulate, UMAP, igraph, 8 other tools, genetics / omics, mouse, 2 references
[5] doi:10.3390/ijms27104466 [code]
Uncovering the Key Circuit FOSL2/FOS/EGR3/EGR1, Contributing to the Hyperexcitability of Excitatory Neurons in the Epileptic Temporal Cortex and Hippocampus.
Journal: International journal of molecular sciences
In common: reticulate, UMAP, igraph, 9 other tools, genetics / omics
[6] doi:10.1073/pnas.2523130123 [code]
FABP7 controls radial glial scaffold stability during human cortical development.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: reticulate, UMAP, igraph, 9 other tools, mouse
[7] doi:10.1002/imt2.70163 [code]
Spatial multi-omics unveils sphingolipid metabolic reprogramming within the retinal pathological niche.
Journal: iMeta
In common: SingleCellExperiment, igraph, circlize, 8 other tools, genetics / omics, mouse, 1 reference
[8] doi:10.1186/s13059-026-04177-w [code]
Genomic sequence evolution underlying human neocortical interareal diversification.
Journal: Genome biology
In common: reticulate, UMAP, igraph, 7 other tools, genetics / omics, mouse, 2 references
[9] doi:10.1016/j.celrep.2026.117500 [code]
Spatio-molecular gene expression reflects dorsal anterior cingulate cortex structure and function in the human brain.
Journal: Cell reports
In common: SingleCellExperiment, reticulate, igraph, 8 other tools, genetics / omics, 1 reference
[10] doi:10.1186/s12974-026-03838-8 [code]
Acarbose modulates microglial Pkm2 acetylation to reshape immunometabolism and preserve retinal neurons after ischemia-reperfusion.
Journal: Journal of neuroinflammation
In common: SingleCellExperiment, reticulate, UMAP, 8 other tools, genetics / omics, mouse

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.