OSCR

Brain PDGFRβ+ Cells Exhibit Diverse Reactive Phenotypes After Stroke Without Requiring KLF4

Code ↔ Paper

21 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 21 matches
  1. [1] § Materials and Methods › Topological Data Analysis (TDA) ↔ Widefield_20x_ROIs_Pdgfrb_TDA.ipynb, lines 314 to 395 · score 0.80 · PointClouds, persistence diagrams, persistent homology, topological features, Ripser, dimension · Report a mismatch
  2. [2] § Results › Morphologically Distinct PDGFRβ + Cells Are Present at the Injured Tissue ↔ FlowCytometry_Pdgfrb_Processing.qmd, lines 477 to 562 · score 0.79 · Live Dead, geom_density2d, geom_stats, geom_gate, FACS_Pdgfrb, dead cells · Report a mismatch
  3. [3] § Results › Morphologically Distinct PDGFRβ + Cells Are Present at the Injured Tissue ↔ FlowCytometry_Pdgfrb_Processing.qmd, lines 476 to 561 · score 0.79 · Live Dead, geom_density2d, geom_stats, geom_gate, FACS_Pdgfrb, dead cells · Report a mismatch
  4. [4] § Materials and Methods › Fluorescence‐Activated Cell Sorting (FACS) ↔ FlowCytometry_Pdgfrb_Processing.qmd, lines 48 to 92 · score 0.75 · flowAI, flowCore, compensation controls, OSF repository, ggcyto, Bioconductor · Report a mismatch
  5. [5] § Materials and Methods › Fluorescence‐Activated Cell Sorting (FACS) ↔ FlowCytometry_Pdgfrb_Processing.qmd, lines 48 to 92 · score 0.75 · flowAI, flowCore, compensation controls, OSF repository, ggcyto, Bioconductor · Report a mismatch
  6. [6] § Materials and Methods › Data Analysis ↔ Widefield_5x_Ipsilateral_Gfap_Convex.qmd, lines 234 to 342 · score 0.73 · adapt_delta, max_treedepth, parameter space, Bayesian modeling, brms package, Loo · Report a mismatch
  7. [7] § Materials and Methods › Data Analysis ↔ Widefield_5x_Ipsilateral_Gfap_Convex.qmd, lines 234 to 342 · score 0.73 · adapt_delta, max_treedepth, parameter space, Bayesian modeling, brms package, Loo · Report a mismatch
  8. [8] § Results › Spatial and Topological Organization of Reactive PDGFRβ + Cells Upon Injury ↔ Widefield_20x_ROIs_Pdgfrb_TDA.ipynb, lines 205 to 253 · score 0.69 · animated vietoris rips, vietoris rips complexes, pdgfrb tda, widefield 20x, point clouds, filtration · Report a mismatch
  9. [9] § Results › KLF4 Induction in Reactive PDGFRβ + Cells Is Minimal Upon Ischemic Stroke ↔ Widefield_10x_Ipsilateral_KO_Klf4_Exp.qmd, lines 654 to 688 · score 0.66 · sigma Genotype, spread_draws, Probability density, stat_halfeye, KLF4 expression, KLF4 KO · Report a mismatch
  10. [10] § Results › KLF4 Induction in Reactive PDGFRβ + Cells Is Minimal Upon Ischemic Stroke ↔ Widefield_10x_Ipsilateral_KO_Klf4_Exp.qmd, lines 653 to 686 · score 0.66 · sigma Genotype, spread_draws, Probability density, stat_halfeye, KLF4 expression, KLF4 KO · Report a mismatch
  11. [11] § Results › Spatial and Topological Organization of Reactive PDGFRβ + Cells Upon Injury ↔ Widefield_20x_ROIs_Pdgfrb_TDA.ipynb, lines 700 to 791 · score 0.61 · dimensional homology, median distances, bottleneck distances, peri, CI, striatum · Report a mismatch
  12. [12] § Results › Reactive PDGFRβ + Cells Comprise Morphologically Diverse Populations ↔ Confocal_40x_ROIs_Pdgfrb_Morpho_Analysis.qmd, lines 317 to 349 · score 0.59 · Cartesian plane, opposite sides, negatively correlated, Cos2, Biplot, PCA · Report a mismatch
  13. [13] § Results › Reactive PDGFRβ + Cells Comprise Morphologically Diverse Populations ↔ Confocal_40x_ROIs_Pdgfrb_Morpho_Analysis.qmd, lines 317 to 349 · score 0.59 · Cartesian plane, opposite sides, negatively correlated, Cos2, Biplot, PCA · Report a mismatch
  14. [14] § Materials and Methods › Data Analysis ↔ Widefield_5x_Ipsilateral_Gfap_Convex.qmd, lines 234 to 342 · score 0.59 · Markov chain Monte, model comparison, brms package, Carlo, MCMC, statistical modeling · Report a mismatch
  15. [15] § Materials and Methods › Data Analysis ↔ Widefield_5x_Ipsilateral_Gfap_Convex.qmd, lines 234 to 342 · score 0.59 · Markov chain Monte, model comparison, brms package, Carlo, MCMC, statistical modeling · Report a mismatch
  16. [16] § Materials and Methods › Point Pattern Analysis (PPA) ↔ Widefield_10x_Ipsilateral_Ki67_PPA.qmd, lines 206 to 276 · score 0.57 · PointPatterns, spatstat package, hyperframes, ppp, PPA, rds · Report a mismatch
  17. [17] § Materials and Methods › Point Pattern Analysis (PPA) ↔ Widefield_10x_Ipsilateral_KO_Klf4_Exp.qmd, lines 198 to 267 · score 0.56 · PointPatterns, spatstat package, hyperframes, ppp, PPA, rds · Report a mismatch
  18. [18] § Results › Spatial and Topological Organization of Reactive PDGFRβ + Cells Upon Injury ↔ Widefield_20x_ROIs_Pdgfrb_Haralick.qmd, lines 651 to 729 · score 0.55 · local variation, inverse, Haralick, IDM, entropy, Ctx · Report a mismatch
  19. [19] § Results › Spatial and Topological Organization of Reactive PDGFRβ + Cells Upon Injury ↔ Widefield_20x_ROIs_Pdgfrb_Haralick.qmd, lines 651 to 729 · score 0.55 · local variation, inverse, Haralick, IDM, entropy, Ctx · Report a mismatch
  20. [20] § Materials and Methods ↔ Widefield_10x_ROIs_CD31-Pdgfrb_Coloc.qmd, lines 195 to 236 · score 0.53 · middle cerebral artery, sham animals, post ischemic, tamoxifen, occlusion, MCAo · Report a mismatch
  21. [21] § Results › Spatial and Topological Organization of Reactive PDGFRβ + Cells Upon Injury ↔ Widefield_10x_ROIs_KO_CD31-ColIV_Coloc.qmd, lines 414 to 493 · score 0.51 · geom_point, add_predicted_draws, stat_halfeye, CD31, sigma, KLF4 · Report a mismatch

Paper

Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 1,065 lines · 45 KB · no license · 3 matches

  1. # %% [markdown]
  2. # # Topological Data Analysis (TDA) of PDGFR-β+ cells
  3. # %% [markdown]
  4. # ## Preview
  5. #
  6. # This notebook reports the topological data analysis (TDA) for PDGFR-β cells in specific brain regions (injured striatum and cortex, and perilesional tissue). Please note that this notebook runs Python code.
  7. #
  8. # **Parent dataset:** PDGFR-β(Td-tomato) ROIs imaged at 20x in different Z-planes. Images were z-projected for analysis. Samples are grouped at 0 (Sham), 3, 7, 14, and 30 days post-ischemia (DPI). The raw images and pre-processing scripts (if applicable) are available at the Zenodo repository (10.5281/zenodo.10553084) under the name `Widefield_20x_ROIs_Pdgfrb.zip`.
  9. #
  10. # **Working dataset**: The `Raw_Data/Widefield_20x_ROIs_Pdgfrb/CellProfiler/Cells.csv`data frame containing the individual xy coordinates of PDGFR-β+ cells generated with CellProfiler [@stirling2021].
  11. #
  12. # **Note:** This notebook runs using python code.
  13. # %% [markdown]
  14. # ## Load required libraries
  15. #
  16. # We import the required Python libraries.
  17. # %%
  18. import pandas as pd
  19. import numpy as np
  20. import seaborn as sns
  21. import os
  22. import csv
  23. import matplotlib.pyplot as plt
  24. from matplotlib.collections import LineCollection
  25. from matplotlib.animation import FuncAnimation
  26. from matplotlib.patches import Circle
  27. import gudhi as gd
  28. from gudhi import RipsComplex, plot_persistence_diagram
  29. from gudhi import bottleneck_distance
  30. from gudhi.wasserstein import wasserstein_distance
  31. import itertools
  32. from ripser import ripser
  33. from persim import plot_diagrams
  34. from random import choices
  35. from scipy.spatial import ConvexHull
  36. # %% [markdown]
  37. # ## Load and handle the dataset
  38. #
  39. # We load the `Raw_Data/Widefield_20x_ROIs_Pdgfrb/CellProfiler/Cells.csv` dataset containing the cell coordinates. We handle the data to obtain and organize the relevant metadata from the file name.
  40. # %%
  41. # Load the dataset
  42. file_path = 'Data_Raw/Widefield_20x_ROIs_Pdgfrb/CellProfiler/Cells.csv'
  43. data = pd.read_csv(file_path)
  44. # Keep only the specified columns
  45. data = data[['Location_Center_X', 'Location_Center_Y', 'FileName_Raw']]
  46. # Define a function to parse the FileName_Raw column
  47. def parse_filename(filename):
  48. parts = filename.split('_')
  49. # Extract the needed parts based on their position
  50. MouseID = parts[0]
  51. DPI = parts[1]
  52. Condition = parts[2]
  53. Lesion = parts[3]
  54. Region = parts[4]
  55. return MouseID, DPI, Condition, Lesion, Region
  56. # Apply the function to the DataFrame
  57. data[['MouseID', 'DPI', 'Condition', 'Lesion', 'Region']] = data.apply(lambda row: parse_filename(row['FileName_Raw']), axis=1, result_type='expand')
  58. # Drop the FileName_Raw column
  59. data.drop('FileName_Raw', axis=1, inplace=True)
  60. # Reorder columns to move Location_Center_X and Location_Center_Y to the right
  61. column_order = ['MouseID', 'DPI', 'Condition', 'Lesion', 'Region', 'Location_Center_X', 'Location_Center_Y']
  62. data = data[column_order]
  63. # Save the updated dataset to a new file
  64. output_file_path = 'Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Coords.csv'
  65. data.to_csv(output_file_path, index=False)
  66. print("Dataset updated and saved to:", output_file_path)
  67. # %% [markdown]
  68. # ## Calculate Vietoris-Rips Complex
  69. #
  70. # In this section, we generate Vietoris-Rips complex for elucidating the topological features embedded within high-dimensional datasets. We construct the complexes from a finite set of points in a metric space to facilitate the analysis of their spatial relationships through a simplicial complex. The construction follows this scheme:
  71. #
  72. # Let $S$ be a finite set of points in a metric space. The Vietoris-Rips complex, denoted as $ VR_{\epsilon}(S) $, is defined for a chosen distance threshold $ \epsilon $. The construction process is as follows:
  73. #
  74. # 1. **Vertices**: Each point in $ S $ is considered a vertex, or 0-simplex.
  75. #
  76. # 2. **Edges**: An edge, or 1-simplex, is formed between any pair of vertices $ (u, v) $ if the distance between them, denoted $ d(u, v) $, satisfies the condition $ d(u, v) \leq \epsilon $.
  77. #
  78. # 3. **Higher-Dimensional Simplices**: A $ k $-simplex is formed by a set of $ k+1 $ vertices if every pair of vertices within this set is connected by an edge. Mathematically, for a subset $ \{v_0, v_1, \ldots, v_k\} \subseteq S $, a $ k $-simplex exists if:
  79. #
  80. # $$
  81. # \forall i, j \in \{0, 1, \ldots, k\},\, d(v_i, v_j) \leq \epsilon
  82. # $$
  83. #
  84. # 4. **Complex Assembly**: The Vietoris-Rips complex $ VR_{\epsilon}(S) $ comprises all simplices, from 0-simplices to the highest dimension formed under the given threshold $ \epsilon $.
  85. #
  86. # We load the cell coordinates to build the Vietoris-Rips complexes. We set `100` as the maximum filtration value to facilitate visualization.
  87. # %%
  88. # Load the organized dataset
  89. data_path = 'Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Coords.csv' # Adjust this path as necessary
  90. data = pd.read_csv(data_path)
  91. # Group the dataset by MouseID and Region, then extract the point clouds
  92. point_clouds_by_mouse_region = data.groupby(['MouseID', 'DPI', 'Region']).apply(lambda df: df[['Location_Center_X', 'Location_Center_Y']].values).to_dict()
  93. def calculate_vietoris_rips_complex(point_cloud, max_dim, max_radius):
  94. """
  95. Calculate the Vietoris-Rips complex for a given point cloud.
  96. Parameters:
  97. - point_cloud: numpy array of shape (n_points, n_dimensions) representing the point cloud.
  98. - max_dim: int, the maximal dimension of the simplices in the Vietoris-Rips complex.
  99. - max_radius: float, the maximal radius for simplices in the Vietoris-Rips complex.
  100. Returns:
  101. - simplex_tree: a GUDHI SimplexTree object representing the Vietoris-Rips complex.
  102. """
  103. rips_complex = gd.RipsComplex(points=point_cloud, max_edge_length=max_radius)
  104. simplex_tree = rips_complex.create_simplex_tree(max_dimension=max_dim)
  105. return simplex_tree
  106. # Parameters for Vietoris-Rips complex calculation
  107. max_dim = 3
  108. max_radius = 100
  109. # Calculate Vietoris-Rips complexes for each region of each animal
  110. vietoris_rips_complexes = {}
  111. for (mouse_id, dpi, region), point_cloud in point_clouds_by_mouse_region.items():
  112. key = (mouse_id, dpi, region) # Use a tuple of MouseID and Region as the key
  113. vietoris_rips_complexes[key] = calculate_vietoris_rips_complex(point_cloud, max_dim, max_radius)
  114. # %% [markdown]
  115. # ### Visualize and save Vietoris-Rips Complex
  116. #
  117. # Once calculated, we save the images in a designated folder:
  118. # %%
  119. def plot_vietoris_rips_complex(point_cloud, simplex_tree, filename, save_dir, x_range=None, y_range=None):
  120. """
  121. Plot and save the Vietoris-Rips complex for a given 2D point cloud, with specified x and y axis ranges.
  122. Parameters:
  123. - point_cloud: numpy array of shape (n_points, 2) representing the 2D point cloud.
  124. - simplex_tree: a gudhi object representing the Vietoris-Rips complex.
  125. - filename: str, the name of the file to save the image.
  126. - save_dir: str, directory to save the image.
  127. - x_range: tuple, the range for the x-axis (optional).
  128. - y_range: tuple, the range for the y-axis (optional).
  129. """
  130. fig, ax = plt.subplots(figsize=(6, 4))
  131. # Plot points
  132. ax.scatter(point_cloud[:, 0], point_cloud[:, 1], c='black', s=20, alpha=0.8)
  133. # Plot edges
  134. edges = []
  135. for simplex in simplex_tree.get_skeleton(2): # Get up to 2-simplices (edges)
  136. if len(simplex[0]) == 2: # Edges
  137. p1, p2 = point_cloud[simplex[0][0]], point_cloud[simplex[0][1]]
  138. edges.append([p1, p2])
  139. lc = LineCollection(edges, colors='green', linewidths=1, alpha=0.7)
  140. ax.add_collection(lc)
  141. # Adjust plot settings
  142. ax.set_xticks([])
  143. ax.set_yticks([])
  144. ax.set_xlabel('')
  145. ax.set_ylabel('')
  146. if x_range is not None:
  147. ax.set_xlim(x_range)
  148. if y_range is not None:
  149. ax.set_ylim(y_range)
  150. plt.tight_layout()
  151. if not os.path.exists(save_dir):
  152. os.makedirs(save_dir)
  153. plt.savefig(os.path.join(save_dir, f"{filename}.png"))
  154. plt.close()
  155. # Directory to save the images
  156. output_dir = "D:/Research/Stroke_PDGFR-B_Reactivity/Images/Widefield_20x-ROIs_Pdgfrb_TDA/VietorisRips_Static" # Update this path
  157. x_range = (0, 2000) # Example x-axis range
  158. y_range = (0, 1500) # Example y-axis range
  159. aspect_ratio = 'auto' # Adjust this based on your data or preferences
  160. # Assuming vietoris_rips_complexes and point_clouds_by_mouse have been computed accordingly
  161. for (mouse_id, dpi, region), simplex_tree in vietoris_rips_complexes.items():
  162. filename = f"{mouse_id}_{dpi}_{region}_VietorisRips"
  163. point_cloud = point_clouds_by_mouse_region[(mouse_id, dpi, region)]
  164. plot_vietoris_rips_complex(point_cloud, simplex_tree, filename, output_dir, x_range, y_range)
  165. # %% [markdown]
  166. # ### Visualize and save Vietoris-Rips Complex (animated)
  167. #
  168. # We created animated Vietoris-rips for illustration in the scientific article. We save animations with a maximum of `300` filtration value.
  169. # %%
  170. def animate_vietoris_rips_mp4(point_cloud, simplex_tree, filename, save_dir, max_radius, step_size=1):
  171. """
  172. Create an .mp4 animation illustrating the growth of the Vietoris-Rips complex for 2D data.
  173. """
  174. fig, ax = plt.subplots(figsize=(7, 7))
  175. ax.scatter(point_cloud[:, 0], point_cloud[:, 1], c='black', s=20, alpha=0.8)
  176. # Function to update the plot for each frame
  177. def update(threshold):
  178. ax.cla() # Clear current plot details
  179. ax.scatter(point_cloud[:, 0], point_cloud[:, 1], c='black', s=10, alpha=0.7)
  180. edges = []
  181. for simplex in simplex_tree.get_skeleton(2):
  182. if simplex[1] <= threshold:
  183. if len(simplex[0]) == 2: # Edges
  184. p1, p2 = point_cloud[simplex[0][0]], point_cloud[simplex[0][1]]
  185. edges.append([p1, p2])
  186. lc = LineCollection(edges, colors='green', alpha=0.7)
  187. ax.add_collection(lc)
  188. ax.set_title(f"Threshold: {threshold:.2f}", fontsize=20)
  189. ax.set_xlabel('X', fontsize=20, labelpad=15)
  190. ax.set_ylabel('Y', fontsize=20, labelpad=15)
  191. ax.tick_params(axis='both', which='major', labelsize=20)
  192. ax.axis('equal')
  193. ani = FuncAnimation(fig, update, frames=np.arange(0, max_radius, step_size), blit=False)
  194. ani_file = os.path.join(save_dir, f"{filename}_Animation.mp4")
  195. ani.save(ani_file, writer='ffmpeg', fps=2, extra_args=['-vcodec', 'libx264'])
  196. plt.close()
  197. return ani_file # Return the file path of the saved animation
  198. # Example usage
  199. output_dir = "D:/Research/Stroke_PDGFR-B_Reactivity/Images/Widefield_20x-ROIs_Pdgfrb_TDA/VietorisRips_Animated"
  200. max_radius = 300 # Adjust as necessary
  201. step_size = 20 # Adjust as necessary
  202. for (mouse_id, dpi, region), simplex_tree in vietoris_rips_complexes.items():
  203. filename = f"{mouse_id}_{dpi}_{region}_VietorisRips"
  204. point_cloud = point_clouds_by_mouse_region[(mouse_id, dpi, region)]
  205. ani_file = animate_vietoris_rips_mp4(point_cloud, simplex_tree, filename, output_dir, max_radius, step_size)
  206. print(f"Saved animation: {ani_file}")
  207. # %% [markdown]
  208. # ## Generate point clouds
  209. #
  210. # We generate point clouds and store them as individual files to use along the TDA. First, we load again the dataset in case is not present in the environment.
  211. # %%
  212. # Load your dataset
  213. data_path = 'Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Coords.csv'
  214. data = pd.read_csv(data_path)
  215. # Group data by MouseID, Region, and DPI
  216. point_clouds_by_group = data.groupby(['MouseID', 'DPI', 'Region',]).apply(
  217. lambda df: df[['Location_Center_X', 'Location_Center_Y']].values
  218. ).to_dict()
  219. # %% [markdown]
  220. # Next, we verify the structire of the data to make sure is grouped as intented.
  221. # %%
  222. if not point_clouds_by_group:
  223. print("The point_clouds_by_group dictionary is empty.")
  224. else:
  225. print("Point clouds loaded:", len(point_clouds_by_group))
  226. # Inspect the first few items to ensure they're structured correctly
  227. for key, value in list(point_clouds_by_group.items())[:5]:
  228. print(key, value.shape)
  229. # %% [markdown]
  230. # Then, we save point clouds as .npy files (Numpy array) in a designated folder
  231. # %%
  232. def save_point_clouds_as_npy(point_clouds_by_group, data_directory):
  233. """
  234. Saves point clouds to .npy files in the specified directory.
  235. Parameters:
  236. point_clouds_by_group (dict): Dictionary with keys as identifiers
  237. (e.g., ('MouseID', 'Region', 'DPI')) and values as numpy arrays representing point clouds.
  238. data_directory (str): Path to the directory where .npy files will be saved.
  239. """
  240. # Ensure the directory exists
  241. if not os.path.exists(data_directory):
  242. os.makedirs(data_directory)
  243. # Iterate through the point clouds and save each as an .npy file
  244. for identifier, point_cloud in point_clouds_by_group.items():
  245. # Create a filename from the identifier
  246. filename = "_".join(identifier) + ".npy"
  247. filepath = os.path.join(data_directory, filename)
  248. np.save(filepath, point_cloud)
  249. print("Point clouds saved as .npy files.")
  250. data_directory = r"PointClouds/Widefield_20x_ROIs_Pdgfrb/"
  251. # Call the function with your data and desired save location
  252. save_point_clouds_as_npy(point_clouds_by_group, data_directory)
  253. # %% [markdown]
  254. # ## Computation of persistent homology
  255. # %% [markdown]
  256. # In this section, we analize the generated point clouds using persisten homology. We use the same data the `process_and_visualize_persistence` function to process the same dataset containing xy coordinates of PDGFR-B+ cells. We compute persistent diagrams using the `ripster` libary. These diagrams capture the birth and death of topological features (like loops or voids) in the data as one varies a filtering parameter. The persistence diagrams are then visualized using the `persim` library.
  257. #
  258. # This analysis provide insights into the topological structure of the data, indicating features that persist across scales. In TDA, features that persist longer (i.e., have a wider gap between birth and death) are deemed more significant as they represent genuine structures rather than noise. Then, we save each persistence diagram as a .png image.
  259. # %% [markdown]
  260. # ### Compute persistence diagrams
  261. # %% [markdown]
  262. # We compute persistence diagrams and save then as .npy file for further analysis and as .png images for visualization
  263. # %%
  264. # Disable LaTeX usage in matplotlib
  265. plt.rcParams['text.usetex'] = False
  266. def process_npy_for_persistence_diagrams(data_directory, save_directory, save_diag_directory, img_size=(6, 6), x_range=None, y_range=None):
  267. """
  268. Batch process .npy files (containing point clouds) to compute, visualize, and save persistence diagrams.
  269. """
  270. # Ensure save directories exist
  271. if not os.path.exists(save_directory):
  272. os.makedirs(save_directory)
  273. if not os.path.exists(save_diag_directory):
  274. os.makedirs(save_diag_directory)
  275. # List all .npy files in the data directory
  276. npy_files = [f for f in os.listdir(data_directory) if f.endswith('.npy')]
  277. for file in npy_files:
  278. # Load the point cloud from the .npy file
  279. points_array = np.load(os.path.join(data_directory, file))
  280. # Compute the persistence diagrams using Ripser
  281. diagrams = ripser(points_array, maxdim=1)['dgms']
  282. # Save each dimension's persistence diagram as a separate .npy file
  283. for dim, diag in enumerate(diagrams):
  284. diag_filename = os.path.splitext(file)[0] + f"_persistence_diag_dim{dim}.npy"
  285. np.save(os.path.join(save_diag_directory, diag_filename), diag)
  286. # Visualize and save the persistence diagrams
  287. fig, ax = plt.subplots(figsize=img_size)
  288. plot_diagrams(diagrams, show=False, ax=ax)
  289. ax.set_title("")
  290. ax.set_xlabel('Birth', fontsize=25)
  291. ax.set_ylabel('Death', fontsize=25)
  292. ax.tick_params(axis='both', which='major', labelsize=25)
  293. # Optionally adjust the legend
  294. legend = ax.legend()
  295. if legend:
  296. for label in legend.get_texts():
  297. label.set_fontsize(20)
  298. plt.tight_layout()
  299. if x_range is not None:
  300. ax.set_xlim(x_range)
  301. if y_range is not None:
  302. ax.set_ylim(y_range)
  303. # Save the figure
  304. filename = os.path.splitext(file)[0] + "_persistence_diagram.png"
  305. fig.savefig(os.path.join(save_directory, filename))
  306. plt.close()
  307. print(f"Persistence diagrams saved to {save_directory}.")
  308. print(f"Persistence diagrams data saved to {save_diag_directory}.")
  309. # Paths to your directories
  310. data_directory = r"PointClouds/Widefield_20x_ROIs_Pdgfrb/"
  311. save_directory = r"D:/Research/Stroke_PDGFR-B_Reactivity/Images/Widefield_20x-ROIs_Pdgfrb_TDA/PersistenceDiagrams"
  312. save_diag_directory = r"Data_Raw/Widefield_20x_ROIs_Pdgfrb/PersistenceDiagrams/"
  313. #x_range = (0, 500)
  314. #y_range = (0, 500)
  315. # Call the function to process, visualize, and save persistence diagrams
  316. process_npy_for_persistence_diagrams(data_directory, save_directory, save_diag_directory)
  317. # %% [markdown]
  318. # ## Calculate Betti curves for the persistent diagrams
  319. # %% [markdown]
  320. # Now, we use Betti curves to encapsulate the evolution of topological features across a spectrum of different scales. We can visualize how connected components ($\beta_0$) and loops ($\beta_1$) change as a function of a scaling parameter,
  321. # The Betti curve for each dimension is a function mapping the scaling parameter to the corresponding Betti number (not shown here).
  322. #
  323. # Here, we compute Betti curves in different chunks for 0D-Homology and 1D-homology. To eliminte bias for the number of connected components, we normalize 0D-hoology Betti curves bubt not 1D-homology (loops). We calculate the Betti curvas by region and plot different lines for each DPI.
  324. # %% [markdown]
  325. # ### Betti curves for 0D homology
  326. # %%
  327. def load_persistence_diagrams(dim0_path):
  328. files = [f for f in os.listdir(dim0_path) if 'dim0' in f]
  329. data = {}
  330. for file in files:
  331. parts = file.split('_')
  332. day, region = parts[1], parts[2]
  333. diagram = np.load(os.path.join(dim0_path, file))
  334. key = (day, region)
  335. data[key] = diagram
  336. return data
  337. def compute_betti_0_curve(persistence_diagram, max_filtration_value, n_steps):
  338. betti_0_curve = []
  339. filtration_values = np.linspace(0, max_filtration_value, n_steps)
  340. for value in filtration_values:
  341. betti_0 = sum(1 for point in persistence_diagram if point[0] <= value and (point[1] > value or point[1] == float('inf')))
  342. betti_0_curve.append(betti_0)
  343. # Normalize the curve to start at 1
  344. if betti_0_curve[0] != 0: # Ensure there is no division by zero
  345. initial_value = betti_0_curve[0]
  346. normalized_betti_0_curve = [b / initial_value for b in betti_0_curve]
  347. else:
  348. # Handle the case where the initial value is 0
  349. normalized_betti_0_curve = betti_0_curve
  350. return normalized_betti_0_curve
  351. dim0_path = "Data_Raw/Widefield_20x_ROIs_Pdgfrb/PersistenceDiagrams/"
  352. dim0_data = load_persistence_diagrams(dim0_path)
  353. n_steps = 300
  354. max_filtration_value = 300
  355. betti_0_curves = {key: compute_betti_0_curve(diagram, max_filtration_value, n_steps) for key, diagram in dim0_data.items()}
  356. # Aggregating Betti curves by region
  357. aggregated_curves_by_region_0 = {}
  358. for (day, region), curve in betti_0_curves.items():
  359. if region not in aggregated_curves_by_region_0:
  360. aggregated_curves_by_region_0[region] = {}
  361. aggregated_curves_by_region_0[region][day] = curve
  362. def plot_aggregated_betti_curves_0(aggregated_curves_by_region_0, save_directory):
  363. if not os.path.exists(save_directory):
  364. os.makedirs(save_directory)
  365. days_order = ['0D', '3D', '7D', '14D', '30D']
  366. day_colors = {
  367. '0D': 'red',
  368. '3D': 'blue',
  369. '7D': 'green',
  370. '14D': 'purple',
  371. '30D': 'orange'
  372. }
  373. for region, curves in aggregated_curves_by_region_0.items():
  374. plt.figure(figsize=(5, 5))
  375. for day in days_order:
  376. if day in curves:
  377. plt.plot(curves[day], label=day, color=day_colors[day])
  378. plt.xlabel("Filtration Value", fontsize=22)
  379. plt.ylabel("Normalized Betti-0", fontsize=22)
  380. plt.xticks(fontsize=22, rotation=45)
  381. plt.yticks(fontsize=22)
  382. leg = plt.legend(title="Day", fontsize=16)
  383. plt.setp(leg.get_title(), fontsize=16)
  384. plt.tight_layout()
  385. plot_path = os.path.join(save_directory, f"Betti0_Curves_{region}.png")
  386. plt.savefig(plot_path)
  387. print(f"Saved Betti-0 curve plot for {region} at {plot_path}")
  388. plt.show()
  389. plt.close()
  390. save_directory = "D:/Research/Stroke_PDGFR-B_Reactivity/Pdgfrb_Reactivity_DataAnalysis/Stroke_Pdgfrb_Reactivity/Plots/Widefield_20x_ROIs_pdgfrb_TDA"
  391. plot_aggregated_betti_curves_0(aggregated_curves_by_region_0, save_directory)
  392. # %% [markdown]
  393. # ### Betti curves for 1D homology
  394. #
  395. # Now, we calculate the Betti curves for 1D homoogy.
  396. # %%
  397. def load_persistence_diagrams(dim1_path):
  398. files = [f for f in os.listdir(dim1_path) if 'dim1' in f]
  399. data = {}
  400. for file in files:
  401. parts = file.split('_')
  402. day, region = parts[1], parts[2]
  403. diagram = np.load(os.path.join(dim1_path, file))
  404. key = (day, region)
  405. data[key] = diagram
  406. return data
  407. dim1_path = "Data_Raw/Widefield_20x_ROIs_Pdgfrb/PersistenceDiagrams/"
  408. dim1_data = load_persistence_diagrams(dim1_path)
  409. def compute_betti_1_curve(persistence_diagram, max_filtration_value, n_steps):
  410. betti_1_curve = []
  411. filtration_values = np.linspace(0, max_filtration_value, n_steps)
  412. for value in filtration_values:
  413. betti_1 = sum(1 for point in persistence_diagram if point[0] <= value and (point[1] > value or point[1] == float('inf')))
  414. betti_1_curve.append(betti_1)
  415. return betti_1_curve
  416. n_steps = 100
  417. max_filtration_value = 700
  418. betti_1_curves = {key: compute_betti_1_curve(diagram, max_filtration_value, n_steps) for key, diagram in dim1_data.items()}
  419. # Aggregating Betti curves by region
  420. aggregated_curves_by_region_1 = {}
  421. for (day, region), curve in betti_1_curves.items():
  422. if region not in aggregated_curves_by_region_1:
  423. aggregated_curves_by_region_1[region] = {}
  424. aggregated_curves_by_region_1[region][day] = curve
  425. def plot_aggregated_betti_curves_1(aggregated_curves_by_region_1, save_directory):
  426. if not os.path.exists(save_directory):
  427. os.makedirs(save_directory)
  428. days_order = ['0D', '3D', '7D', '14D', '30D']
  429. day_colors = {
  430. '0D': 'red',
  431. '3D': 'blue',
  432. '7D': 'green',
  433. '14D': 'purple',
  434. '30D': 'orange'
  435. }
  436. for region, curves in aggregated_curves_by_region_1.items():
  437. plt.figure(figsize=(5, 5)) # Adjust figure size as needed
  438. for day in days_order:
  439. if day in curves:
  440. plt.plot(curves[day], label=day, color=day_colors[day])
  441. plt.xlabel("Filtration Value", fontsize=22)
  442. plt.ylabel("Betti-1", fontsize=22)
  443. plt.xticks(fontsize=22) # Increase tick label size
  444. plt.yticks(fontsize=22) # Increase tick label size
  445. leg = plt.legend(title="Day", fontsize=16)
  446. plt.setp(leg.get_title(), fontsize=16) # Increase legend title size
  447. plt.tight_layout()
  448. # Save the plot before calling plt.show()
  449. plot_path = os.path.join(save_directory, f"Betti1_Curves_{region}.png")
  450. plt.savefig(plot_path)
  451. print(f"Saved Betti-1 curve plot for {region} at {plot_path}")
  452. # Show the plot inline
  453. plt.show()
  454. plt.close()
  455. # Specify the directory where you want to save the plots
  456. save_directory = "Plots/Widefield_20x_ROIs_pdgfrb_TDA/"
  457. # Call the function with your aggregated Betti curves and the save directory
  458. plot_aggregated_betti_curves_1(aggregated_curves_by_region_1, save_directory)
  459. # %% [markdown]
  460. # ## Calculate Bottleneck distances
  461. #
  462. # Now, we use the bottleneck distance to compare the persistent diagrams. Given two persistence diagrams D1 and D2, the bottleneck distance measures the minimum amount one needs to move points in D1 to match points in D2 and vice versa, such that the worst-case (or maximum) movement is minimized. Mathematically, the bottleneck distance is defined as:
  463. #
  464. # $$
  465. # \text{bottleneck}(D_1, D_2) = \inf_{\phi: D_1 \to D_2} \sup_{x \in D_1} ||x - \phi(x)||_{\infty}
  466. # $$
  467. #
  468. # Performing bootstrapping, we estimate the distribution of the bottleneck distances by resampling with replacement from the data. Then, we repeat the sampling 1000 times to compute 95% confidence intervals.
  469. #
  470. # In our analysis, we load organize persistence diagrams corresponding to various days, regions, and proteins from designated file paths. The diagrams are gouped based on the mentioned factors and perform specific pairwise day comparisons. We use bootstrap iterations to obtain bottleneck distances between the persistence diagrams of two distinct days, representing the topological disparity between them. These results are stored in a CSV file and the visualized as a heat map.
  471. # %% [markdown]
  472. # ### Load persistance diagrams
  473. #
  474. # We load the persistent diagrams by **0D** or **1D** homology Interchangeably. From each diagram, we obtain the grouping metadata and perform the bootstraping procedure to calculate Bottlenecks distances. Please note this procedure is expensive and make take hours to complete.
  475. #
  476. # **Note:** To shift focus to dimension 1 persistence diagrams, one can replace 'dim0.npy' with 'dim1.npy' in the code.
  477. # %%
  478. ### To calculate dimension 1, change the dim0 to dim1
  479. # Define the path to persistence diagrams
  480. path = "Data_Raw/Widefield_20x_ROIs_Pdgfrb/PersistenceDiagrams/"
  481. # Load .npy files for dimension 1 diagrams
  482. files = [f for f in os.listdir(path) if f.endswith('dim1.npy')]
  483. # Organize data into a nested dictionary {day: {region: [diagrams, ...], ...}, ...}
  484. data = {}
  485. for file in files:
  486. # Extract information from the filename
  487. parts = file.split("_")
  488. day, region = parts[1], parts[2]
  489. # Load the persistence diagram
  490. diagram = np.load(os.path.join(path, file))
  491. # Initialize day if not already in data
  492. if day not in data:
  493. data[day] = {}
  494. # Append the diagram to the correct day and region
  495. data[day].setdefault(region, []).append(diagram)
  496. # %% [markdown]
  497. # Next, we verifying the loaded data structure
  498. # %%
  499. for day in data:
  500. print(f"Day: {day}")
  501. for region in data[day]:
  502. num_diagrams = len(data[day][region])
  503. print(f" Region: {region}, Number of diagrams: {num_diagrams}")
  504. # Optionally, print out some details of the first diagram as an example
  505. if num_diagrams > 0:
  506. example_diagram = data[day][region][0]
  507. print(f" Example diagram size: {example_diagram.shape}")
  508. print(f" Example diagram content (first few pairs): {example_diagram[:5]}")
  509. else:
  510. print(" No diagrams for this region and day.")
  511. print("\n")
  512. # %% [markdown]
  513. # ### Computing distances by boostraping and saving metrics
  514. #
  515. # Now we perform the analysis between persistence diagrams for specified pairwise day comparisons.First, we define the specific pairwise day comparisons of interest (0D-3D, 3D-7D, 7D-14D, 14D-30D) and extract unique days, regions, and proteins from the data. For each unique combination of Region and Protein, we extract persistence diagrams associated with each day. The region Peri (Perilesional) does not contain information from 0D. Therefore, we skip to the next iteration.
  516. # %%
  517. # Define the specified pairwise day comparisons
  518. day_pairs = [("0D", "3D"), ("3D", "7D"), ("7D", "14D"), ("14D", "30D")]
  519. # Extract unique days and regions from the data
  520. unique_days = set(data.keys())
  521. unique_regions = set(region for day in data for region in data[day])
  522. results = []
  523. # Define number of bootstrap replications
  524. num_replications = 1000
  525. # For each unique combination of Day and Region
  526. for region in unique_regions:
  527. # Perform the specified pairwise comparison of diagrams from different days
  528. for day1, day2 in day_pairs:
  529. bootstrap_distances = []
  530. for _ in range(num_replications):
  531. # Randomly sample diagrams from day1 and day2 with replacement
  532. bootstrap_sample1 = choices(data[day1].get(region, []), k=len(data[day1].get(region, [])))
  533. bootstrap_sample2 = choices(data[day2].get(region, []), k=len(data[day2].get(region, [])))
  534. # Compute pairwise bottleneck distances for the bootstrap samples
  535. distances = [bottleneck_distance(diag1, diag2) for diag1 in bootstrap_sample1 for diag2 in bootstrap_sample2 if diag1.size and diag2.size]
  536. # Only proceed if there are distances to calculate
  537. if distances:
  538. # Compute the median distance for this bootstrap replication
  539. bootstrap_distances.append(np.median(distances))
  540. # Skip if no distances were calculated
  541. if not bootstrap_distances:
  542. continue
  543. # Compute metrics
  544. median_distance = np.median(bootstrap_distances)
  545. confidence_lower = np.percentile(bootstrap_distances, 2.5)
  546. confidence_upper = np.percentile(bootstrap_distances, 97.5)
  547. # Store the results
  548. results.append((day1, day2, region, median_distance, confidence_lower, confidence_upper))
  549. # Define the results path and save results to CSV
  550. results_path = "Data_Processed//Widefield_20x_ROIs_Pdgfrb/"
  551. # Change the name for 1D
  552. csv_file = os.path.join(results_path, "Widefield_20x_ROIs_Pdgfrb_Bottleneck_0d.csv")
  553. with open(csv_file, 'w', newline='') as csvfile:
  554. csvwriter = csv.writer(csvfile)
  555. # Write header
  556. csvwriter.writerow(['Day1', 'Day2', 'Region', 'Median Bottleneck Distance', '95% CI Lower', '95% CI Upper'])
  557. # Write the results
  558. for row in results:
  559. csvwriter.writerow(row)
  560. print(f"Bootstrap results saved to {csv_file}.")
  561. # %% [markdown]
  562. # ### Visualization of bottleneck distances by heatmap
  563. # %% [markdown]
  564. # We vislauzie the bottleneck distances by heatmap using seaborn:
  565. # %%
  566. # Load CSV data for 0-dimension and 1-dimension homology
  567. csv_path_dim0 = os.path.join("Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Bottleneck_0d.csv")
  568. csv_path_dim1 = os.path.join("Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Bottleneck_1d.csv")
  569. data_dim0 = pd.read_csv(csv_path_dim0)
  570. data_dim1 = pd.read_csv(csv_path_dim1)
  571. # Assuming the structure of your CSV files matches the expected columns
  572. # Define label mappings and structure
  573. regions = ["Peri", "Str", "Ctx"]
  574. region_labels = ["Perilesion", "Striatum", "Cortex"]
  575. days = ["0D-3D", "3D-7D", "7D-14D", "14D-30D"]
  576. dimensions = ["dim0", "dim1"] # Representing 0-dimension and 1-dimension homology
  577. # Initialize an empty dictionary to store heatmap data for each dimension
  578. heatmap_data = {dim: pd.DataFrame(index=regions, columns=days) for dim in dimensions}
  579. heatmap_annotations = {dim: pd.DataFrame(index=regions, columns=days) for dim in dimensions}
  580. # Function to populate matrices with data from CSV
  581. def populate_heatmap(data, dim):
  582. for _, row in data.iterrows():
  583. day_pair, region, median_distance, lower, upper = row['Day1']+'-'+row['Day2'], row['Region'], round(row['Median Bottleneck Distance']), round(row['95% CI Lower']), round(row['95% CI Upper'])
  584. if region in regions:
  585. heatmap_data[dim].loc[region, day_pair] = median_distance
  586. # Adjust annotation format here to round and remove decimals
  587. annotation = f"{median_distance}\n({lower} - {upper})"
  588. heatmap_annotations[dim].loc[region, day_pair] = annotation
  589. populate_heatmap(data_dim0, "dim0")
  590. populate_heatmap(data_dim1, "dim1")
  591. # Convert the data to numeric and handle NaN values
  592. for dim in dimensions:
  593. heatmap_data[dim] = heatmap_data[dim].astype(float)
  594. heatmap_data[dim].fillna(-1, inplace=True) # Fill NaN values with -1 for missing data
  595. # Visualization
  596. fig, axes = plt.subplots(1, 2, figsize=(16, 7), sharey=True)
  597. for ax, dim in zip(axes, dimensions):
  598. sns.heatmap(heatmap_data[dim], annot=heatmap_annotations[dim], cmap='viridis', ax=ax,
  599. cbar_kws={'label': 'Bottleneck Distance', 'shrink': 0.5},
  600. mask=(heatmap_data[dim] == -1), fmt="", annot_kws={"size": 16})
  601. ax.set_title(f"Dimension: {dim[-1]}", fontsize=16)
  602. ax.set_xlabel("Day Pair", fontsize=16)
  603. ax.set_ylabel("Region", fontsize=16)
  604. ax.set_yticklabels(region_labels, fontsize=16, rotation=0)
  605. ax.tick_params(axis='x', labelsize=16)
  606. # Adjust colorbar label size
  607. cbar = ax.collections[0].colorbar
  608. cbar.ax.tick_params(labelsize=16)
  609. cbar.ax.set_ylabel('Bottleneck Distance', fontsize=16)
  610. plt.tight_layout()
  611. plt.show()
  612. # Update the output directory path for saving individual images
  613. output_dir = "Plots/Widefield_20x_ROIs_pdgfrb_TDA"
  614. # Ensure the output directory exists
  615. if not os.path.exists(output_dir):
  616. os.makedirs(output_dir)
  617. # Save individual heatmap images for each dimension
  618. for dim in dimensions:
  619. fig, ax = plt.subplots(figsize=(7, 5))
  620. sns.heatmap(heatmap_data[dim], annot=heatmap_annotations[dim], cmap='viridis', ax=ax,
  621. cbar_kws={'label': 'Bottleneck Distance'},
  622. mask=(heatmap_data[dim] == -1), fmt="s")
  623. ax.set_title(f"Bottleneck Distances (Dimension: {dim[-1]})", fontsize=16)
  624. ax.set_xlabel("Day Pair", fontsize=16)
  625. ax.set_ylabel("Region", fontsize=16)
  626. ax.set_yticklabels(region_labels, fontsize=16)
  627. ax.tick_params(axis='x', labelsize=16)
  628. # Adjust colorbar label size
  629. cbar = ax.collections[0].colorbar
  630. cbar.ax.tick_params(labelsize=16)
  631. cbar.ax.set_ylabel('Bottleneck Distance', fontsize=16)
  632. plt.tight_layout()
  633. # Save the figure
  634. fig.savefig(os.path.join(output_dir, f"Bottleneck_Heatmap_{dim}.png"))
  635. plt.close(fig)
  636. # %% [markdown]
  637. # In the graphs, we observed the **median** Bottleneck distances and 95% onfidence intervals for day pairs.
  638. # %% [markdown]
  639. # ## Calculate Wasserstain distances
  640. # %% [markdown]
  641. # We calculate as well Wasserstein distances, also known as the Earth Mover's or Kantorovich-Rubinstein distance. This metric comprises all possible pairs of points between the two diagrams and weighs them by their distance. This represents the minimum "effort" or "cost" to transform one persistence diagram into another.
  642. # %% [markdown]
  643. # ### Load persistance diagrams
  644. # %% [markdown]
  645. # **Noe** Load the data from the Bottleneck distances section. Please make sure to change dim0 or dim1 as needed.
  646. # %% [markdown]
  647. # ### Compute Wasserstein distances by boostrapping and saving metrics
  648. # %%
  649. # Define the specified pairwise day comparisons
  650. day_pairs = [("0D", "3D"), ("3D", "7D"), ("7D", "14D"), ("14D", "30D")]
  651. # Extract unique days and regions from the data
  652. unique_days = set(data.keys())
  653. unique_regions = set(region for day in data for region in data[day])
  654. results = []
  655. # Define number of bootstrap replications
  656. num_replications = 1000
  657. # For each unique combination of Day and Region
  658. for region in unique_regions:
  659. # Perform the specified pairwise comparison of diagrams from different days
  660. for day1, day2 in day_pairs:
  661. if day1 in data and day2 in data and region in data[day1] and region in data[day2]:
  662. bootstrap_distances = []
  663. for _ in range(num_replications):
  664. # Randomly sample diagrams from day1 and day2 with replacement
  665. bootstrap_sample1 = choices(data[day1].get(region, []), k=len(data[day1].get(region, [])))
  666. bootstrap_sample2 = choices(data[day2].get(region, []), k=len(data[day2].get(region, [])))
  667. # Compute pairwise Wasserstein distances for the bootstrap samples
  668. distances = [wasserstein_distance(diag1, diag2, order=2) for diag1 in bootstrap_sample1 for diag2 in bootstrap_sample2 if diag1.size and diag2.size]
  669. # Only proceed if there are distances to calculate
  670. if distances:
  671. # Compute the median distance for this bootstrap replication
  672. bootstrap_distances.append(np.median(distances))
  673. # Skip if no distances were calculated
  674. if not bootstrap_distances:
  675. continue
  676. # Compute metrics
  677. median_distance = np.median(bootstrap_distances)
  678. confidence_lower = np.percentile(bootstrap_distances, 2.5)
  679. confidence_upper = np.percentile(bootstrap_distances, 97.5)
  680. # Store the results
  681. results.append((day1, day2, region, median_distance, confidence_lower, confidence_upper))
  682. # Define the results path and save results to CSV
  683. results_path = "D:/Research/Stroke_PDGFR-B_Reactivity/Pdgfrb_Reactivity_DataAnalysis/Stroke_Pdgfrb_Reactivity/Data_Raw/Widefield_20x_ROIs_Pdgfrb/"
  684. csv_file = os.path.join(results_path, "Widefield_20x_ROIs_Pdgfrb_Wasserstein_1d.csv")
  685. with open(csv_file, 'w', newline='') as csvfile:
  686. csvwriter = csv.writer(csvfile)
  687. # Write header
  688. csvwriter.writerow(['Day1', 'Day2', 'Region', 'Median Wasserstein Distance', '95% CI Lower', '95% CI Upper'])
  689. # Write the results
  690. for row in results:
  691. csvwriter.writerow(row)
  692. print(f"Wasserstein distance bootstrap results saved to {csv_file}.")
  693. # %% [markdown]
  694. # ### Visualization of Wasserstein distances by heatmap
  695. # %% [markdown]
  696. # We plot the calculated Wasserstein distances and save corresponding plots in the designated folder.
  697. # %%
  698. # Load CSV data for 0-dimension and 1-dimension homology
  699. csv_path_dim0 = os.path.join("Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Wasserstein_0d.csv")
  700. csv_path_dim1 = os.path.join("Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Wasserstein_1d.csv")
  701. data_dim0 = pd.read_csv(csv_path_dim0)
  702. data_dim1 = pd.read_csv(csv_path_dim1)
  703. # Assuming the structure of your CSV files matches the expected columns
  704. # Define label mappings and structure
  705. regions = ["Peri", "Str", "Ctx"]
  706. region_labels = ["Perilesion", "Striatum", "Cortex"]
  707. days = ["0D-3D", "3D-7D", "7D-14D", "14D-30D"]
  708. dimensions = ["dim0", "dim1"] # Representing 0-dimension and 1-dimension homology
  709. # Initialize an empty dictionary to store heatmap data for each dimension
  710. heatmap_data = {dim: pd.DataFrame(index=regions, columns=days) for dim in dimensions}
  711. heatmap_annotations = {dim: pd.DataFrame(index=regions, columns=days) for dim in dimensions}
  712. # Function to populate matrices with data from CSV
  713. def populate_heatmap(data, dim):
  714. for _, row in data.iterrows():
  715. day_pair, region, median_distance, lower, upper = row['Day1']+'-'+row['Day2'], row['Region'], round(row['Median Wasserstein Distance']), round(row['95% CI Lower']), round(row['95% CI Upper'])
  716. if region in regions:
  717. heatmap_data[dim].loc[region, day_pair] = median_distance
  718. # Adjust annotation format here to round and remove decimals
  719. annotation = f"{median_distance}\n({lower} - {upper})"
  720. heatmap_annotations[dim].loc[region, day_pair] = annotation
  721. populate_heatmap(data_dim0, "dim0")
  722. populate_heatmap(data_dim1, "dim1")
  723. # Convert the data to numeric and handle NaN values
  724. for dim in dimensions:
  725. heatmap_data[dim] = heatmap_data[dim].astype(float)
  726. heatmap_data[dim].fillna(-1, inplace=True) # Fill NaN values with -1 for missing data
  727. # Visualization
  728. fig, axes = plt.subplots(1, 2, figsize=(16, 7), sharey=True)
  729. for ax, dim in zip(axes, dimensions):
  730. sns.heatmap(heatmap_data[dim], annot=heatmap_annotations[dim], cmap='viridis', ax=ax,
  731. cbar_kws={'label': 'Wasserstein Distance', 'shrink': 0.5},
  732. mask=(heatmap_data[dim] == -1), fmt="", annot_kws={"size": 16})
  733. ax.set_title(f"Dimension: {dim[-1]}", fontsize=16)
  734. ax.set_xlabel("Day Pair", fontsize=16)
  735. ax.set_ylabel("Region", fontsize=16)
  736. ax.set_yticklabels(region_labels, fontsize=16, rotation=0)
  737. ax.tick_params(axis='x', labelsize=16)
  738. # Adjust colorbar label size
  739. cbar = ax.collections[0].colorbar
  740. cbar.ax.tick_params(labelsize=16)
  741. cbar.ax.set_ylabel('Wasserstein Distance', fontsize=16)
  742. plt.tight_layout()
  743. plt.show()
  744. # Update the output directory path for saving individual images
  745. output_dir = "Plots/Widefield_20x_ROIs_pdgfrb_TDA"
  746. # Ensure the output directory exists
  747. if not os.path.exists(output_dir):
  748. os.makedirs(output_dir)
  749. # Save individual heatmap images for each dimension
  750. for dim in dimensions:
  751. fig, ax = plt.subplots(figsize=(7, 5))
  752. sns.heatmap(heatmap_data[dim], annot=heatmap_annotations[dim], cmap='viridis', ax=ax,
  753. cbar_kws={'label': 'Wasserstein Distance'},
  754. mask=(heatmap_data[dim] == -1), fmt="s")
  755. ax.set_title(f"Wasserstein Distances (Dimension: {dim[-1]})", fontsize=16)
  756. ax.set_xlabel("Day Pair", fontsize=16)
  757. ax.set_ylabel("Region", fontsize=16)
  758. ax.set_yticklabels(region_labels, fontsize=16)
  759. ax.tick_params(axis='x', labelsize=16)
  760. # Adjust colorbar label size
  761. cbar = ax.collections[0].colorbar
  762. cbar.ax.tick_params(labelsize=16)
  763. cbar.ax.set_ylabel('Wasserstein Distance', fontsize=16)
  764. plt.tight_layout()
  765. # Save the figure
  766. fig.savefig(os.path.join(output_dir, f"Wasserstein_Heatmap_{dim}.png"))
  767. plt.close(fig)
  768. # %% [markdown]
  769. # Analogous to Bottleneck distances, here we appreacite the estimates and its uncertainty for each day pair.
  770. # %% [markdown]
  771. # ## Addon: Generate another set of images for illustration
  772. # %% [markdown]
  773. # Here, we create an additional visualization of Vietoris-rips complexes plotting the filtration circles at different values. We plot representative images and then store a full set of images in a designated location.
  774. # %%
  775. def load_point_cloud(file_path):
  776. return np.load(file_path, allow_pickle=True)
  777. def get_edges_at_filtration(points, filtration_value):
  778. rips_complex = RipsComplex(points=points, max_edge_length=filtration_value)
  779. simplex_tree = rips_complex.create_simplex_tree(max_dimension=1)
  780. edges = []
  781. for simplex, f_value in simplex_tree.get_filtration():
  782. if len(simplex) == 2 and f_value <= filtration_value:
  783. edges.append(simplex)
  784. return edges
  785. def plot_point_cloud_with_structures(points, edges, ax, filtration_value):
  786. ax.scatter(points[:, 0], points[:, 1], color='green', s=10, zorder=1)
  787. for edge in edges:
  788. point1 = points[edge[0]]
  789. point2 = points[edge[1]]
  790. ax.plot([point1[0], point2[0]], [point1[1], point2[1]], 'black', alpha=0.9, zorder=1.5)
  791. for point in points:
  792. circle = Circle(point, radius=filtration_value, color='skyblue', fill=True, alpha=0.2)
  793. ax.add_patch(circle)
  794. ax.set_xticks([])
  795. ax.set_yticks([])
  796. ax.set_xlabel('')
  797. ax.set_ylabel('')
  798. file_paths = ['PointClouds/Widefield_20x_ROIs_Pdgfrb/Td047_30D_Peri.npy',
  799. 'PointClouds/Widefield_20x_ROIs_Pdgfrb/Td047_30D_Str.npy',
  800. 'PointClouds/Widefield_20x_ROIs_Pdgfrb/Td047_30D_Ctx.npy']
  801. filtration_values = [0, 50, 100, 200]
  802. # Ensure each point cloud gets its subplot for each filtration value
  803. fig, axs = plt.subplots(len(file_paths), len(filtration_values), figsize=(15, 5 * len(file_paths)))
  804. for file_index, file_path in enumerate(file_paths):
  805. points = load_point_cloud(file_path)
  806. for ax_index, filtration_value in enumerate(filtration_values):
  807. ax = axs[file_index, ax_index] if len(file_paths) > 1 else axs[ax_index]
  808. edges = get_edges_at_filtration(points, filtration_value)
  809. plot_point_cloud_with_structures(points, edges, ax, filtration_value)
  810. plt.tight_layout()
  811. plt.show()
  812. # %% [markdown]
  813. # Finally, we save a series f images in a destinated folder for illustration in the scientific article.
  814. # %%
  815. import os
  816. # Load all .npy files in a folder
  817. def load_point_clouds_from_folder(folder_path):
  818. point_cloud_files = [os.path.join(folder_path, f) for f in os.listdir(folder_path) if f.endswith('.npy')]
  819. return point_cloud_files
  820. # Save the plot for a given point cloud and filtration values
  821. def save_plots_for_point_cloud(file_path, filtration_values, save_folder):
  822. points = load_point_cloud(file_path)
  823. for filtration_value in filtration_values:
  824. edges = get_edges_at_filtration(points, filtration_value)
  825. fig, ax = plt.subplots(figsize=(5, 5)) # Adjust size as needed
  826. plot_point_cloud_with_structures(points, edges, ax, filtration_value)
  827. # Save plot to the designated folder
  828. base_filename = os.path.splitext(os.path.basename(file_path))[0]
  829. save_path = os.path.join(save_folder, f'{base_filename}_filtration_{filtration_value}.png')
  830. plt.savefig(save_path)
  831. plt.close(fig) # Close the figure after saving to free up memory
  832. # Specify folder paths
  833. point_clouds_folder = 'PointClouds/Widefield_20x_ROIs_Pdgfrb/'
  834. save_folder = 'D:/Research/Stroke_PDGFR-B_Reactivity/Images/Widefield_20x-ROIs_Pdgfrb_TDA/FiltrationValues'
  835. filtration_values = [0, 50, 100, 200]
  836. # Ensure the save folder exists
  837. if not os.path.exists(save_folder):
  838. os.makedirs(save_folder)
  839. # Load and process each point cloud file
  840. point_cloud_files = load_point_clouds_from_folder(point_clouds_folder)
  841. for file_path in point_cloud_files:
  842. save_plots_for_point_cloud(file_path, filtration_values, save_folder)
  843. # %%

Widefield_20x_ROIs_Pdgfrb_TDA.ipynb at commit ae087d5, no license · at the source

Overview

  1. Neuroscience Axis, Research Center of CHU de Québec‐Université Laval Quebec City Quebec Canada
  2. Department of Psychiatry and Neuroscience, Faculty of Medicine Université Laval Quebec City Quebec Canada
Journal: Glia, volume 75, issue 1, article e70232
Dates: received 2 April 2026; accepted 12 September 2026; published online 31 October 2026; in print January 2027
Type: Research article · Language: English
License: CC BY
Identifiers: DOI · PMCID PMC13723038
Status: code verified (how the code is found and verified)
Categories: stroke (population), cellular / molecular (subfield)
Methods: Smoothing, state filtering, decompositions, Machine learning, Statistics, fMRI & imaging
Keywords: ischemic stroke, KLF4, PDGFRβ + cells, point pattern analysis (PPA), tissue scar, topological data analysis (TDA)
Citations: not cited yet (Europe PMC); 79 references in the paper
Research resources: PDGFRα immunolabeling RRID:AB_2, RRID:AB_2227288, RRID:AB_777165, RRID:AB_839504, some GFAP‐stained cytoskeletons RRID:AB_86543, homozygous for B6.129S6‐Klf4tm1khk/Mmmh RRID:MMRRC_029877

Abstract

The pathobiology of ischemic stroke is associated with the activation of brain platelet‐derived growth factor receptor (PDGFR)β + perivascular cells. While Krüppel‐like factor (KLF)4 is a well‐established mediator of peripheral PDGFRβ + cell activation, its role in brain PDGFRβ + cells remains elusive. Herein, we aimed to characterize the spatiotemporal reactivity of brain PDGFRβ + cells, while assessing KLF4 contribution upon experimental ischemic stroke using transgenic mice that enable tracking PDGFRβ + cells or conditionally depleting KLF4 in PDGFRβ + cells. Moving beyond the qualitative descriptors, we sought to develop point pattern analysis (PPA) and topological data analysis (TDA) workflows to generate robust and reusable mathematical models to assess spatial cell responses and tissue scar topological changes. First, we report the emergence of phenotypically distinct reactive parenchymal PDGFRβ + cells, which cannot be causally and exclusively linked, as it is currently proposed, to local cell proliferation or adjacent vascular detachment. Furthermore, we show that KLF4 is barely expressed in brain PDGFRβ + cells under normal conditions and that it is only marginally induced in reactive PDGFRβ + cells at the injured brain. Notably, the conditional attenuation of KLF4 expression specifically in reactive PDGFRβ + cells does not alter spatial cellular distribution nor scar organization. This suggests that, in contrast to peripheral PDGFRβ + cells upon ischemic insults, KLF4 is not a main regulator of the reactivity of brain cells in response to ischemic stroke. Ultimately, our findings supported by open‐source spatiotemporal models indicate that the reactivity of brain PDGFRβ + perivascular cells is highly heterogeneous and fundamentally distinct from peripheral PDGFRβ + cells.

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

Repositories

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

Zenodo 13948420

License: CC-BY-4.0
State: the link answers, verified on 1 November 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Data Availability Statement”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: tidyverse (27 files), ggplot2 (26 files), brms (23 files), easystats (23 files), patchwork (23 files), emmeans (15 files), broom (5 files), NumPy (4 files), pandas (4 files), Matplotlib (3 files), reshape2 (3 files), SciPy (3 files), seaborn (2 files), data.table (1 file), scikit-image (1 file), tifffile (1 file)
Availability: 1 check, the latest on 1 November 2026: the link answers (HTTP 200)
  • 1 November 2026: the link answers (HTTP 200)
35 files

elalilab/stroke_pdgfr-b_reactivity

License: none: the authors keep all their rights
State: the link answers, verified on 1 November 2026
Evidence: files inventoried
Commit: ae087d5288616f7e66b467932c2f2627aa7fb910, 26 March 2026
Languages: Quarto (33), Jupyter (2)
Size: 41 files, 35 scripts
Software Heritage: not archived
Found in: the Zenodo archive record
Holds: README, 35 notebooks
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: tidyverse (28 files), ggplot2 (27 files), brms (24 files), easystats (24 files), patchwork (24 files), emmeans (15 files), broom (6 files), NumPy (4 files), pandas (4 files), reshape2 (4 files), Matplotlib (3 files), SciPy (3 files), seaborn (2 files), data.table (1 file), scikit-image (1 file), tifffile (1 file)
Availability: 1 check, the latest on 1 November 2026: the link answers
  • 1 November 2026: the link answers
36 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:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 69 scripts, each with its path and the digest of its content;
  • 21 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 Statement

All the data associated with the publication are made fully available via https://zenodo.org/records/10553084, which includes original microscopy images stored in Zenodo (10.5281/zenodo.10553084 (https://doi.org/10.5281/zenodo.10553084)) as well as access to complementary data stored in Open Science Framework (OSF) repository (10.17605/OSF.IO/74MQN (https://doi.org/10.17605/OSF.IO/74MQN)), and reproducible workflows for image processing and analysis are available on GitHub (10.5281/zenodo.13948420 (https://doi.org/10.5281/zenodo.13948420)).

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 1, 1 November 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 2 authors, 6 keywords, 3 funders, 70 references, 6 RRIDs.

Cite

This paper

Manrique‐Castano, D., & ElAli, A. (2026). Brain PDGFRβ+ Cells Exhibit Diverse Reactive Phenotypes After Stroke Without Requiring KLF4. Glia, 75(1), e70232.

BibTeX

@article{manriquecastano2026brain,
author = {Manrique‐Castano, Daniel and ElAli, Ayman},
title = {{Brain PDGFRβ+ Cells Exhibit Diverse Reactive Phenotypes After Stroke Without Requiring KLF4}},
journal = {Glia},
year = {2026},
month = oct,
volume = {75},
number = {1},
pages = {e70232},
publisher = {Wiley},
issn = {0894-1491},
pmcid = {PMC13723038}
}

RIS

TY - JOUR
AU - Manrique‐Castano, Daniel
AU - ElAli, Ayman
TI - Brain PDGFRβ+ Cells Exhibit Diverse Reactive Phenotypes After Stroke Without Requiring KLF4
T2 - Glia
J2 - Glia
PY - 2026
DA - 2026/10/31
VL - 75
IS - 1
SP - e70232
SN - 0894-1491
PB - Wiley
LA - en
ER -

CSL-JSON

{
"id": "pmcid:PMC13723038",
"type": "article-journal",
"title": "Brain PDGFRβ+ Cells Exhibit Diverse Reactive Phenotypes After Stroke Without Requiring KLF4",
"container-title": "Glia",
"author": [
{
"family": "Manrique‐Castano",
"given": "Daniel"
},
{
"family": "ElAli",
"given": "Ayman"
}
],
"container-title-short": "Glia",
"volume": "75",
"issue": "1",
"page": "e70232",
"PMCID": "PMC13723038",
"ISSN": "0894-1491",
"publisher": "Wiley",
"language": "en",
"issued": {
"date-parts": [
[
2026,
10,
31
]
]
}
}

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.1007/s00018-024-05244-w [code]
Endothelial PDGF-D contributes to neurovascular protection after ischemic stroke by rescuing pericyte functions.
Journal: Cellular and molecular life sciences : CMLS
In common: broom, patchwork, ggplot2, 1 other tool, stroke, cellular / molecular, 8 references, author Ayman ElAli
[2] doi:10.1016/j.xpro.2024.102989 [code]
Unbiased quantification of the spatial distribution of murine cells using point pattern analysis.
Journal: STAR protocols
In common: brms, easystats, emmeans, 5 other tools, cellular / molecular, 4 references, author Ayman ElAli
[3] 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: tifffile, patchwork, scikit-image, 9 other tools, cellular / molecular, 2 references
[4] doi:10.1016/j.nicl.2025.103881 [code]
Brain age gap reduction following exercise mirrors clinical improvements in schizophrenia spectrum disorders.
Journal: NeuroImage. Clinical
In common: brms, easystats, emmeans, 9 other tools, 1 reference
[5] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: brms, broom, patchwork, 10 other tools, cellular / molecular
[6] doi:10.1016/j.celrep.2026.117505 [code]
Impaired spatial coding and neuronal hyperactivity in the medial entorhinal cortex of aged APP knock-in mice.
Journal: Cell reports
In common: brms, broom, easystats, 8 other tools, 1 reference
[7] doi:10.1523/jneurosci.1285-25.2025 [code]
Hidden State Inference or Continuous Belief Updating during a Dynamic Visuomotor Skill.
Journal: The Journal of neuroscience : the official journal of the Society for Neuroscience
In common: brms, easystats, emmeans, 7 other tools, 2 references
[8] doi:10.1038/s41467-026-73865-9 [code]
Histamine shapes the neurocomputational dynamics of human learning.
Journal: Nature communications
In common: brms, broom, easystats, 9 other tools
[9] doi:10.1186/s11689-025-09651-3 [code]
The association between infant EEG aperiodic exponent and the trajectory of restricted and repetitive behaviors for toddlers with and without autism.
Journal: Journal of neurodevelopmental disorders
In common: brms, broom, easystats, 9 other tools
[10] doi:10.1038/s41586-022-05279-8 [code]
Inferring and perturbing cell fate regulomes in human brain organoids.
Journal: Nature
In common: brms, easystats, patchwork, 8 other tools, 1 reference

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.

Add the badge to the code's README

OSCR: the badge of this paper

If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.

Markdown
<!-- oscr-badge:start -->
[![OSCR](https://openscicode.org/badge/repo/zenodo:13948420.svg)](https://openscicode.org/badge/repo/zenodo:13948420)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/repo/zenodo:13948420.svg
   :target: https://openscicode.org/badge/repo/zenodo:13948420
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/repo/zenodo:13948420"><img src="https://openscicode.org/badge/repo/zenodo:13948420.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->

On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".

If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge 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: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.

Report a mismatch between the code and the paper · Report a code error (signed in)

Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.

Report a reproduction failure (signed in)

Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.

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