OSCR

StPedf: Cell trajectory inference of spatial transcriptomics via spatial proximity embedding and spatial density-adaptive fusion.

Code ↔ Paper

7 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 7 matches
  1. [1] § 5. Methods › 5.5. Cell density-aware adaptive weighting mechanism and low-confidence region protection ↔ TI/single_time/transition.py, lines 130–233 · score 0.84 · low confidence region, density adaptive, Low density, spatial weights, hole, connectivity
  2. [2] § 2. Results › 2.1. Overview ↔ TI/single_time/transition.py, lines 130–233 · score 0.80 · fused cost, adaptive transition matrix, cost matrix, high density, low density, spatial distance
  3. [3] § 2. Results › 2.1. Overview ↔ TI/single_time/lap.py, lines 381–439 · score 0.61 · terminal cells, vector field, transition matrix, adjacency, optimal, embedding
  4. [4] § 5. Methods › 5.4. Multi-task training objectives ↔ example.ipynb, lines 204–219 · score 0.60 · Kendall correlation, Spearman correlation, ground truth
  5. [5] § 5. Methods › 5.8. Selection of starting cells ↔ TI/single_time/velocity.py, lines 21–99 · score 0.55 · transition probability matrix, optimal transport, sequence, cells
  6. [6] § 5. Methods › 5.10. Cell velocity calculation and trajectory construction methods ↔ TI/single_time/velocity.py, lines 650–717 · score 0.53 · displacement vector, cell velocity, weighted
  7. [7] § 5. Methods › 5.10. Cell velocity calculation and trajectory construction methods ↔ TI/single_time/velocity.py, lines 212–310 · score 0.52 · numerical stability, transition probabilities, velocity, cell

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

Python · 834 lines · 25 KB · no license · 3 matches

  1. import ot
  2. import sys
  3. import scanpy as sc
  4. import pandas as pd
  5. import numpy as np
  6. from anndata import AnnData
  7. from sklearn.metrics.pairwise import euclidean_distances
  8. from sklearn.neighbors import NearestNeighbors
  9. from scipy.sparse import csr_matrix
  10. from scipy.stats import norm
  11. from numpy.random import RandomState
  12. import plotly.graph_objs as go
  13. import plotly.offline as py
  14. from ipywidgets import VBox
  15. import seaborn as sns
  16. from typing import Optional, Union, Literal, Tuple, List
  17. from .utils import nearest_neighbors, kmeans_centers
  18. def get_ot_matrix(
  19. adata: AnnData,
  20. data_type: str,
  21. alpha1: int = 1,
  22. alpha2: int = 1,
  23. random_state: Union[None, int, RandomState] = 0,
  24. ) -> np.ndarray:
  25. """
  26. Calculate transfer probabilities between cells.
  27. Using optimal transport theory based on gene expression and/or spatial location information.
  28. Parameters
  29. ----------
  30. adata
  31. An :class:`~anndata.AnnData` object.
  32. data_type
  33. The type of sequencing data.
  34. - ``'spatial'``: for the spatial transcriptome data.
  35. - ``'single-cell'``: for the single-cell sequencing data.
  36. alpha1
  37. The proportion of spatial location information.
  38. (Default: 1)
  39. alpha2
  40. The proportion of gene expression information.
  41. (Default: 1)
  42. random_state
  43. Different initial states for the pca.
  44. (Default: 0)
  45. Returns
  46. -------
  47. :class:`~numpy.ndarray`
  48. Cell transition probability matrix.
  49. """
  50. if "X_pca" not in adata.obsm:
  51. print("X_pca is not in adata.obsm, automatically do PCA first.")
  52. sc.tl.pca(adata, svd_solver="arpack", random_state=random_state)
  53. newdata = adata.obsm["X_pca"]
  54. newdata2 = newdata.copy()
  55. if data_type == "spatial":
  56. newcoor = adata.obsm["X_spatial"]
  57. newcoor2 = newcoor.copy()
  58. # calculate physical distance
  59. ed_coor = euclidean_distances(newcoor, newcoor2, squared=True)
  60. m1 = ed_coor / sum(sum(ed_coor))
  61. # calculate gene expression PCA space distance
  62. ed_gene = euclidean_distances(newdata, newdata2, squared=True)
  63. m2 = ed_gene / sum(sum(ed_gene))
  64. M = alpha1 * m1 + alpha2 * m2
  65. M /= M.max()
  66. row, col = np.diag_indices_from(M)
  67. M[row, col] = M.max() * 1000000
  68. elif data_type == "single-cell":
  69. ed = euclidean_distances(newdata, newdata2, squared=True)
  70. M = ed / sum(sum(ed))
  71. M /= M.max()
  72. row, col = np.diag_indices_from(M)
  73. M[row, col] = M.max() * 1000000
  74. else:
  75. sys.exit(
  76. "Please give the right data type, choose from 'spatial' or 'single-cell'."
  77. )
  78. a, b = (
  79. np.ones((adata.n_obs,)) / adata.n_obs,
  80. np.ones((adata.n_obs,)) / adata.n_obs,
  81. )
  82. lambd = 1e-1
  83. Gs = np.array(ot.sinkhorn(a, b, M, lambd))
  84. return Gs
  85. def set_start_cells(
  86. adata: AnnData,
  87. select_way: Literal["coordinates", "cell_type"],
  88. cell_type: Optional[str] = None,
  89. start_point: Optional[Tuple[int, int]] = None,
  90. basis: str = "spatial",
  91. split: bool = False,
  92. n_clusters: int = 2,
  93. n_neigh: int = 5,
  94. ) -> list:
  95. """
  96. Use coordinates or cell type to manually select starting cells.
  97. Parameters
  98. ----------
  99. adata
  100. An :class:`~anndata.AnnData` object.
  101. select_way
  102. Ways to select starting cells.
  103. (1) ``'cell_type'``: select by cell type.
  104. (2) ``'coordinates'``: select by coordinates.
  105. cell_type
  106. Restrict the cell type of starting cells.
  107. (Deafult: None)
  108. start_point
  109. The coordinates of the start point in 'coordinates' mode.
  110. basis
  111. The basis in `adata.obsm` to store position information.
  112. split
  113. Whether to split the specific type of cells into several small clusters according to cell density.
  114. n_clsuters
  115. The number of cluster centers after splitting.
  116. n_neigh
  117. The number of neighbors next to the start point/cluster center selected as the starting cell.
  118. Returns
  119. -------
  120. list
  121. The index number of selected starting cells.
  122. """
  123. if select_way == "coordinates":
  124. if start_point is None:
  125. raise ValueError(
  126. f"`start_point` must be specified in the 'coordinates' mode."
  127. )
  128. start_cells = nearest_neighbors(start_point, adata.obsm["X_" + basis], n_neigh)[
  129. 0
  130. ]
  131. if cell_type is not None:
  132. type_cells = np.where(adata.obs["cluster"] == cell_type)[0]
  133. start_cells = set(start_cells).intersection(set(type_cells))
  134. elif select_way == "cell_type":
  135. if cell_type is None:
  136. raise ValueError("in 'cell_type' mode, `cell_type` cannot be None.")
  137. start_cells = np.where(adata.obs["cluster"] == cell_type)[0]
  138. if split == True:
  139. mask = adata.obs["cluster"] == cell_type
  140. cell_coords = adata.obsm["X_" + basis][mask]
  141. cluster_centers = kmeans_centers(cell_coords, n_clusters=n_clusters)
  142. select_cluster_coords = adata.obsm["X_" + basis].copy()
  143. select_cluster_coords[np.logical_not(mask)] = 1e10
  144. start_cells = nearest_neighbors(
  145. cluster_centers, select_cluster_coords, n_neigh
  146. ).flatten()
  147. else:
  148. raise ValueError(f"`select_way` must choose from 'coordinates' or 'cell_type'.")
  149. return list(start_cells)
  150. def get_ptime(adata: AnnData, start_cells: list):
  151. """
  152. Get the cell pseudotime based on transition probabilities from initial cells.
  153. Parameters
  154. ----------
  155. adata
  156. An :class:`~anndata.AnnData` object.
  157. start_cells
  158. List of index numbers of starting cells.
  159. Returns
  160. -------
  161. :class:`~numpy.ndarray`
  162. Ptime correspongding to cells.
  163. """
  164. select_trans = adata.obsp["trans"][start_cells]
  165. cell_tran = np.sum(select_trans, axis=0)
  166. adata.obs["tran"] = cell_tran
  167. cell_tran_sort = list(np.argsort(cell_tran))
  168. cell_tran_sort = cell_tran_sort[::-1]
  169. ptime = pd.Series(dtype="float32", index=adata.obs.index)
  170. for i in range(adata.n_obs):
  171. ptime[cell_tran_sort[i]] = i / (adata.n_obs - 1)
  172. return ptime.values
  173. from scipy.sparse import issparse, csr_matrix
  174. import numpy as np
  175. from sklearn.neighbors import NearestNeighbors
  176. def get_neigh_trans(
  177. adata: AnnData, basis: str, n_neigh_pos: int = 10, n_neigh_embed: int = 0
  178. ):
  179. """
  180. Get transport neighbors using both spatial positions and SEDR embeddings
  181. Parameters
  182. ----------
  183. adata
  184. An :class:`~anndata.AnnData` object.
  185. basis
  186. The basis used in visualizing the cell position.
  187. n_neigh_pos
  188. Number of neighbors based on spatial positions.
  189. n_neigh_embed
  190. Number of neighbors based on SEDR embeddings.
  191. Returns
  192. -------
  193. :class:`~scipy.sparse._csr.csr_matrix`
  194. Sparse matrix of transition probabilities for selected neighbors.
  195. """
  196. if n_neigh_pos == 0 and n_neigh_embed == 0:
  197. raise ValueError(
  198. "Number of position and embedding neighbors cannot both be zero."
  199. )
  200. # Ensure numerical stability of transition matrix
  201. trans_mat = adata.obsp["trans"]
  202. if np.any(trans_mat < 0):
  203. trans_mat = np.maximum(trans_mat, 0)
  204. row_sums = trans_mat.sum(axis=1)
  205. if np.any(row_sums == 0):
  206. trans_mat += 1e-10
  207. row_sums = trans_mat.sum(axis=1)
  208. trans_mat = trans_mat / row_sums[:, np.newaxis]
  209. adata.obsp["trans"] = trans_mat
  210. # Spatial position neighbors
  211. if n_neigh_pos:
  212. nn_pos = NearestNeighbors(n_neighbors=n_neigh_pos, n_jobs=-1)
  213. nn_pos.fit(adata.obsm["X_" + basis])
  214. _, neigh_pos = nn_pos.kneighbors(adata.obsm["X_" + basis])
  215. neigh_pos = neigh_pos[:, 1:] # exclude self
  216. # SEDR embedding neighbors
  217. if n_neigh_embed:
  218. if "StPedf" not in adata.obsm:
  219. raise ValueError("StPedf embedding not found in adata.obsm")
  220. nn_embed = NearestNeighbors(n_neighbors=n_neigh_embed, n_jobs=-1)
  221. nn_embed.fit(adata.obsm["StPedf"])
  222. _, neigh_embed = nn_embed.kneighbors(adata.obsm["StPedf"])
  223. neigh_embed = neigh_embed[:, 1:] # exclude self
  224. # Build neighbor list
  225. neigh_list = []
  226. for i in range(adata.n_obs):
  227. neighbors = []
  228. if n_neigh_pos:
  229. neighbors.extend(neigh_pos[i])
  230. if n_neigh_embed:
  231. neighbors.extend(neigh_embed[i])
  232. # Deduplicate and exclude self
  233. neighbors = np.unique(neighbors)
  234. neighbors = neighbors[neighbors != i]
  235. neigh_list.append(neighbors)
  236. # Build sparse transition probability matrix
  237. indptr = [0]
  238. indices = []
  239. csr_data = []
  240. for i in range(adata.n_obs):
  241. neighbors = neigh_list[i]
  242. if len(neighbors) == 0:
  243. continue
  244. # Get transition probabilities and normalize
  245. probs = adata.obsp["trans"][i, neighbors]
  246. if issparse(probs):
  247. probs = probs.toarray().flatten()
  248. prob_sum = probs.sum()
  249. if prob_sum > 0:
  250. probs = probs / prob_sum
  251. else:
  252. probs = np.ones_like(probs) / len(probs)
  253. indices.extend(neighbors)
  254. csr_data.extend(probs)
  255. indptr.append(len(indices))
  256. trans_neigh_csr = csr_matrix(
  257. (csr_data, indices, indptr), shape=(adata.n_obs, adata.n_obs)
  258. )
  259. return trans_neigh_csr
  260. '''def get_neigh_trans(
  261. adata: AnnData, basis: str, n_neigh_pos: int = 10, n_neigh_gene: int = 0
  262. ):
  263. """
  264. Get the transport neighbors from two ways, position and/or gene expression
  265. Parameters
  266. ----------
  267. adata
  268. An :class:`~anndata.AnnData` object.
  269. basis
  270. The basis used in visualizing the cell position.
  271. n_neigh_pos
  272. Number of neighbors based on cell positions such as spatial or umap coordinates.
  273. (Default: 10)
  274. n_neigh_gene
  275. Number of neighbors based on gene expression (PCA).
  276. (Default: 0)
  277. Returns
  278. -------
  279. :class:`~scipy.sparse._csr.csr_matrix`
  280. A sparse matrix composed of transition probabilities of selected neighbor cells.
  281. """
  282. if n_neigh_pos == 0 and n_neigh_gene == 0:
  283. raise ValueError(
  284. "the number of position neighbors and gene neighbors cannot be zero at the same time."
  285. )
  286. if n_neigh_pos:
  287. nn = NearestNeighbors(n_neighbors=n_neigh_pos, n_jobs=-1)
  288. nn.fit(adata.obsm["X_" + basis])
  289. dist_pos, neigh_pos = nn.kneighbors(adata.obsm["X_" + basis])
  290. dist_pos = dist_pos[:, 1:]
  291. neigh_pos = neigh_pos[:, 1:]
  292. neigh_pos_list = []
  293. for i in range(adata.n_obs):
  294. idx = neigh_pos[i] # embedding上的邻居
  295. idx2 = neigh_pos[idx] # embedding上邻居的邻居
  296. idx2 = np.setdiff1d(idx2, i)
  297. neigh_pos_list.append(np.unique(np.concatenate([idx, idx2])))
  298. # neigh_pos_list.append(idx)
  299. if n_neigh_gene:
  300. if "X_pca" not in adata.obsm:
  301. print("X_pca is not in adata.obsm, automatically do PCA first.")
  302. sc.tl.pca(adata)
  303. sc.pp.neighbors(
  304. adata, use_rep="X_pca", key_added="X_pca", n_neighbors=n_neigh_gene
  305. )
  306. neigh_gene = adata.obsp["distances"].indices.reshape(
  307. -1, adata.uns["neighbors"]["params"]["n_neighbors"] - 1
  308. )
  309. indptr = [0]
  310. indices = []
  311. csr_data = []
  312. count = 0
  313. for i in range(adata.n_obs):
  314. if n_neigh_pos == 0:
  315. n_all = neigh_gene[i]
  316. elif n_neigh_gene == 0:
  317. n_all = neigh_pos_list[i]
  318. else:
  319. n_all = np.unique(np.concatenate([neigh_pos_list[i], neigh_gene[i]]))
  320. count += len(n_all)
  321. indptr.append(count)
  322. indices.extend(n_all)
  323. csr_data.extend(
  324. adata.obsp["trans"][i][n_all]
  325. / (adata.obsp["trans"][i][n_all].sum()) # normalize
  326. )
  327. trans_neigh_csr = csr_matrix(
  328. (csr_data, indices, indptr), shape=(adata.n_obs, adata.n_obs)
  329. )
  330. return trans_neigh_csr'''
  331. '''def get_velocity(
  332. adata: AnnData,
  333. basis: str,
  334. n_neigh_pos: int = 10,
  335. n_neigh_gene: int = 0,
  336. grid_num=50,
  337. smooth=0.5,
  338. density=1.0,
  339. ) -> tuple:
  340. """
  341. Get the velocity of each cell.
  342. The speed can be determined in terms of the cell location and/or gene expression.
  343. Parameters
  344. ----------
  345. adata
  346. An :class:`~anndata.AnnData` object.
  347. basis
  348. The label of cell coordinates, for example, `umap` or `spatial`.
  349. n_neigh_pos
  350. Number of neighbors based on cell positions such as spatial or umap coordinates.
  351. (Default: 10)
  352. n_neigh_gene
  353. Number of neighbors based on gene expression.
  354. (Default: 0)
  355. Returns
  356. -------
  357. tuple
  358. The grid coordinates and cell velocities on each grid to draw the streamplot figure.
  359. """
  360. adata.obsp["trans_neigh_csr"] = get_neigh_trans(
  361. adata, basis, n_neigh_pos, n_neigh_gene
  362. )
  363. position = adata.obsm["X_" + basis]
  364. V = np.zeros(position.shape) # 速度为2维
  365. for cell in range(adata.n_obs): # 循环每个细胞
  366. cell_u = 0.0 # 初始化细胞速度
  367. cell_v = 0.0
  368. x1 = position[cell][0] # 初始化细胞坐标
  369. y1 = position[cell][1]
  370. for neigh in adata.obsp["trans_neigh_csr"][cell].indices: # 针对每个邻居
  371. p = adata.obsp["trans_neigh_csr"][cell, neigh]
  372. if (
  373. adata.obs["ptime"][neigh] < adata.obs["ptime"][cell]
  374. ): # 若邻居的ptime小于当前的,则概率反向
  375. p = -p
  376. x2 = position[neigh][0]
  377. y2 = position[neigh][1]
  378. # 正交向量确定速度方向,乘上概率确定速度大小
  379. sub_u = p * (x2 - x1) / (np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2))
  380. sub_v = p * (y2 - y1) / (np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2))
  381. cell_u += sub_u
  382. cell_v += sub_v
  383. V[cell][0] = cell_u / adata.obsp["trans_neigh_csr"][cell].indptr[1]
  384. V[cell][1] = cell_v / adata.obsp["trans_neigh_csr"][cell].indptr[1]
  385. adata.obsm["velocity_" + basis] = V
  386. print(f"The velocity of cells store in 'velocity_{basis}'.")
  387. P_grid, V_grid = get_velocity_grid(
  388. adata,
  389. P=position,
  390. V=adata.obsm["velocity_" + basis],
  391. grid_num=grid_num,
  392. smooth=smooth,
  393. density=density,
  394. )
  395. return P_grid, V_grid'''
  396. def get_velocity(
  397. adata: AnnData,
  398. basis: str,
  399. n_neigh_pos: int = 10,
  400. n_neigh_embed: int = 5,
  401. grid_num=50,
  402. smooth=0.5,
  403. density=1.0,
  404. ) -> tuple:
  405. """
  406. Calculate cell velocity using both spatial positions and SEDR embeddings
  407. Parameters
  408. ----------
  409. adata
  410. An :class:`~anndata.AnnData` object.
  411. basis
  412. Coordinate system label (e.g., 'spatial').
  413. n_neigh_pos
  414. Number of spatial position neighbors.
  415. n_neigh_embed
  416. Number of SEDR embedding neighbors.
  417. grid_num
  418. Resolution for grid-based visualization.
  419. smooth
  420. Smoothing factor for visualization.
  421. density
  422. Density factor for visualization.
  423. Returns
  424. -------
  425. tuple
  426. Grid positions and velocities for visualization.
  427. """
  428. # Get neighbor matrix (based on spatial positions and SEDR embeddings)
  429. adata.obsp["trans_neigh_csr"] = get_neigh_trans(
  430. adata, basis, n_neigh_pos, n_neigh_embed
  431. )
  432. position = adata.obsm["X_" + basis]
  433. V = np.zeros(position.shape)
  434. # Add small value to prevent division by zero
  435. epsilon = 1e-10
  436. for cell in range(adata.n_obs):
  437. cell_u = 0.0
  438. cell_v = 0.0
  439. x1, y1 = position[cell]
  440. neighbors = adata.obsp["trans_neigh_csr"][cell].indices
  441. n_neighbors = len(neighbors)
  442. if n_neighbors == 0:
  443. V[cell] = [0.0, 0.0]
  444. continue
  445. for neigh in neighbors:
  446. p = adata.obsp["trans_neigh_csr"][cell, neigh]
  447. # Pseudotime direction correction
  448. if adata.obs["ptime"][neigh] < adata.obs["ptime"][cell]:
  449. p = -p
  450. x2, y2 = position[neigh]
  451. # Calculate distance and direction
  452. dx = x2 - x1
  453. dy = y2 - y1
  454. dist = np.sqrt(dx**2 + dy**2) + epsilon
  455. # Calculate velocity components
  456. sub_u = p * dx / dist
  457. sub_v = p * dy / dist
  458. # Numerical stability check
  459. if not np.isfinite(sub_u):
  460. sub_u = 0.0
  461. if not np.isfinite(sub_v):
  462. sub_v = 0.0
  463. cell_u += sub_u
  464. cell_v += sub_v
  465. # Average velocity
  466. V[cell][0] = cell_u / n_neighbors
  467. V[cell][1] = cell_v / n_neighbors
  468. # Final check
  469. if not np.isfinite(V[cell][0]):
  470. V[cell][0] = 0.0
  471. if not np.isfinite(V[cell][1]):
  472. V[cell][1] = 0.0
  473. # Add small noise to prevent all zeros
  474. noise = np.random.normal(0, 1e-6, V.shape)
  475. V += noise
  476. adata.obsm["velocity_" + basis] = V
  477. print(f"Cell velocities stored in 'velocity_{basis}'")
  478. # Grid processing for visualization
  479. P_grid, V_grid = get_velocity_grid(
  480. adata,
  481. P=position,
  482. V=V,
  483. grid_num=grid_num,
  484. smooth=smooth,
  485. density=density,
  486. )
  487. return P_grid, V_grid
  488. def get_2_biggest_clusters(adata):
  489. """Automatic selection of starting cells to determine the direction of cell trajectories
  490. Args:
  491. adata (anndata):
  492. Returns:
  493. tuple: Contains 2 cluster with maximum sum of transition probabilities
  494. """
  495. clusters = np.unique(adata.obs["cluster"])
  496. cluster_trans = pd.DataFrame(index=clusters, columns=clusters)
  497. for start_cluster in clusters:
  498. for end_cluster in clusters:
  499. if start_cluster == end_cluster:
  500. cluster_trans.loc[start_cluster, end_cluster] = 0
  501. continue
  502. starts = adata.obs["cluster"] == start_cluster
  503. ends = adata.obs["cluster"] == end_cluster
  504. cluster_trans.loc[start_cluster, end_cluster] = (
  505. np.sum(adata.obsp["trans"][starts][:, ends])
  506. / np.sum(starts)
  507. / np.sum(ends)
  508. )
  509. highest_2_clusters = cluster_trans.stack().astype(float).idxmax()
  510. return highest_2_clusters
  511. def auto_get_start_cluster(adata, clusters: Optional[list] = None):
  512. """
  513. Select the start cluster with the largest sum of transfer probability
  514. Parameters
  515. ----------
  516. adata
  517. Anndata
  518. clusters, list
  519. Give clusters to find, by default None, each cluster will be traversed and calculated
  520. Returns
  521. -------
  522. str
  523. One cluster with maximum sum of transition probabilities
  524. """
  525. if clusters == None:
  526. clusters = np.unique(adata.obs["cluster"])
  527. cluster_prob_sum = {}
  528. for cluster in clusters:
  529. start_cells = set_start_cells(adata, select_way="cell_type", cell_type=cluster)
  530. adata.obs["ptime"] = get_ptime(adata, start_cells)
  531. cell_time_sort = adata.obs["ptime"].values.argsort()
  532. prob_sum = 0
  533. for i in range(len(cell_time_sort) - 1):
  534. pre = cell_time_sort[i]
  535. next = cell_time_sort[i + 1]
  536. prob = adata.obsp["trans"][pre, next]
  537. prob_sum += prob
  538. cluster_prob_sum[cluster] = prob_sum
  539. highest_cluster = max(cluster_prob_sum, key=cluster_prob_sum.get)
  540. print(
  541. "The auto selecting cluster is: '"
  542. + highest_cluster
  543. + "'. If there is a large discrepancy with the known biological knowledge, please manually select the starting cluster."
  544. )
  545. return highest_cluster
  546. def get_velocity_grid(
  547. adata,
  548. P: np.ndarray,
  549. V: np.ndarray,
  550. grid_num: int = 50,
  551. smooth: float = 0.5,
  552. density: float = 1.0,
  553. ) -> tuple:
  554. """
  555. Convert cell velocity to grid velocity for streamline display
  556. The visualization of vector field borrows idea from scTour: https://github.com/LiQian-XC/sctour/blob/main/sctour.
  557. Parameters
  558. ----------
  559. P
  560. The position of cells.
  561. V
  562. The velocity of cells.
  563. smooth
  564. The factor for scale in Gaussian pdf.
  565. (Default: 0.5)
  566. density
  567. grid density
  568. (Default: 1.0)
  569. Returns
  570. ----------
  571. tuple
  572. The embedding and unitary displacement vectors in grid level.
  573. """
  574. grids = []
  575. for dim in range(P.shape[1]):
  576. m, M = np.min(P[:, dim]), np.max(P[:, dim])
  577. m = m - 0.01 * np.abs(M - m)
  578. M = M + 0.01 * np.abs(M - m)
  579. gr = np.linspace(m, M, int(grid_num * density))
  580. grids.append(gr)
  581. meshes = np.meshgrid(*grids)
  582. P_grid = np.vstack([i.flat for i in meshes]).T
  583. n_neighbors = int(P.shape[0] / grid_num)
  584. nn = NearestNeighbors(n_neighbors=n_neighbors, n_jobs=-1)
  585. nn.fit(P)
  586. dists, neighs = nn.kneighbors(P_grid)
  587. scale = np.mean([grid[1] - grid[0] for grid in grids]) * smooth
  588. weight = norm.pdf(x=dists, scale=scale)
  589. p_mass = weight.sum(1)
  590. V_grid = (V[neighs] * weight[:, :, None]).sum(1)
  591. V_grid /= np.maximum(1, p_mass)[:, None]
  592. P_grid = np.stack(grids)
  593. ns = P_grid.shape[1]
  594. V_grid = V_grid.T.reshape(2, ns, ns)
  595. mass = np.sqrt((V_grid * V_grid).sum(0))
  596. min_mass = 1e-5
  597. min_mass = np.clip(min_mass, None, np.percentile(mass, 99) * 0.01)
  598. cutoff = mass < min_mass
  599. V_grid[0][cutoff] = np.nan
  600. adata.uns["P_grid"] = P_grid
  601. adata.uns["V_grid"] = V_grid
  602. return P_grid, V_grid
  603. class Lasso:
  604. """
  605. Lasso an region of interest (ROI) based on spatial cluster.
  606. Parameters
  607. ----------
  608. adata
  609. An :class:`~anndata.AnnData` object.
  610. """
  611. __sub_index = []
  612. sub_cells = []
  613. def __init__(self, adata):
  614. self.adata = adata
  615. def vi_plot(
  616. self,
  617. basis: str = "spatial",
  618. cell_type: Optional[str] = None,
  619. ):
  620. """
  621. Plot figures.
  622. Parameters
  623. ----------
  624. basis
  625. The basis in `adata.obsm` to store position information.
  626. (Deafult: 'spatial')
  627. cell_type
  628. Restrict the cell type of starting cells.
  629. (Deafult: None)
  630. Returns
  631. -------
  632. The container of cell scatter plot and table.
  633. """
  634. cell_types = self.adata.obs["cluster"].unique()
  635. colors = sns.color_palette(n_colors=len(cell_types)).as_hex()
  636. cluster_color = dict(zip(cell_types, colors))
  637. self.adata.uns["cluster_color"] = cluster_color
  638. df = pd.DataFrame()
  639. df["group_ID"] = self.adata.obs_names
  640. df["labels"] = self.adata.obs["cluster"].values
  641. df["spatial_0"] = self.adata.obsm["X_" + basis][:, 0]
  642. df["spatial_1"] = self.adata.obsm["X_" + basis][:, 1]
  643. df["color"] = df.labels.map(self.adata.uns["cluster_color"])
  644. py.init_notebook_mode()
  645. f = go.FigureWidget(
  646. [
  647. go.Scatter(
  648. x=df["spatial_0"],
  649. y=df["spatial_1"],
  650. mode="markers",
  651. marker_color=df["color"],
  652. )
  653. ]
  654. )
  655. scatter = f.data[0]
  656. f.layout.plot_bgcolor = "rgb(255,255,255)"
  657. f.layout.autosize = False
  658. axis_dict = dict(
  659. showticklabels=True,
  660. autorange=True,
  661. )
  662. f.layout.yaxis = axis_dict
  663. f.layout.xaxis = axis_dict
  664. f.layout.width = 600
  665. f.layout.height = 600
  666. # Create a table FigureWidget that updates on selection from points in the scatter plot of f
  667. t = go.FigureWidget(
  668. [
  669. go.Table(
  670. header=dict(
  671. values=["group_ID", "labels", "spatial_0", "spatial_1"],
  672. fill=dict(color="#C2D4FF"),
  673. align=["left"] * 5,
  674. ),
  675. cells=dict(
  676. values=[
  677. df[col]
  678. for col in ["group_ID", "labels", "spatial_0", "spatial_1"]
  679. ],
  680. fill=dict(color="#F5F8FF"),
  681. align=["left"] * 5,
  682. ),
  683. )
  684. ]
  685. )
  686. def selection_fn(trace, points, selector):
  687. t.data[0].cells.values = [
  688. df.loc[points.point_inds][col]
  689. for col in ["group_ID", "labels", "spatial_0", "spatial_1"]
  690. ]
  691. Lasso.__sub_index = t.data[0].cells.values[0]
  692. Lasso.sub_cells = np.where(self.adata.obs.index.isin(Lasso.__sub_index))[0]
  693. if cell_type is not None:
  694. type_cells = np.where(self.adata.obs["cluster"] == cell_type)[0]
  695. Lasso.sub_cells = sorted(
  696. set(Lasso.sub_cells).intersection(set(type_cells))
  697. )
  698. scatter.on_selection(selection_fn)
  699. # Put everything together
  700. return VBox((f, t))

velocity.py at commit 57c3873, no license · at the source

Overview

Authors: Yuan Zhang1, Ziyan Sun1, Zhixin Shi1, Mengdi Nan1, Yuhan Fu1, Qing Ren1, Jie Gao1
ORCID iDs: Jie Gao
  1. School of Science, Jiangnan University, Wuxi, Jiangsu, China
Institutions: Jiangnan University (China)
Journal: PLoS computational biology, volume 22, issue 6, article e1014346
Dates: received 29 December 2025; accepted 19 May 2026; published online 5 June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1371/journal.pcbi.1014346 · PMID 42247447 · PMCID PMC13240877 · OpenAlex W7163716196
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: genetics / omics (modality), human (organism)
Methods: Smoothing, state filtering, decompositions, Machine learning, Connectivity
MeSH: Spatial Transcriptomics*, Transcriptome*, Algorithms, Animals, Cell Differentiation, Computational Biology, Computer Simulation, Humans, Neural Networks, Computer (* major topic)
Topic: Single-cell and spatial transcriptomics (Molecular Biology, Biochemistry, Genetics and Molecular Biology), according to OpenAlex
Funding: Innovative Research Group Project of the National Natural Science Foundation of China (11831015); National Natural Science Foundation of China (11831015, 12271216, 92370131)
Citations: not cited yet (Europe PMC); 40 references in the paper

Abstract

Spatial transcriptomics is transforming our multidimensional understanding of cellular spatial organization and its functional mechanisms in processes such as development and disease by systematically resolving the spatial heterogeneity of gene expression within tissues. To delve deeper into the dynamic processes underlying spatial expression patterns, spatial trajectory inference integrates genetic and spatial information to reconstruct the spatial developmental trajectories of cells within tissues. This approach reveals the patterns of differentiation and dynamic changes as cellular states evolve continuously along spatial axes. However, existing methods often struggle to uniformly model the complex, nonlinear interactions between high-dimensional gene expression and spatial coordinates. Here, we introduce StPedf, whose core lies in employing a neural network with a masking mechanism to capture complex nonlinear interactions between high-dimensional genes and spatial positions. It further leverages spatial proximity information as a guiding cue, dynamically and adaptively adjusting the embedding of gene and spatial information and the weighting of spatial proximity information based on spatial density. This enables trajectory inference guided by spatial information. This enables optimal transport to derive intercellular transition matrices, reconstruct cellular differentiation trajectories, and construct pseudo-spatiotemporal maps. StPedf demonstrates superior performance over existing methods on five structurally distinct simulated datasets. Using StPedf, we successfully mapped distinct lineages in the spatial trajectories of telencephalon regeneration in the Ambystoma mexicanum, multiple malignant lineages expanding within primary tumors, and developmental spatial trajectories and pseudo-spatiotemporal maps in human dorsolateral prefrontal cortex (DLPFC). StPedf significantly enhances the accuracy and interpretability of spatial trajectory inference, providing critical technical support for revealing the dynamic patterns of cellular fate transitions within tissue microenvironments.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

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

YuanZ0316/StPedf

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 57c3873d95126c76f8bd28d5f141f7416c749464, 8 April 2026
Languages: Python (16), Jupyter (3)
Size: 42 files, 19 scripts
Software Heritage: not archived
Found in: “Data Availability”
Holds: README, environment (environment.yml), 3 notebooks
Not found: license file, CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (16 files), scikit-learn (14 files), Scanpy (12 files), SciPy (10 files), Matplotlib (9 files), pandas (9 files), anndata (8 files), PyTorch (8 files), seaborn (5 files), NetworkX (3 files), statsmodels (2 files), Plotly (1 file), rpy2 (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
20 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;
  • 19 scripts, each with its path and the digest of its content;
  • 7 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

Data Availability

Reproducible codes: The StPedf method is implemented as a Python package, publicly available at GitHub: https://github.com/YuanZ0316/StPedf Data availability statement: Simulated datasets: The simulated datasets can be downloaded from GitHub: https://github.com/YuanZ0316/StPedf/tree/main/Simulated_dataset Public datasets: The original public data used in this paper can be accessed through the following links: (1) Ambystoma mexicanum brain regeneration dataset (CNP0002068): https://db.cngb.org/data_resources/project/CNP0002068 (2) Intrahepatic cholangiocarcinoma (ICC) dataset (CNP0002199): https://db.cngb.org/data_resources/project/CNP0002199 (3) Human DLPFC 10x Visium dataset: http://research.libd.org/spatialLIBD/.

Reproduced under the paper's license (CC BY), from the paper cited above.

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 2, 28 September 2026

  • Funding: added Innovative Research Group Project of the National Natural Science Foundation of China: 11831015; National Natural Science Foundation of China: 11831015, 12271216, 92370131

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 7 authors, 9 MeSH terms, 38 references.

Cite

This paper

Zhang, Y., Sun, Z., Shi, Z., Nan, M., Fu, Y., Ren, Q., & Gao, J. (2026). StPedf: Cell trajectory inference of spatial transcriptomics via spatial proximity embedding and spatial density-adaptive fusion. PLoS computational biology, 22(6), e1014346. https://doi.org/10.1371/journal.pcbi.1014346

BibTeX

@article{zhang2026stpedf,
author = {Zhang, Yuan and Sun, Ziyan and Shi, Zhixin and Nan, Mengdi and Fu, Yuhan and Ren, Qing and Gao, Jie},
title = {{StPedf: Cell trajectory inference of spatial transcriptomics via spatial proximity embedding and spatial density-adaptive fusion}},
journal = {PLoS computational biology},
year = {2026},
month = jun,
volume = {22},
number = {6},
pages = {e1014346},
publisher = {PLOS},
issn = {1553-734X},
doi = {10.1371/journal.pcbi.1014346},
url = {https://doi.org/10.1371/journal.pcbi.1014346},
pmid = {42247447},
pmcid = {PMC13240877}
}

RIS

TY - JOUR
AU - Zhang, Yuan
AU - Sun, Ziyan
AU - Shi, Zhixin
AU - Nan, Mengdi
AU - Fu, Yuhan
AU - Ren, Qing
AU - Gao, Jie
TI - StPedf: Cell trajectory inference of spatial transcriptomics via spatial proximity embedding and spatial density-adaptive fusion
T2 - PLoS computational biology
J2 - PLoS Comput Biol
PY - 2026
DA - 2026/06/05
VL - 22
IS - 6
SP - e1014346
SN - 1553-734X
PB - PLOS
DO - 10.1371/journal.pcbi.1014346
UR - https://doi.org/10.1371/journal.pcbi.1014346
LA - en
ER -

CSL-JSON

{
"id": "10.1371/journal.pcbi.1014346",
"type": "article-journal",
"title": "StPedf: Cell trajectory inference of spatial transcriptomics via spatial proximity embedding and spatial density-adaptive fusion",
"container-title": "PLoS computational biology",
"author": [
{
"family": "Zhang",
"given": "Yuan"
},
{
"family": "Sun",
"given": "Ziyan"
},
{
"family": "Shi",
"given": "Zhixin"
},
{
"family": "Nan",
"given": "Mengdi"
},
{
"family": "Fu",
"given": "Yuhan"
},
{
"family": "Ren",
"given": "Qing"
},
{
"family": "Gao",
"given": "Jie"
}
],
"container-title-short": "PLoS Comput Biol",
"volume": "22",
"issue": "6",
"page": "e1014346",
"DOI": "10.1371/journal.pcbi.1014346",
"PMID": "42247447",
"PMCID": "PMC13240877",
"ISSN": "1553-734X",
"publisher": "PLOS",
"URL": "https://doi.org/10.1371/journal.pcbi.1014346",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
5
]
]
}
}

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.1002/advs.77003 [code]
SemanticST: A Scalable Multi-Contextual Graph Learning Framework for Uncovering Spatial Niches and Robust Multi-Sample Integration in Spatial Transcriptomics.
Journal: Advanced science (Weinheim, Baden-Wurttemberg, Germany)
In common: rpy2, anndata, Scanpy, 7 other tools, genetics / omics, 7 references
[2] doi:10.1093/bib/bbag298 [code]
Empowering multifaceted analysis of spatial transcriptomics data with RGAST.
Journal: Briefings in bioinformatics
In common: rpy2, anndata, Scanpy, 8 other tools, genetics / omics, 5 references
[3] doi:10.21203/rs.3.rs-9676637/v1 [code]
A Comprehensive Benchmarking of Spatial Deconvolution and Domain Detection Methods across Diverse Tissues and Spatial Transcriptomic Technologies
Journal: Research Square (preprint)
In common: rpy2, anndata, Scanpy, 7 other tools, genetics / omics, 5 references
[4] doi:10.1038/s41592-026-03194-8 [code]
Beyond benchmarking: an expert-guided consensus approach to spatially aware clustering.
Journal: Nature methods
In common: rpy2, anndata, Scanpy, 7 other tools, genetics / omics, 5 references
[5] doi:10.1093/bioinformatics/btag540 [code]
Deciphering spatial heterogeneity by multimodal spatial transcriptomics modelling with SpatialModal.
Journal: Bioinformatics (Oxford, England)
In common: rpy2, anndata, Scanpy, 7 other tools, genetics / omics, 5 references
[6] doi:10.1016/j.isci.2026.116055 [code]
Mapping the transcriptional diversity of calcium signaling in the mouse and human brain.
Journal: iScience
In common: rpy2, anndata, Scanpy, 10 other tools, genetics / omics, 1 reference
[7] doi:10.1093/bioinformatics/btag430 [code]
SPIDER: spatially integrated denoising via embedding regularization with single cell supervision.
Journal: Bioinformatics (Oxford, England)
In common: rpy2, anndata, Scanpy, 6 other tools, genetics / omics, 5 references
[8] doi:10.1186/s13073-026-01704-z [code]
Gene expression profiling enables refined parcellation of cortical layers in the heterogeneous human cerebral cortex.
Journal: Genome medicine
In common: anndata, Scanpy, PyTorch, 6 other tools, db.cngb.org/data_resources/project, genetics / omics, 2 references
[9] 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: rpy2, anndata, Scanpy, 8 other tools, genetics / omics, 2 references
[10] doi:10.1016/j.stemcr.2026.103015 [code]
Brain injury reactivates a developmental program driving genesis and integration of transient LGE-class interneurons.
Journal: Stem cell reports
In common: anndata, Scanpy, NetworkX, 8 other tools, 3 references

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.