Spaces:
Running
Running
File size: 11,864 Bytes
9b23ae9 bce4bae 9b23ae9 8643122 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 8643122 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 bce4bae 9b23ae9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 | """ROI Connectivity - Research Grade."""
import streamlit as st
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
from session import init_session, log_analysis, get_carried_rois, download_csv_button, show_analysis_log
from theme import inject_theme, section_header
from utils import (
make_roi_indices, compute_connectivity, cluster_rois, graph_metrics,
partial_correlation, betweenness_centrality, modularity_score,
ROI_GROUPS,
)
from synthetic import generate_realistic_predictions
st.set_page_config(page_title="ROI Connectivity", page_icon="🔗", layout="wide")
init_session()
inject_theme()
show_analysis_log()
st.title("🔗 ROI Connectivity Analysis")
st.markdown("Functional connectivity between brain regions: correlation structure, network organization, and graph topology.")
# --- Sidebar ---
with st.sidebar:
st.header("Configuration")
stim_type = st.selectbox("Stimulus type", ["visual", "auditory", "language", "multimodal"])
n_timepoints = st.slider("Duration (TRs)", 30, 200, 80)
tr_seconds = st.slider("TR (seconds)", 0.5, 2.0, 1.0, 0.1)
seed = st.number_input("Seed", value=42, min_value=0)
st.subheader("Analysis Parameters")
n_clusters = st.slider("Number of clusters", 2, 8, 4)
threshold = st.slider("Edge threshold", 0.1, 0.8, 0.3, 0.05)
use_partial = st.checkbox("Use partial correlation", value=False,
help="Control for shared mean signal across all ROIs")
carried = get_carried_rois()
use_carried = False
if carried:
use_carried = st.checkbox(f"Filter to {len(carried)} carried ROIs", value=False)
# --- Generate Data ---
roi_indices, n_vertices = make_roi_indices()
predictions = generate_realistic_predictions(n_timepoints, roi_indices, stim_type, tr_seconds, seed=seed)
log_analysis(f"Connectivity: {stim_type}, partial={use_partial}")
# Filter ROIs if carrying from alignment
active_indices = roi_indices
if use_carried and carried:
active_indices = {k: v for k, v in roi_indices.items() if k in carried}
# --- Compute Connectivity ---
if use_partial:
corr_matrix, roi_names = partial_correlation(predictions, active_indices)
corr_label = "Partial Correlation"
else:
corr_matrix, roi_names = compute_connectivity(predictions, active_indices)
corr_label = "Pearson Correlation"
n_rois = len(roi_names)
# --- Correlation Matrix with Cluster Boundaries ---
st.subheader(f"{corr_label} Matrix")
clusters, labels = cluster_rois(corr_matrix, roi_names, n_clusters)
# Sort ROIs by cluster for block-diagonal structure
sorted_idx = np.argsort(labels)
sorted_corr = corr_matrix[np.ix_(sorted_idx, sorted_idx)]
sorted_names = [roi_names[i] for i in sorted_idx]
sorted_labels = labels[sorted_idx]
fig_corr = go.Figure(go.Heatmap(
z=sorted_corr, x=sorted_names, y=sorted_names,
colorscale="RdBu_r", zmid=0, zmin=-1, zmax=1,
colorbar=dict(title="r"),
))
# Add cluster boundary lines
boundaries = []
for i in range(1, len(sorted_labels)):
if sorted_labels[i] != sorted_labels[i - 1]:
boundaries.append(i - 0.5)
for b in boundaries:
fig_corr.add_shape(type="line", x0=b, x1=b, y0=-0.5, y1=n_rois - 0.5,
line=dict(color="white", width=1.5, dash="dot"))
fig_corr.add_shape(type="line", x0=-0.5, x1=n_rois - 0.5, y0=b, y1=b,
line=dict(color="white", width=1.5, dash="dot"))
fig_corr.update_layout(
height=550, template="plotly_dark",
xaxis=dict(tickangle=45, tickfont=dict(size=8)),
yaxis=dict(tickfont=dict(size=8)),
)
st.plotly_chart(fig_corr, use_container_width=True)
st.caption(f"White dotted lines indicate cluster boundaries ({n_clusters} clusters)")
# --- Dendrogram ---
st.divider()
col_dendro, col_clusters = st.columns([1, 1])
with col_dendro:
st.subheader("Hierarchical Clustering Dendrogram")
from scipy.cluster.hierarchy import linkage, dendrogram
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use("Agg")
dist = 1.0 - np.abs(corr_matrix)
np.fill_diagonal(dist, 0.0)
condensed = [dist[i, j] for i in range(n_rois) for j in range(i + 1, n_rois)]
Z = linkage(condensed, method="average")
fig_dendro, ax = plt.subplots(figsize=(8, 4))
ax.set_facecolor("#0E1117")
fig_dendro.patch.set_facecolor("#0E1117")
dendrogram(Z, labels=roi_names, leaf_rotation=90, leaf_font_size=7, ax=ax,
color_threshold=Z[-n_clusters + 1, 2] if n_clusters < n_rois else 0)
ax.tick_params(colors="white")
ax.set_ylabel("Distance (1 - |r|)", color="white")
for spine in ax.spines.values():
spine.set_color("white")
st.pyplot(fig_dendro)
plt.close()
with col_clusters:
st.subheader("Network Clusters")
mod_q = modularity_score(corr_matrix, labels)
st.metric("Modularity (Q)", f"{mod_q:.3f}",
help="Newman's modularity. Higher = stronger community structure. Q > 0.3 is typically considered meaningful.")
for cid in sorted(clusters.keys()):
rois = clusters[cid]
# Identify dominant functional group
group_counts = {}
for roi in rois:
for g, g_rois in ROI_GROUPS.items():
if roi in g_rois:
group_counts[g] = group_counts.get(g, 0) + 1
dominant = max(group_counts, key=group_counts.get) if group_counts else "Mixed"
st.markdown(f"**Network {cid}** ({dominant}): {', '.join(rois)}")
# --- Centrality Comparison ---
st.divider()
st.subheader("Centrality Analysis")
col_deg, col_btw = st.columns(2)
degrees = graph_metrics(corr_matrix, roi_names, threshold)
btw = betweenness_centrality(corr_matrix, roi_names, threshold)
with col_deg:
st.markdown("**Degree Centrality** - fraction of ROIs connected to each node")
deg_df = pd.DataFrame(sorted(degrees.items(), key=lambda x: x[1], reverse=True), columns=["ROI", "Degree"])
fig_deg = go.Figure(go.Bar(x=deg_df["Degree"], y=deg_df["ROI"], orientation="h", marker_color="#6C5CE7"))
fig_deg.update_layout(xaxis_range=[0, 1], height=max(300, n_rois * 20), template="plotly_dark",
yaxis=dict(autorange="reversed", tickfont=dict(size=9)))
st.plotly_chart(fig_deg, use_container_width=True)
with col_btw:
st.markdown("**Betweenness Centrality** - how often a node lies on shortest paths between others")
btw_df = pd.DataFrame(sorted(btw.items(), key=lambda x: x[1], reverse=True), columns=["ROI", "Betweenness"])
fig_btw = go.Figure(go.Bar(x=btw_df["Betweenness"], y=btw_df["ROI"], orientation="h", marker_color="#FF6B6B"))
fig_btw.update_layout(height=max(300, n_rois * 20), template="plotly_dark",
yaxis=dict(autorange="reversed", tickfont=dict(size=9)))
st.plotly_chart(fig_btw, use_container_width=True)
# Combined table
centrality_df = pd.merge(deg_df, btw_df, on="ROI")
download_csv_button(centrality_df, "centrality_metrics.csv")
# --- Edge Weight Distribution ---
st.divider()
col_dist, col_graph = st.columns([1, 2])
with col_dist:
st.subheader("Edge Weight Distribution")
upper_tri = corr_matrix[np.triu_indices(n_rois, k=1)]
fig_hist = go.Figure(go.Histogram(x=upper_tri, nbinsx=40, marker_color="rgba(108,92,231,0.7)"))
fig_hist.add_vline(x=threshold, line_color="red", line_dash="dash", annotation_text="Threshold")
fig_hist.add_vline(x=-threshold, line_color="red", line_dash="dash")
fig_hist.update_layout(
xaxis_title="Correlation", yaxis_title="Count",
height=350, template="plotly_dark",
)
st.plotly_chart(fig_hist, use_container_width=True)
n_edges = np.sum(np.abs(upper_tri) > threshold)
max_edges = n_rois * (n_rois - 1) // 2
st.caption(f"{n_edges}/{max_edges} edges above threshold ({100 * n_edges / max(max_edges, 1):.1f}% density)")
# --- Network Graph ---
with col_graph:
st.subheader("Network Graph")
try:
import networkx as nx
G = nx.Graph()
for name in roi_names:
G.add_node(name)
for i in range(n_rois):
for j in range(i + 1, n_rois):
w = abs(corr_matrix[i, j])
if w > threshold:
G.add_edge(roi_names[i], roi_names[j], weight=w)
pos = nx.spring_layout(G, seed=seed, k=2.5)
color_map = px.colors.qualitative.Set2
node_colors = [color_map[(labels[i] - 1) % len(color_map)] for i in range(n_rois)]
# Edges with width proportional to weight
for u, v, data in G.edges(data=True):
x0, y0 = pos[u]
x1, y1 = pos[v]
fig_graph = go.Figure() if not hasattr(st, '_graph_fig') else st._graph_fig
fig_net = go.Figure()
for u, v, d in G.edges(data=True):
x0, y0 = pos[u]
x1, y1 = pos[v]
w = d.get("weight", 0.3)
fig_net.add_trace(go.Scatter(
x=[x0, x1, None], y=[y0, y1, None], mode="lines",
line=dict(width=w * 3, color=f"rgba(150,150,150,{min(w, 0.8)})"),
hoverinfo="none", showlegend=False,
))
# Node sizes by degree
max_deg = max(degrees.values()) if degrees else 1
node_sizes = [8 + 20 * degrees.get(name, 0) / max(max_deg, 0.01) for name in roi_names]
node_x = [pos[n][0] for n in roi_names]
node_y = [pos[n][1] for n in roi_names]
fig_net.add_trace(go.Scatter(
x=node_x, y=node_y, mode="markers+text",
marker=dict(size=node_sizes, color=node_colors, line=dict(width=1, color="white")),
text=roi_names, textposition="top center", textfont=dict(size=7, color="white"),
hovertext=[f"{name}<br>Degree: {degrees.get(name, 0):.2f}<br>Betweenness: {btw.get(name, 0):.3f}" for name in roi_names],
hoverinfo="text", showlegend=False,
))
fig_net.update_layout(
height=450, template="plotly_dark",
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
)
st.plotly_chart(fig_net, use_container_width=True)
except ImportError:
st.info("Install `networkx` for graph visualization: `pip install networkx`")
# --- Methodology ---
with st.expander("Methodology", expanded=False):
st.markdown("""
**Functional Connectivity** is computed as pairwise Pearson correlation between ROI
timecourses (mean activation across vertices within each ROI).
**Partial Correlation** controls for the shared mean signal by computing the precision
matrix (inverse covariance) and normalizing. This removes indirect correlations mediated
by a common driver.
**Hierarchical Clustering** uses agglomerative clustering with average linkage on a
distance matrix defined as ``1 - |correlation|``. The dendrogram shows the hierarchical
merging of ROIs into networks.
**Modularity (Q)** quantifies how strongly the network divides into communities compared
to a random network with the same degree distribution. Q > 0.3 typically indicates
meaningful community structure. (Newman, 2006, *PNAS*)
**Degree Centrality** is the fraction of other nodes each node is connected to (above
the correlation threshold). High degree = hub region.
**Betweenness Centrality** counts how often a node lies on the shortest path between
other node pairs. High betweenness = bridge between communities.
**Edge Weight Distribution** shows the histogram of all pairwise correlations. The
threshold (red line) determines which connections are retained for graph analysis.
**References**:
- Rubinov & Sporns, 2010, *NeuroImage* (graph metrics for brain networks)
- Newman, 2006, *PNAS* (modularity in networks)
- Smith et al., 2011, *NeuroImage* (partial correlation for fMRI connectivity)
""")
|