Brain PDGFRβ+ Cells Exhibit Diverse Reactive Phenotypes After Stroke Without Requiring KLF4
The 21 matches
- [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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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] § 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
- # %% [markdown]
- # # Topological Data Analysis (TDA) of PDGFR-β+ cells
- # %% [markdown]
- # ## Preview
- #
- # 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.
- #
- # **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`.
- #
- # **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].
- #
- # **Note:** This notebook runs using python code.
- # %% [markdown]
- # ## Load required libraries
- #
- # We import the required Python libraries.
- # %%
- import pandas as pd
- import numpy as np
- import seaborn as sns
- import os
- import csv
- import matplotlib.pyplot as plt
- from matplotlib.collections import LineCollection
- from matplotlib.animation import FuncAnimation
- from matplotlib.patches import Circle
- import gudhi as gd
- from gudhi import RipsComplex, plot_persistence_diagram
- from gudhi import bottleneck_distance
- from gudhi.wasserstein import wasserstein_distance
- import itertools
- from ripser import ripser
- from persim import plot_diagrams
- from random import choices
- from scipy.spatial import ConvexHull
- # %% [markdown]
- # ## Load and handle the dataset
- #
- # 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.
- # %%
- # Load the dataset
- file_path = 'Data_Raw/Widefield_20x_ROIs_Pdgfrb/CellProfiler/Cells.csv'
- data = pd.read_csv(file_path)
- # Keep only the specified columns
- data = data[['Location_Center_X', 'Location_Center_Y', 'FileName_Raw']]
- # Define a function to parse the FileName_Raw column
- def parse_filename(filename):
- parts = filename.split('_')
- # Extract the needed parts based on their position
- MouseID = parts[0]
- DPI = parts[1]
- Condition = parts[2]
- Lesion = parts[3]
- Region = parts[4]
- return MouseID, DPI, Condition, Lesion, Region
- # Apply the function to the DataFrame
- data[['MouseID', 'DPI', 'Condition', 'Lesion', 'Region']] = data.apply(lambda row: parse_filename(row['FileName_Raw']), axis=1, result_type='expand')
- # Drop the FileName_Raw column
- data.drop('FileName_Raw', axis=1, inplace=True)
- # Reorder columns to move Location_Center_X and Location_Center_Y to the right
- column_order = ['MouseID', 'DPI', 'Condition', 'Lesion', 'Region', 'Location_Center_X', 'Location_Center_Y']
- data = data[column_order]
- # Save the updated dataset to a new file
- output_file_path = 'Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Coords.csv'
- data.to_csv(output_file_path, index=False)
- print("Dataset updated and saved to:", output_file_path)
- # %% [markdown]
- # ## Calculate Vietoris-Rips Complex
- #
- # 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:
- #
- # 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:
- #
- # 1. **Vertices**: Each point in $ S $ is considered a vertex, or 0-simplex.
- #
- # 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 $.
- #
- # 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:
- #
- # $$
- # \forall i, j \in \{0, 1, \ldots, k\},\, d(v_i, v_j) \leq \epsilon
- # $$
- #
- # 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 $.
- #
- # We load the cell coordinates to build the Vietoris-Rips complexes. We set `100` as the maximum filtration value to facilitate visualization.
- # %%
- # Load the organized dataset
- data_path = 'Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Coords.csv' # Adjust this path as necessary
- data = pd.read_csv(data_path)
- # Group the dataset by MouseID and Region, then extract the point clouds
- point_clouds_by_mouse_region = data.groupby(['MouseID', 'DPI', 'Region']).apply(lambda df: df[['Location_Center_X', 'Location_Center_Y']].values).to_dict()
- def calculate_vietoris_rips_complex(point_cloud, max_dim, max_radius):
- """
- Calculate the Vietoris-Rips complex for a given point cloud.
- Parameters:
- - point_cloud: numpy array of shape (n_points, n_dimensions) representing the point cloud.
- - max_dim: int, the maximal dimension of the simplices in the Vietoris-Rips complex.
- - max_radius: float, the maximal radius for simplices in the Vietoris-Rips complex.
- Returns:
- - simplex_tree: a GUDHI SimplexTree object representing the Vietoris-Rips complex.
- """
- rips_complex = gd.RipsComplex(points=point_cloud, max_edge_length=max_radius)
- simplex_tree = rips_complex.create_simplex_tree(max_dimension=max_dim)
- return simplex_tree
- # Parameters for Vietoris-Rips complex calculation
- max_dim = 3
- max_radius = 100
- # Calculate Vietoris-Rips complexes for each region of each animal
- vietoris_rips_complexes = {}
- for (mouse_id, dpi, region), point_cloud in point_clouds_by_mouse_region.items():
- key = (mouse_id, dpi, region) # Use a tuple of MouseID and Region as the key
- vietoris_rips_complexes[key] = calculate_vietoris_rips_complex(point_cloud, max_dim, max_radius)
- # %% [markdown]
- # ### Visualize and save Vietoris-Rips Complex
- #
- # Once calculated, we save the images in a designated folder:
- # %%
- def plot_vietoris_rips_complex(point_cloud, simplex_tree, filename, save_dir, x_range=None, y_range=None):
- """
- Plot and save the Vietoris-Rips complex for a given 2D point cloud, with specified x and y axis ranges.
- Parameters:
- - point_cloud: numpy array of shape (n_points, 2) representing the 2D point cloud.
- - simplex_tree: a gudhi object representing the Vietoris-Rips complex.
- - filename: str, the name of the file to save the image.
- - save_dir: str, directory to save the image.
- - x_range: tuple, the range for the x-axis (optional).
- - y_range: tuple, the range for the y-axis (optional).
- """
- fig, ax = plt.subplots(figsize=(6, 4))
- # Plot points
- ax.scatter(point_cloud[:, 0], point_cloud[:, 1], c='black', s=20, alpha=0.8)
- # Plot edges
- edges = []
- for simplex in simplex_tree.get_skeleton(2): # Get up to 2-simplices (edges)
- if len(simplex[0]) == 2: # Edges
- p1, p2 = point_cloud[simplex[0][0]], point_cloud[simplex[0][1]]
- edges.append([p1, p2])
- lc = LineCollection(edges, colors='green', linewidths=1, alpha=0.7)
- ax.add_collection(lc)
- # Adjust plot settings
- ax.set_xticks([])
- ax.set_yticks([])
- ax.set_xlabel('')
- ax.set_ylabel('')
- if x_range is not None:
- ax.set_xlim(x_range)
- if y_range is not None:
- ax.set_ylim(y_range)
- plt.tight_layout()
- if not os.path.exists(save_dir):
- os.makedirs(save_dir)
- plt.savefig(os.path.join(save_dir, f"{filename}.png"))
- plt.close()
- # Directory to save the images
- output_dir = "D:/Research/Stroke_PDGFR-B_Reactivity/Images/Widefield_20x-ROIs_Pdgfrb_TDA/VietorisRips_Static" # Update this path
- x_range = (0, 2000) # Example x-axis range
- y_range = (0, 1500) # Example y-axis range
- aspect_ratio = 'auto' # Adjust this based on your data or preferences
- # Assuming vietoris_rips_complexes and point_clouds_by_mouse have been computed accordingly
- for (mouse_id, dpi, region), simplex_tree in vietoris_rips_complexes.items():
- filename = f"{mouse_id}_{dpi}_{region}_VietorisRips"
- point_cloud = point_clouds_by_mouse_region[(mouse_id, dpi, region)]
- plot_vietoris_rips_complex(point_cloud, simplex_tree, filename, output_dir, x_range, y_range)
- # %% [markdown]
- # ### Visualize and save Vietoris-Rips Complex (animated)
- #
- # We created animated Vietoris-rips for illustration in the scientific article. We save animations with a maximum of `300` filtration value.
- # %%
- def animate_vietoris_rips_mp4(point_cloud, simplex_tree, filename, save_dir, max_radius, step_size=1):
- """
- Create an .mp4 animation illustrating the growth of the Vietoris-Rips complex for 2D data.
- """
- fig, ax = plt.subplots(figsize=(7, 7))
- ax.scatter(point_cloud[:, 0], point_cloud[:, 1], c='black', s=20, alpha=0.8)
- # Function to update the plot for each frame
- def update(threshold):
- ax.cla() # Clear current plot details
- ax.scatter(point_cloud[:, 0], point_cloud[:, 1], c='black', s=10, alpha=0.7)
- edges = []
- for simplex in simplex_tree.get_skeleton(2):
- if simplex[1] <= threshold:
- if len(simplex[0]) == 2: # Edges
- p1, p2 = point_cloud[simplex[0][0]], point_cloud[simplex[0][1]]
- edges.append([p1, p2])
- lc = LineCollection(edges, colors='green', alpha=0.7)
- ax.add_collection(lc)
- ax.set_title(f"Threshold: {threshold:.2f}", fontsize=20)
- ax.set_xlabel('X', fontsize=20, labelpad=15)
- ax.set_ylabel('Y', fontsize=20, labelpad=15)
- ax.tick_params(axis='both', which='major', labelsize=20)
- ax.axis('equal')
- ani = FuncAnimation(fig, update, frames=np.arange(0, max_radius, step_size), blit=False)
- ani_file = os.path.join(save_dir, f"{filename}_Animation.mp4")
- ani.save(ani_file, writer='ffmpeg', fps=2, extra_args=['-vcodec', 'libx264'])
- plt.close()
- return ani_file # Return the file path of the saved animation
- # Example usage
- output_dir = "D:/Research/Stroke_PDGFR-B_Reactivity/Images/Widefield_20x-ROIs_Pdgfrb_TDA/VietorisRips_Animated"
- max_radius = 300 # Adjust as necessary
- step_size = 20 # Adjust as necessary
- for (mouse_id, dpi, region), simplex_tree in vietoris_rips_complexes.items():
- filename = f"{mouse_id}_{dpi}_{region}_VietorisRips"
- point_cloud = point_clouds_by_mouse_region[(mouse_id, dpi, region)]
- ani_file = animate_vietoris_rips_mp4(point_cloud, simplex_tree, filename, output_dir, max_radius, step_size)
- print(f"Saved animation: {ani_file}")
- # %% [markdown]
- # ## Generate point clouds
- #
- # 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.
- # %%
- # Load your dataset
- data_path = 'Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Coords.csv'
- data = pd.read_csv(data_path)
- # Group data by MouseID, Region, and DPI
- point_clouds_by_group = data.groupby(['MouseID', 'DPI', 'Region',]).apply(
- lambda df: df[['Location_Center_X', 'Location_Center_Y']].values
- ).to_dict()
- # %% [markdown]
- # Next, we verify the structire of the data to make sure is grouped as intented.
- # %%
- if not point_clouds_by_group:
- print("The point_clouds_by_group dictionary is empty.")
- else:
- print("Point clouds loaded:", len(point_clouds_by_group))
- # Inspect the first few items to ensure they're structured correctly
- for key, value in list(point_clouds_by_group.items())[:5]:
- print(key, value.shape)
- # %% [markdown]
- # Then, we save point clouds as .npy files (Numpy array) in a designated folder
- # %%
- def save_point_clouds_as_npy(point_clouds_by_group, data_directory):
- """
- Saves point clouds to .npy files in the specified directory.
- Parameters:
- point_clouds_by_group (dict): Dictionary with keys as identifiers
- (e.g., ('MouseID', 'Region', 'DPI')) and values as numpy arrays representing point clouds.
- data_directory (str): Path to the directory where .npy files will be saved.
- """
- # Ensure the directory exists
- if not os.path.exists(data_directory):
- os.makedirs(data_directory)
- # Iterate through the point clouds and save each as an .npy file
- for identifier, point_cloud in point_clouds_by_group.items():
- # Create a filename from the identifier
- filename = "_".join(identifier) + ".npy"
- filepath = os.path.join(data_directory, filename)
- np.save(filepath, point_cloud)
- print("Point clouds saved as .npy files.")
- data_directory = r"PointClouds/Widefield_20x_ROIs_Pdgfrb/"
- # Call the function with your data and desired save location
- save_point_clouds_as_npy(point_clouds_by_group, data_directory)
- # %% [markdown]
- # ## Computation of persistent homology
- # %% [markdown]
- # 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.
- #
- # 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.
- # %% [markdown]
- # ### Compute persistence diagrams
- # %% [markdown]
- # We compute persistence diagrams and save then as .npy file for further analysis and as .png images for visualization
- # %%
- # Disable LaTeX usage in matplotlib
- plt.rcParams['text.usetex'] = False
- def process_npy_for_persistence_diagrams(data_directory, save_directory, save_diag_directory, img_size=(6, 6), x_range=None, y_range=None):
- """
- Batch process .npy files (containing point clouds) to compute, visualize, and save persistence diagrams.
- """
- # Ensure save directories exist
- if not os.path.exists(save_directory):
- os.makedirs(save_directory)
- if not os.path.exists(save_diag_directory):
- os.makedirs(save_diag_directory)
- # List all .npy files in the data directory
- npy_files = [f for f in os.listdir(data_directory) if f.endswith('.npy')]
- for file in npy_files:
- # Load the point cloud from the .npy file
- points_array = np.load(os.path.join(data_directory, file))
- # Compute the persistence diagrams using Ripser
- diagrams = ripser(points_array, maxdim=1)['dgms']
- # Save each dimension's persistence diagram as a separate .npy file
- for dim, diag in enumerate(diagrams):
- diag_filename = os.path.splitext(file)[0] + f"_persistence_diag_dim{dim}.npy"
- np.save(os.path.join(save_diag_directory, diag_filename), diag)
- # Visualize and save the persistence diagrams
- fig, ax = plt.subplots(figsize=img_size)
- plot_diagrams(diagrams, show=False, ax=ax)
- ax.set_title("")
- ax.set_xlabel('Birth', fontsize=25)
- ax.set_ylabel('Death', fontsize=25)
- ax.tick_params(axis='both', which='major', labelsize=25)
- # Optionally adjust the legend
- legend = ax.legend()
- if legend:
- for label in legend.get_texts():
- label.set_fontsize(20)
- plt.tight_layout()
- if x_range is not None:
- ax.set_xlim(x_range)
- if y_range is not None:
- ax.set_ylim(y_range)
- # Save the figure
- filename = os.path.splitext(file)[0] + "_persistence_diagram.png"
- fig.savefig(os.path.join(save_directory, filename))
- plt.close()
- print(f"Persistence diagrams saved to {save_directory}.")
- print(f"Persistence diagrams data saved to {save_diag_directory}.")
- # Paths to your directories
- data_directory = r"PointClouds/Widefield_20x_ROIs_Pdgfrb/"
- save_directory = r"D:/Research/Stroke_PDGFR-B_Reactivity/Images/Widefield_20x-ROIs_Pdgfrb_TDA/PersistenceDiagrams"
- save_diag_directory = r"Data_Raw/Widefield_20x_ROIs_Pdgfrb/PersistenceDiagrams/"
- #x_range = (0, 500)
- #y_range = (0, 500)
- # Call the function to process, visualize, and save persistence diagrams
- process_npy_for_persistence_diagrams(data_directory, save_directory, save_diag_directory)
- # %% [markdown]
- # ## Calculate Betti curves for the persistent diagrams
- # %% [markdown]
- # 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,
- # The Betti curve for each dimension is a function mapping the scaling parameter to the corresponding Betti number (not shown here).
- #
- # 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.
- # %% [markdown]
- # ### Betti curves for 0D homology
- # %%
- def load_persistence_diagrams(dim0_path):
- files = [f for f in os.listdir(dim0_path) if 'dim0' in f]
- data = {}
- for file in files:
- parts = file.split('_')
- day, region = parts[1], parts[2]
- diagram = np.load(os.path.join(dim0_path, file))
- key = (day, region)
- data[key] = diagram
- return data
- def compute_betti_0_curve(persistence_diagram, max_filtration_value, n_steps):
- betti_0_curve = []
- filtration_values = np.linspace(0, max_filtration_value, n_steps)
- for value in filtration_values:
- betti_0 = sum(1 for point in persistence_diagram if point[0] <= value and (point[1] > value or point[1] == float('inf')))
- betti_0_curve.append(betti_0)
- # Normalize the curve to start at 1
- if betti_0_curve[0] != 0: # Ensure there is no division by zero
- initial_value = betti_0_curve[0]
- normalized_betti_0_curve = [b / initial_value for b in betti_0_curve]
- else:
- # Handle the case where the initial value is 0
- normalized_betti_0_curve = betti_0_curve
- return normalized_betti_0_curve
- dim0_path = "Data_Raw/Widefield_20x_ROIs_Pdgfrb/PersistenceDiagrams/"
- dim0_data = load_persistence_diagrams(dim0_path)
- n_steps = 300
- max_filtration_value = 300
- betti_0_curves = {key: compute_betti_0_curve(diagram, max_filtration_value, n_steps) for key, diagram in dim0_data.items()}
- # Aggregating Betti curves by region
- aggregated_curves_by_region_0 = {}
- for (day, region), curve in betti_0_curves.items():
- if region not in aggregated_curves_by_region_0:
- aggregated_curves_by_region_0[region] = {}
- aggregated_curves_by_region_0[region][day] = curve
- def plot_aggregated_betti_curves_0(aggregated_curves_by_region_0, save_directory):
- if not os.path.exists(save_directory):
- os.makedirs(save_directory)
- days_order = ['0D', '3D', '7D', '14D', '30D']
- day_colors = {
- '0D': 'red',
- '3D': 'blue',
- '7D': 'green',
- '14D': 'purple',
- '30D': 'orange'
- }
- for region, curves in aggregated_curves_by_region_0.items():
- plt.figure(figsize=(5, 5))
- for day in days_order:
- if day in curves:
- plt.plot(curves[day], label=day, color=day_colors[day])
- plt.xlabel("Filtration Value", fontsize=22)
- plt.ylabel("Normalized Betti-0", fontsize=22)
- plt.xticks(fontsize=22, rotation=45)
- plt.yticks(fontsize=22)
- leg = plt.legend(title="Day", fontsize=16)
- plt.setp(leg.get_title(), fontsize=16)
- plt.tight_layout()
- plot_path = os.path.join(save_directory, f"Betti0_Curves_{region}.png")
- plt.savefig(plot_path)
- print(f"Saved Betti-0 curve plot for {region} at {plot_path}")
- plt.show()
- plt.close()
- save_directory = "D:/Research/Stroke_PDGFR-B_Reactivity/Pdgfrb_Reactivity_DataAnalysis/Stroke_Pdgfrb_Reactivity/Plots/Widefield_20x_ROIs_pdgfrb_TDA"
- plot_aggregated_betti_curves_0(aggregated_curves_by_region_0, save_directory)
- # %% [markdown]
- # ### Betti curves for 1D homology
- #
- # Now, we calculate the Betti curves for 1D homoogy.
- # %%
- def load_persistence_diagrams(dim1_path):
- files = [f for f in os.listdir(dim1_path) if 'dim1' in f]
- data = {}
- for file in files:
- parts = file.split('_')
- day, region = parts[1], parts[2]
- diagram = np.load(os.path.join(dim1_path, file))
- key = (day, region)
- data[key] = diagram
- return data
- dim1_path = "Data_Raw/Widefield_20x_ROIs_Pdgfrb/PersistenceDiagrams/"
- dim1_data = load_persistence_diagrams(dim1_path)
- def compute_betti_1_curve(persistence_diagram, max_filtration_value, n_steps):
- betti_1_curve = []
- filtration_values = np.linspace(0, max_filtration_value, n_steps)
- for value in filtration_values:
- betti_1 = sum(1 for point in persistence_diagram if point[0] <= value and (point[1] > value or point[1] == float('inf')))
- betti_1_curve.append(betti_1)
- return betti_1_curve
- n_steps = 100
- max_filtration_value = 700
- betti_1_curves = {key: compute_betti_1_curve(diagram, max_filtration_value, n_steps) for key, diagram in dim1_data.items()}
- # Aggregating Betti curves by region
- aggregated_curves_by_region_1 = {}
- for (day, region), curve in betti_1_curves.items():
- if region not in aggregated_curves_by_region_1:
- aggregated_curves_by_region_1[region] = {}
- aggregated_curves_by_region_1[region][day] = curve
- def plot_aggregated_betti_curves_1(aggregated_curves_by_region_1, save_directory):
- if not os.path.exists(save_directory):
- os.makedirs(save_directory)
- days_order = ['0D', '3D', '7D', '14D', '30D']
- day_colors = {
- '0D': 'red',
- '3D': 'blue',
- '7D': 'green',
- '14D': 'purple',
- '30D': 'orange'
- }
- for region, curves in aggregated_curves_by_region_1.items():
- plt.figure(figsize=(5, 5)) # Adjust figure size as needed
- for day in days_order:
- if day in curves:
- plt.plot(curves[day], label=day, color=day_colors[day])
- plt.xlabel("Filtration Value", fontsize=22)
- plt.ylabel("Betti-1", fontsize=22)
- plt.xticks(fontsize=22) # Increase tick label size
- plt.yticks(fontsize=22) # Increase tick label size
- leg = plt.legend(title="Day", fontsize=16)
- plt.setp(leg.get_title(), fontsize=16) # Increase legend title size
- plt.tight_layout()
- # Save the plot before calling plt.show()
- plot_path = os.path.join(save_directory, f"Betti1_Curves_{region}.png")
- plt.savefig(plot_path)
- print(f"Saved Betti-1 curve plot for {region} at {plot_path}")
- # Show the plot inline
- plt.show()
- plt.close()
- # Specify the directory where you want to save the plots
- save_directory = "Plots/Widefield_20x_ROIs_pdgfrb_TDA/"
- # Call the function with your aggregated Betti curves and the save directory
- plot_aggregated_betti_curves_1(aggregated_curves_by_region_1, save_directory)
- # %% [markdown]
- # ## Calculate Bottleneck distances
- #
- # 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:
- #
- # $$
- # \text{bottleneck}(D_1, D_2) = \inf_{\phi: D_1 \to D_2} \sup_{x \in D_1} ||x - \phi(x)||_{\infty}
- # $$
- #
- # 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.
- #
- # 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.
- # %% [markdown]
- # ### Load persistance diagrams
- #
- # 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.
- #
- # **Note:** To shift focus to dimension 1 persistence diagrams, one can replace 'dim0.npy' with 'dim1.npy' in the code.
- # %%
- ### To calculate dimension 1, change the dim0 to dim1
- # Define the path to persistence diagrams
- path = "Data_Raw/Widefield_20x_ROIs_Pdgfrb/PersistenceDiagrams/"
- # Load .npy files for dimension 1 diagrams
- files = [f for f in os.listdir(path) if f.endswith('dim1.npy')]
- # Organize data into a nested dictionary {day: {region: [diagrams, ...], ...}, ...}
- data = {}
- for file in files:
- # Extract information from the filename
- parts = file.split("_")
- day, region = parts[1], parts[2]
- # Load the persistence diagram
- diagram = np.load(os.path.join(path, file))
- # Initialize day if not already in data
- if day not in data:
- data[day] = {}
- # Append the diagram to the correct day and region
- data[day].setdefault(region, []).append(diagram)
- # %% [markdown]
- # Next, we verifying the loaded data structure
- # %%
- for day in data:
- print(f"Day: {day}")
- for region in data[day]:
- num_diagrams = len(data[day][region])
- print(f" Region: {region}, Number of diagrams: {num_diagrams}")
- # Optionally, print out some details of the first diagram as an example
- if num_diagrams > 0:
- example_diagram = data[day][region][0]
- print(f" Example diagram size: {example_diagram.shape}")
- print(f" Example diagram content (first few pairs): {example_diagram[:5]}")
- else:
- print(" No diagrams for this region and day.")
- print("\n")
- # %% [markdown]
- # ### Computing distances by boostraping and saving metrics
- #
- # 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.
- # %%
- # Define the specified pairwise day comparisons
- day_pairs = [("0D", "3D"), ("3D", "7D"), ("7D", "14D"), ("14D", "30D")]
- # Extract unique days and regions from the data
- unique_days = set(data.keys())
- unique_regions = set(region for day in data for region in data[day])
- results = []
- # Define number of bootstrap replications
- num_replications = 1000
- # For each unique combination of Day and Region
- for region in unique_regions:
- # Perform the specified pairwise comparison of diagrams from different days
- for day1, day2 in day_pairs:
- bootstrap_distances = []
- for _ in range(num_replications):
- # Randomly sample diagrams from day1 and day2 with replacement
- bootstrap_sample1 = choices(data[day1].get(region, []), k=len(data[day1].get(region, [])))
- bootstrap_sample2 = choices(data[day2].get(region, []), k=len(data[day2].get(region, [])))
- # Compute pairwise bottleneck distances for the bootstrap samples
- distances = [bottleneck_distance(diag1, diag2) for diag1 in bootstrap_sample1 for diag2 in bootstrap_sample2 if diag1.size and diag2.size]
- # Only proceed if there are distances to calculate
- if distances:
- # Compute the median distance for this bootstrap replication
- bootstrap_distances.append(np.median(distances))
- # Skip if no distances were calculated
- if not bootstrap_distances:
- continue
- # Compute metrics
- median_distance = np.median(bootstrap_distances)
- confidence_lower = np.percentile(bootstrap_distances, 2.5)
- confidence_upper = np.percentile(bootstrap_distances, 97.5)
- # Store the results
- results.append((day1, day2, region, median_distance, confidence_lower, confidence_upper))
- # Define the results path and save results to CSV
- results_path = "Data_Processed//Widefield_20x_ROIs_Pdgfrb/"
- # Change the name for 1D
- csv_file = os.path.join(results_path, "Widefield_20x_ROIs_Pdgfrb_Bottleneck_0d.csv")
- with open(csv_file, 'w', newline='') as csvfile:
- csvwriter = csv.writer(csvfile)
- # Write header
- csvwriter.writerow(['Day1', 'Day2', 'Region', 'Median Bottleneck Distance', '95% CI Lower', '95% CI Upper'])
- # Write the results
- for row in results:
- csvwriter.writerow(row)
- print(f"Bootstrap results saved to {csv_file}.")
- # %% [markdown]
- # ### Visualization of bottleneck distances by heatmap
- # %% [markdown]
- # We vislauzie the bottleneck distances by heatmap using seaborn:
- # %%
- # Load CSV data for 0-dimension and 1-dimension homology
- csv_path_dim0 = os.path.join("Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Bottleneck_0d.csv")
- csv_path_dim1 = os.path.join("Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Bottleneck_1d.csv")
- data_dim0 = pd.read_csv(csv_path_dim0)
- data_dim1 = pd.read_csv(csv_path_dim1)
- # Assuming the structure of your CSV files matches the expected columns
- # Define label mappings and structure
- regions = ["Peri", "Str", "Ctx"]
- region_labels = ["Perilesion", "Striatum", "Cortex"]
- days = ["0D-3D", "3D-7D", "7D-14D", "14D-30D"]
- dimensions = ["dim0", "dim1"] # Representing 0-dimension and 1-dimension homology
- # Initialize an empty dictionary to store heatmap data for each dimension
- heatmap_data = {dim: pd.DataFrame(index=regions, columns=days) for dim in dimensions}
- heatmap_annotations = {dim: pd.DataFrame(index=regions, columns=days) for dim in dimensions}
- # Function to populate matrices with data from CSV
- def populate_heatmap(data, dim):
- for _, row in data.iterrows():
- 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'])
- if region in regions:
- heatmap_data[dim].loc[region, day_pair] = median_distance
- # Adjust annotation format here to round and remove decimals
- annotation = f"{median_distance}\n({lower} - {upper})"
- heatmap_annotations[dim].loc[region, day_pair] = annotation
- populate_heatmap(data_dim0, "dim0")
- populate_heatmap(data_dim1, "dim1")
- # Convert the data to numeric and handle NaN values
- for dim in dimensions:
- heatmap_data[dim] = heatmap_data[dim].astype(float)
- heatmap_data[dim].fillna(-1, inplace=True) # Fill NaN values with -1 for missing data
- # Visualization
- fig, axes = plt.subplots(1, 2, figsize=(16, 7), sharey=True)
- for ax, dim in zip(axes, dimensions):
- sns.heatmap(heatmap_data[dim], annot=heatmap_annotations[dim], cmap='viridis', ax=ax,
- cbar_kws={'label': 'Bottleneck Distance', 'shrink': 0.5},
- mask=(heatmap_data[dim] == -1), fmt="", annot_kws={"size": 16})
- ax.set_title(f"Dimension: {dim[-1]}", fontsize=16)
- ax.set_xlabel("Day Pair", fontsize=16)
- ax.set_ylabel("Region", fontsize=16)
- ax.set_yticklabels(region_labels, fontsize=16, rotation=0)
- ax.tick_params(axis='x', labelsize=16)
- # Adjust colorbar label size
- cbar = ax.collections[0].colorbar
- cbar.ax.tick_params(labelsize=16)
- cbar.ax.set_ylabel('Bottleneck Distance', fontsize=16)
- plt.tight_layout()
- plt.show()
- # Update the output directory path for saving individual images
- output_dir = "Plots/Widefield_20x_ROIs_pdgfrb_TDA"
- # Ensure the output directory exists
- if not os.path.exists(output_dir):
- os.makedirs(output_dir)
- # Save individual heatmap images for each dimension
- for dim in dimensions:
- fig, ax = plt.subplots(figsize=(7, 5))
- sns.heatmap(heatmap_data[dim], annot=heatmap_annotations[dim], cmap='viridis', ax=ax,
- cbar_kws={'label': 'Bottleneck Distance'},
- mask=(heatmap_data[dim] == -1), fmt="s")
- ax.set_title(f"Bottleneck Distances (Dimension: {dim[-1]})", fontsize=16)
- ax.set_xlabel("Day Pair", fontsize=16)
- ax.set_ylabel("Region", fontsize=16)
- ax.set_yticklabels(region_labels, fontsize=16)
- ax.tick_params(axis='x', labelsize=16)
- # Adjust colorbar label size
- cbar = ax.collections[0].colorbar
- cbar.ax.tick_params(labelsize=16)
- cbar.ax.set_ylabel('Bottleneck Distance', fontsize=16)
- plt.tight_layout()
- # Save the figure
- fig.savefig(os.path.join(output_dir, f"Bottleneck_Heatmap_{dim}.png"))
- plt.close(fig)
- # %% [markdown]
- # In the graphs, we observed the **median** Bottleneck distances and 95% onfidence intervals for day pairs.
- # %% [markdown]
- # ## Calculate Wasserstain distances
- # %% [markdown]
- # 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.
- # %% [markdown]
- # ### Load persistance diagrams
- # %% [markdown]
- # **Noe** Load the data from the Bottleneck distances section. Please make sure to change dim0 or dim1 as needed.
- # %% [markdown]
- # ### Compute Wasserstein distances by boostrapping and saving metrics
- # %%
- # Define the specified pairwise day comparisons
- day_pairs = [("0D", "3D"), ("3D", "7D"), ("7D", "14D"), ("14D", "30D")]
- # Extract unique days and regions from the data
- unique_days = set(data.keys())
- unique_regions = set(region for day in data for region in data[day])
- results = []
- # Define number of bootstrap replications
- num_replications = 1000
- # For each unique combination of Day and Region
- for region in unique_regions:
- # Perform the specified pairwise comparison of diagrams from different days
- for day1, day2 in day_pairs:
- if day1 in data and day2 in data and region in data[day1] and region in data[day2]:
- bootstrap_distances = []
- for _ in range(num_replications):
- # Randomly sample diagrams from day1 and day2 with replacement
- bootstrap_sample1 = choices(data[day1].get(region, []), k=len(data[day1].get(region, [])))
- bootstrap_sample2 = choices(data[day2].get(region, []), k=len(data[day2].get(region, [])))
- # Compute pairwise Wasserstein distances for the bootstrap samples
- distances = [wasserstein_distance(diag1, diag2, order=2) for diag1 in bootstrap_sample1 for diag2 in bootstrap_sample2 if diag1.size and diag2.size]
- # Only proceed if there are distances to calculate
- if distances:
- # Compute the median distance for this bootstrap replication
- bootstrap_distances.append(np.median(distances))
- # Skip if no distances were calculated
- if not bootstrap_distances:
- continue
- # Compute metrics
- median_distance = np.median(bootstrap_distances)
- confidence_lower = np.percentile(bootstrap_distances, 2.5)
- confidence_upper = np.percentile(bootstrap_distances, 97.5)
- # Store the results
- results.append((day1, day2, region, median_distance, confidence_lower, confidence_upper))
- # Define the results path and save results to CSV
- results_path = "D:/Research/Stroke_PDGFR-B_Reactivity/Pdgfrb_Reactivity_DataAnalysis/Stroke_Pdgfrb_Reactivity/Data_Raw/Widefield_20x_ROIs_Pdgfrb/"
- csv_file = os.path.join(results_path, "Widefield_20x_ROIs_Pdgfrb_Wasserstein_1d.csv")
- with open(csv_file, 'w', newline='') as csvfile:
- csvwriter = csv.writer(csvfile)
- # Write header
- csvwriter.writerow(['Day1', 'Day2', 'Region', 'Median Wasserstein Distance', '95% CI Lower', '95% CI Upper'])
- # Write the results
- for row in results:
- csvwriter.writerow(row)
- print(f"Wasserstein distance bootstrap results saved to {csv_file}.")
- # %% [markdown]
- # ### Visualization of Wasserstein distances by heatmap
- # %% [markdown]
- # We plot the calculated Wasserstein distances and save corresponding plots in the designated folder.
- # %%
- # Load CSV data for 0-dimension and 1-dimension homology
- csv_path_dim0 = os.path.join("Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Wasserstein_0d.csv")
- csv_path_dim1 = os.path.join("Data_Processed/Widefield_20x_ROIs_Pdgfrb/Widefield_20x_ROIs_Pdgfrb_Wasserstein_1d.csv")
- data_dim0 = pd.read_csv(csv_path_dim0)
- data_dim1 = pd.read_csv(csv_path_dim1)
- # Assuming the structure of your CSV files matches the expected columns
- # Define label mappings and structure
- regions = ["Peri", "Str", "Ctx"]
- region_labels = ["Perilesion", "Striatum", "Cortex"]
- days = ["0D-3D", "3D-7D", "7D-14D", "14D-30D"]
- dimensions = ["dim0", "dim1"] # Representing 0-dimension and 1-dimension homology
- # Initialize an empty dictionary to store heatmap data for each dimension
- heatmap_data = {dim: pd.DataFrame(index=regions, columns=days) for dim in dimensions}
- heatmap_annotations = {dim: pd.DataFrame(index=regions, columns=days) for dim in dimensions}
- # Function to populate matrices with data from CSV
- def populate_heatmap(data, dim):
- for _, row in data.iterrows():
- 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'])
- if region in regions:
- heatmap_data[dim].loc[region, day_pair] = median_distance
- # Adjust annotation format here to round and remove decimals
- annotation = f"{median_distance}\n({lower} - {upper})"
- heatmap_annotations[dim].loc[region, day_pair] = annotation
- populate_heatmap(data_dim0, "dim0")
- populate_heatmap(data_dim1, "dim1")
- # Convert the data to numeric and handle NaN values
- for dim in dimensions:
- heatmap_data[dim] = heatmap_data[dim].astype(float)
- heatmap_data[dim].fillna(-1, inplace=True) # Fill NaN values with -1 for missing data
- # Visualization
- fig, axes = plt.subplots(1, 2, figsize=(16, 7), sharey=True)
- for ax, dim in zip(axes, dimensions):
- sns.heatmap(heatmap_data[dim], annot=heatmap_annotations[dim], cmap='viridis', ax=ax,
- cbar_kws={'label': 'Wasserstein Distance', 'shrink': 0.5},
- mask=(heatmap_data[dim] == -1), fmt="", annot_kws={"size": 16})
- ax.set_title(f"Dimension: {dim[-1]}", fontsize=16)
- ax.set_xlabel("Day Pair", fontsize=16)
- ax.set_ylabel("Region", fontsize=16)
- ax.set_yticklabels(region_labels, fontsize=16, rotation=0)
- ax.tick_params(axis='x', labelsize=16)
- # Adjust colorbar label size
- cbar = ax.collections[0].colorbar
- cbar.ax.tick_params(labelsize=16)
- cbar.ax.set_ylabel('Wasserstein Distance', fontsize=16)
- plt.tight_layout()
- plt.show()
- # Update the output directory path for saving individual images
- output_dir = "Plots/Widefield_20x_ROIs_pdgfrb_TDA"
- # Ensure the output directory exists
- if not os.path.exists(output_dir):
- os.makedirs(output_dir)
- # Save individual heatmap images for each dimension
- for dim in dimensions:
- fig, ax = plt.subplots(figsize=(7, 5))
- sns.heatmap(heatmap_data[dim], annot=heatmap_annotations[dim], cmap='viridis', ax=ax,
- cbar_kws={'label': 'Wasserstein Distance'},
- mask=(heatmap_data[dim] == -1), fmt="s")
- ax.set_title(f"Wasserstein Distances (Dimension: {dim[-1]})", fontsize=16)
- ax.set_xlabel("Day Pair", fontsize=16)
- ax.set_ylabel("Region", fontsize=16)
- ax.set_yticklabels(region_labels, fontsize=16)
- ax.tick_params(axis='x', labelsize=16)
- # Adjust colorbar label size
- cbar = ax.collections[0].colorbar
- cbar.ax.tick_params(labelsize=16)
- cbar.ax.set_ylabel('Wasserstein Distance', fontsize=16)
- plt.tight_layout()
- # Save the figure
- fig.savefig(os.path.join(output_dir, f"Wasserstein_Heatmap_{dim}.png"))
- plt.close(fig)
- # %% [markdown]
- # Analogous to Bottleneck distances, here we appreacite the estimates and its uncertainty for each day pair.
- # %% [markdown]
- # ## Addon: Generate another set of images for illustration
- # %% [markdown]
- # 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.
- # %%
- def load_point_cloud(file_path):
- return np.load(file_path, allow_pickle=True)
- def get_edges_at_filtration(points, filtration_value):
- rips_complex = RipsComplex(points=points, max_edge_length=filtration_value)
- simplex_tree = rips_complex.create_simplex_tree(max_dimension=1)
- edges = []
- for simplex, f_value in simplex_tree.get_filtration():
- if len(simplex) == 2 and f_value <= filtration_value:
- edges.append(simplex)
- return edges
- def plot_point_cloud_with_structures(points, edges, ax, filtration_value):
- ax.scatter(points[:, 0], points[:, 1], color='green', s=10, zorder=1)
- for edge in edges:
- point1 = points[edge[0]]
- point2 = points[edge[1]]
- ax.plot([point1[0], point2[0]], [point1[1], point2[1]], 'black', alpha=0.9, zorder=1.5)
- for point in points:
- circle = Circle(point, radius=filtration_value, color='skyblue', fill=True, alpha=0.2)
- ax.add_patch(circle)
- ax.set_xticks([])
- ax.set_yticks([])
- ax.set_xlabel('')
- ax.set_ylabel('')
- file_paths = ['PointClouds/Widefield_20x_ROIs_Pdgfrb/Td047_30D_Peri.npy',
- 'PointClouds/Widefield_20x_ROIs_Pdgfrb/Td047_30D_Str.npy',
- 'PointClouds/Widefield_20x_ROIs_Pdgfrb/Td047_30D_Ctx.npy']
- filtration_values = [0, 50, 100, 200]
- # Ensure each point cloud gets its subplot for each filtration value
- fig, axs = plt.subplots(len(file_paths), len(filtration_values), figsize=(15, 5 * len(file_paths)))
- for file_index, file_path in enumerate(file_paths):
- points = load_point_cloud(file_path)
- for ax_index, filtration_value in enumerate(filtration_values):
- ax = axs[file_index, ax_index] if len(file_paths) > 1 else axs[ax_index]
- edges = get_edges_at_filtration(points, filtration_value)
- plot_point_cloud_with_structures(points, edges, ax, filtration_value)
- plt.tight_layout()
- plt.show()
- # %% [markdown]
- # Finally, we save a series f images in a destinated folder for illustration in the scientific article.
- # %%
- import os
- # Load all .npy files in a folder
- def load_point_clouds_from_folder(folder_path):
- point_cloud_files = [os.path.join(folder_path, f) for f in os.listdir(folder_path) if f.endswith('.npy')]
- return point_cloud_files
- # Save the plot for a given point cloud and filtration values
- def save_plots_for_point_cloud(file_path, filtration_values, save_folder):
- points = load_point_cloud(file_path)
- for filtration_value in filtration_values:
- edges = get_edges_at_filtration(points, filtration_value)
- fig, ax = plt.subplots(figsize=(5, 5)) # Adjust size as needed
- plot_point_cloud_with_structures(points, edges, ax, filtration_value)
- # Save plot to the designated folder
- base_filename = os.path.splitext(os.path.basename(file_path))[0]
- save_path = os.path.join(save_folder, f'{base_filename}_filtration_{filtration_value}.png')
- plt.savefig(save_path)
- plt.close(fig) # Close the figure after saving to free up memory
- # Specify folder paths
- point_clouds_folder = 'PointClouds/Widefield_20x_ROIs_Pdgfrb/'
- save_folder = 'D:/Research/Stroke_PDGFR-B_Reactivity/Images/Widefield_20x-ROIs_Pdgfrb_TDA/FiltrationValues'
- filtration_values = [0, 50, 100, 200]
- # Ensure the save folder exists
- if not os.path.exists(save_folder):
- os.makedirs(save_folder)
- # Load and process each point cloud file
- point_cloud_files = load_point_clouds_from_folder(point_clouds_folder)
- for file_path in point_cloud_files:
- save_plots_for_point_cloud(file_path, filtration_values, save_folder)
- # %%
Widefield_20x_ROIs_Pdgfrb_TDA.ipynb at commit ae087d5, no license · at the source
Overview
- Neuroscience Axis, Research Center of CHU de Québec‐Université Laval Quebec City Quebec Canada
- Department of Psychiatry and Neuroscience, Faculty of Medicine Université Laval Quebec City Quebec Canada
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
Availability: 1 check, the latest on 1 November 2026: the link answers (HTTP 200)
- 1 November 2026: the link answers (HTTP 200)
35 files
- Confocal_20x_ROIs_Ki67-P
dgfrb_Coloc.qmd , Quarto, 610 lines - Confocal_20x_ROIs_Klf4-P
dgfrb_Coloc.qmd , Quarto, 716 lines - Confocal_40x_ROIs_Pdgfrb
_Morpho_Analysis.qmd , Quarto, 636 lines, 1 match - Confocal_40x_ROIs_Pdgfrb
_Morpho_BatchScript.qmd , Quarto, 228 lines - FlowCytometry_Pdgfrb_Ana
lysis.qmd , Quarto, 621 lines - FlowCytometry_Pdgfrb_Pro
cessing.qmd , Quarto, 683 lines, 2 matches - PDGFR-B_Reactivity_Suppl
ementaryTables.qmd , Quarto, 786 lines - Widefield_10x_Ipsilatera
l_KO_CD31-ColIV_Inten.qm , Quarto, 488 linesd - Widefield_10x_Ipsilatera
l_KO_Fish_Pdgfrb.qmd , Quarto, 781 lines - Widefield_10x_Ipsilatera
l_KO_Klf4_Exp.qmd , Quarto, 736 lines, 1 match - Widefield_10x_Ipsilatera
l_Ki67-Pdgfrb_Coloc.qmd , Quarto, 641 lines - Widefield_10x_Ipsilatera
l_Ki67_PPA.qmd , Quarto, 480 lines - Widefield_10x_Ipsilatera
l_Klf4_Exp.qmd , Quarto, 1,260 lines - Widefield_10x_ROIs_CD31-
Pdgfrb_Coloc.qmd , Quarto, 2,000 lines - Widefield_10x_ROIs_Flox-
Cre_Fish_Pdgfrb.qmd , Quarto, 728 lines - Widefield_10x_ROIs_Gfap-
Pdgfrb_Covariance.qmd , Quarto, 1,375 lines - Widefield_10x_ROIs_Gfap-
Pdgfrb_Handling.qmd , Quarto, 295 lines - Widefield_10x_ROIs_KO_CD
31-ColIV_Coloc.qmd , Quarto, 1,015 lines - Widefield_10x_ROIs_KO_Pd
gfrb_TDA.ipynb , Jupyter, 607 lines - Widefield_20x_ROIs_KO_Pi
cosirius.qmd , Quarto, 456 lines - Widefield_20x_ROIs_Pdgfr
b_Haralick.qmd , Quarto, 947 lines, 1 match - Widefield_20x_ROIs_Pdgfr
b_Haralick_BatchScript.q , Quarto, 215 linesmd - Widefield_20x_ROIs_Pdgfr
b_TDA.ipynb , Jupyter, 1,065 lines - Widefield_5x_Ipsilateral
_EarlyKO_Pdgfrb-Gfap.qmd , Quarto, 617 lines - Widefield_5x_Ipsilateral
_Gfap-Pdgfrb_Covariance. , Quarto, 662 linesqmd - Widefield_5x_Ipsilateral
_Gfap-Pdgfrb_Covariance_ , Quarto, 325 linesStr.qmd - Widefield_5x_Ipsilateral
_Gfap-Pdgfrb_Handling.qm , Quarto, 964 linesd - Widefield_5x_Ipsilateral
_Gfap_Convex.qmd , Quarto, 571 lines, 2 matches - Widefield_5x_Ipsilateral
_Gfap_IntDen.qmd , Quarto, 956 lines - Widefield_5x_Ipsilateral
_KO_Structure.qmd , Quarto, 1,043 lines - Widefield_5x_Ipsilateral
_Pdgfrb_IntDen.qmd , Quarto, 925 lines - Widefield_5x_Ipsilateral
_Pdgfrb_LowHigh.qmd , Quarto, 525 lines - Widefield_5x_Ipsilateral
_Pdgfrb_LowHigh_Str.qmd , Quarto, 394 lines - Widefield_5x_Ipsilateral
_Pdgfrb_Shrinkage.qmd , Quarto, 818 lines - README.md, Text, 114 lines
elalilab/stroke_pdgfr-b_reactivity
ae087d5288616f7e66b467932c2f2627aa7fb910, 26 March 2026Availability: 1 check, the latest on 1 November 2026: the link answers
- 1 November 2026: the link answers
36 files
- Confocal_20x_ROIs_Ki67-P
dgfrb_Coloc.qmd , Quarto, 610 lines - Confocal_20x_ROIs_Klf4-P
dgfrb_Coloc.qmd , Quarto, 716 lines - Confocal_40x_ROIs_Pdgfrb
_Morpho_Analysis.qmd , Quarto, 636 lines, 1 match - Confocal_40x_ROIs_Pdgfrb
_Morpho_BatchScript.qmd , Quarto, 228 lines - FlowCytometry_Pdgfrb_Ana
lysis.qmd , Quarto, 623 lines - FlowCytometry_Pdgfrb_Pro
cessing.qmd , Quarto, 688 lines, 2 matches - PDGFR-B_Reactivity_Suppl
ementaryTables.qmd , Quarto, 803 lines - Widefield_10x_Ipsilatera
l_KO_Fish_Pdgfrb.qmd , Quarto, 785 lines - Widefield_10x_Ipsilatera
l_KO_Klf4_Exp.qmd , Quarto, 738 lines, 2 matches - Widefield_10x_Ipsilatera
l_Ki67-Pdgfrb_Coloc.qmd , Quarto, 641 lines - Widefield_10x_Ipsilatera
l_Ki67_PPA.qmd , Quarto, 480 lines, 1 match - Widefield_10x_Ipsilatera
l_Klf4_Exp.qmd , Quarto, 1,260 lines - Widefield_10x_ROIs_CD31-
Pdgfrb_Coloc.qmd , Quarto, 2,000 lines, 1 match - Widefield_10x_ROIs_Flox-
Cre_Fish_Pdgfrb.qmd , Quarto, 730 lines - Widefield_10x_ROIs_Gfap-
Pdgfrb_Covariance.qmd , Quarto, 1,375 lines - Widefield_10x_ROIs_Gfap-
Pdgfrb_Handling.qmd , Quarto, 295 lines - Widefield_10x_ROIs_KO_CD
31-ColIV_Coloc.qmd , Quarto, 1,025 lines, 1 match - Widefield_10x_ROIs_KO_Pd
gfrb_TDA.ipynb , Jupyter, 607 lines - Widefield_20x_ROIs_KO_Pi
cosirius.qmd , Quarto, 456 lines - Widefield_20x_ROIs_Pdgfr
b_Haralick.qmd , Quarto, 983 lines, 1 match - Widefield_20x_ROIs_Pdgfr
b_Haralick_BatchScript.q , Quarto, 215 linesmd - Widefield_20x_ROIs_Pdgfr
b_TDA.ipynb , Jupyter, 1,065 lines, 3 matches - Widefield_5x_Ipsilateral
_EarlyKO_Pdgfrb-Gfap.qmd , Quarto, 619 lines - Widefield_5x_Ipsilateral
_Gfap-Pdgfrb_Covariance. , Quarto, 662 linesqmd - Widefield_5x_Ipsilateral
_Gfap-Pdgfrb_Covariance_ , Quarto, 325 linesStr.qmd - Widefield_5x_Ipsilateral
_Gfap-Pdgfrb_Handling.qm , Quarto, 964 linesd - Widefield_5x_Ipsilateral
_Gfap_Convex.qmd , Quarto, 571 lines, 2 matches - Widefield_5x_Ipsilateral
_Gfap_IntDen.qmd , Quarto, 956 lines - Widefield_5x_Ipsilateral
_KO_CD31-ColIV_Inten.qmd , Quarto, 489 lines - Widefield_5x_Ipsilateral
_KO_Structure.qmd , Quarto, 1,047 lines - Widefield_5x_Ipsilateral
_Pdgfrb_IntDen.qmd , Quarto, 925 lines - Widefield_5x_Ipsilateral
_Pdgfrb_LowHigh.qmd , Quarto, 525 lines - Widefield_5x_Ipsilateral
_Pdgfrb_LowHigh_Str.qmd , Quarto, 394 lines - Widefield_5x_Ipsilateral
_Pdgfrb_Shrinkage.qmd , Quarto, 818 lines - Widefield_5x_Whole_Early
KO_Pdfrb-NeuN-Gfap_Cells , Quarto, 767 linesRatio.qmd - README.md, Text, 114 lines
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
- osf:74mqn, at OSF; found in “Data Availability Statement”
- osf:jxb4z, at OSF; found in the text, “Astrocytes and PDGFRβ + Cells Respond…”
- zenodo:10553084, at Zenodo; found in “Data Availability Statement”
Data Availability Statement
All the data associated with the publication are made fully available via https://
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{manriquecastano
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/
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":
"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 : CMLSIn 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 protocolsIn 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. MedicineIn 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. ClinicalIn 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 biologyIn 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 reportsIn 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 NeuroscienceIn 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 communicationsIn 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 disordersIn 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: NatureIn 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.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 2 repositories of the authors' code, each at its verified commit and with its license, 69 scripts, and 21 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:c683e83a25f2f6f8…
Add the badge to the code's README
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 -->
[](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:endHTML
<!-- 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.
