Spaces:
Running
Running
File size: 11,636 Bytes
9b23ae9 bce4bae 9b23ae9 8643122 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 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 | """Temporal Dynamics - Research Grade."""
import streamlit as st
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
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, peak_latency, temporal_correlation, decompose_response, ROI_GROUPS, ALL_ROIS
from synthetic import generate_realistic_predictions, generate_correlated_features
st.set_page_config(page_title="Temporal Dynamics", page_icon="⏱️", layout="wide")
init_session()
inject_theme()
show_analysis_log()
st.title("⏱️ Temporal Dynamics")
st.markdown("Analyze how brain responses evolve over time, including processing hierarchy and temporal coupling with model features.")
# --- Sidebar ---
with st.sidebar:
st.header("Configuration")
stim_type = st.selectbox("Stimulus type", ["visual", "auditory", "language", "multimodal"],
index=["visual", "auditory", "language", "multimodal"].index(st.session_state.get("stimulus_type", "visual")))
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("ROI Selection")
carried = get_carried_rois()
use_carried = False
if carried:
use_carried = st.checkbox(f"Use {len(carried)} ROIs from Brain Alignment", value=True)
if use_carried and carried:
selected_rois = carried
st.caption(f"Using: {', '.join(selected_rois[:5])}{'...' if len(selected_rois) > 5 else ''}")
else:
selected_group = st.selectbox("Region group", list(ROI_GROUPS.keys()))
available_rois = ROI_GROUPS[selected_group]
selected_rois = st.multiselect("ROIs to analyze", available_rois, default=available_rois[:4])
max_lag = st.slider("Max correlation lag (TRs)", 5, 30, 15)
cutoff = st.slider("Decomposition cutoff (seconds)", 1.0, 10.0, 4.0, 0.5)
if not selected_rois:
st.warning("Select at least one ROI.")
st.stop()
# --- Generate Data ---
roi_indices, n_vertices = make_roi_indices()
predictions = generate_realistic_predictions(n_timepoints, roi_indices, stim_type, tr_seconds, seed=seed)
features = generate_correlated_features(predictions, alignment_strength=0.5, feature_dim=64, seed=seed + 1)
log_analysis(f"Temporal dynamics: {stim_type}, {len(selected_rois)} ROIs")
time_axis = np.arange(n_timepoints) * tr_seconds
colors = ["#00D2FF", "#FF6B6B", "#A29BFE", "#FFEAA7", "#55EFC4", "#FD79A8", "#74B9FF", "#E17055"]
# --- Raw ROI Timecourses ---
st.subheader("Raw ROI Timecourses")
st.markdown("Mean absolute activation over time for each selected ROI. Note the hemodynamic response shape after stimulus events.")
fig_raw = go.Figure()
for i, roi in enumerate(selected_rois):
if roi in roi_indices:
verts = roi_indices[roi]
valid = verts[verts < predictions.shape[1]]
if len(valid) > 0:
tc = np.abs(predictions[:, valid]).mean(axis=1)
fig_raw.add_trace(go.Scatter(
x=time_axis, y=tc, name=roi,
line=dict(color=colors[i % len(colors)], width=2),
))
fig_raw.update_layout(
xaxis_title="Time (seconds)", yaxis_title="Mean |activation|",
height=400, template="plotly_dark",
legend=dict(orientation="h", yanchor="bottom", y=1.02),
)
st.plotly_chart(fig_raw, use_container_width=True)
# --- Peak Latency (sorted = processing hierarchy) ---
st.divider()
st.subheader("Peak Response Latency (Processing Hierarchy)")
st.markdown("ROIs sorted by peak latency reveal the cortical processing hierarchy: early sensory areas respond first, association cortex later.")
latency_data = []
for roi in selected_rois:
if roi in roi_indices:
lat = peak_latency(predictions, roi_indices, roi, tr_seconds)
# Determine functional group
group = "Other"
for g, rois in ROI_GROUPS.items():
if roi in rois:
group = g
break
latency_data.append({"ROI": roi, "Peak Latency (s)": lat, "Group": group})
lat_df = pd.DataFrame(latency_data).sort_values("Peak Latency (s)")
group_colors = {"Visual": "#00D2FF", "Auditory": "#FF6B6B", "Language": "#A29BFE", "Executive": "#FFEAA7", "Other": "#888"}
col1, col2 = st.columns([2, 1])
with col1:
fig_lat = go.Figure(go.Bar(
x=lat_df["Peak Latency (s)"], y=lat_df["ROI"],
orientation="h",
marker_color=[group_colors.get(g, "#888") for g in lat_df["Group"]],
))
fig_lat.update_layout(
xaxis_title="Time to peak (seconds)", height=max(250, len(selected_rois) * 30),
template="plotly_dark", yaxis=dict(autorange="reversed"),
)
st.plotly_chart(fig_lat, use_container_width=True)
with col2:
st.dataframe(lat_df[["ROI", "Peak Latency (s)", "Group"]], use_container_width=True, hide_index=True)
download_csv_button(lat_df, "peak_latencies.csv")
# --- Lag Correlation with Significance ---
st.divider()
st.subheader("Temporal Correlation (Brain vs Model Features)")
st.markdown("Pearson correlation at different time lags. The peak indicates optimal temporal alignment. "
"Gray band shows 95% null range from shuffled data.")
lags = np.arange(-max_lag, max_lag + 1) * tr_seconds
fig_corr = go.Figure()
# Null band (shuffle features, compute correlation envelope)
rng = np.random.default_rng(seed)
null_corrs = []
for _ in range(50):
shuffled = features[rng.permutation(len(features))]
for roi in selected_rois[:1]: # Use first ROI for null band
if roi in roi_indices:
nc = temporal_correlation(predictions, shuffled, roi_indices, roi, max_lag)
null_corrs.append(nc)
if null_corrs:
null_arr = np.array(null_corrs)
null_hi = np.percentile(null_arr, 97.5, axis=0)
null_lo = np.percentile(null_arr, 2.5, axis=0)
fig_corr.add_trace(go.Scatter(x=lags, y=null_hi, mode="lines", line=dict(width=0), showlegend=False))
fig_corr.add_trace(go.Scatter(x=lags, y=null_lo, mode="lines", line=dict(width=0),
fill="tonexty", fillcolor="rgba(150,150,150,0.2)",
name="95% null range"))
# Actual correlations
optimal_lags = []
for i, roi in enumerate(selected_rois):
if roi in roi_indices:
corr = temporal_correlation(predictions, features, roi_indices, roi, max_lag)
fig_corr.add_trace(go.Scatter(
x=lags, y=corr, name=roi,
line=dict(color=colors[i % len(colors)], width=2),
))
opt_idx = np.argmax(np.abs(corr))
optimal_lags.append({"ROI": roi, "Optimal Lag (s)": lags[opt_idx], "Max |r|": float(np.abs(corr[opt_idx]))})
fig_corr.add_vline(x=0, line_dash="dash", line_color="gray", opacity=0.5)
fig_corr.update_layout(
xaxis_title="Lag (seconds)", yaxis_title="Pearson Correlation",
height=400, template="plotly_dark",
legend=dict(orientation="h", yanchor="bottom", y=1.02),
)
st.plotly_chart(fig_corr, use_container_width=True)
# --- Optimal Lag Summary ---
if optimal_lags:
st.subheader("Optimal Lag Summary")
opt_df = pd.DataFrame(optimal_lags).sort_values("Max |r|", ascending=False)
st.dataframe(opt_df, use_container_width=True, hide_index=True)
download_csv_button(opt_df, "optimal_lags.csv")
# --- Cross-ROI Lag Matrix ---
if len(selected_rois) >= 2:
st.divider()
st.subheader("Cross-ROI Lag Matrix")
st.markdown("Optimal lag between each pair of ROIs. Positive values mean the row ROI leads the column ROI.")
n_rois = len(selected_rois)
lag_matrix = np.zeros((n_rois, n_rois))
for i, roi_a in enumerate(selected_rois):
if roi_a not in roi_indices:
continue
verts_a = roi_indices[roi_a]
valid_a = verts_a[verts_a < predictions.shape[1]]
if len(valid_a) == 0:
continue
tc_a = np.abs(predictions[:, valid_a]).mean(axis=1)
for j, roi_b in enumerate(selected_rois):
if i == j or roi_b not in roi_indices:
continue
verts_b = roi_indices[roi_b]
valid_b = verts_b[verts_b < predictions.shape[1]]
if len(valid_b) == 0:
continue
tc_b = np.abs(predictions[:, valid_b]).mean(axis=1)
# Cross-correlation to find optimal lag
corrs_ab = temporal_correlation(predictions, tc_b, roi_indices, roi_a, max_lag)
opt_idx = np.argmax(np.abs(corrs_ab))
lag_matrix[i, j] = lags[opt_idx]
fig_lagmat = go.Figure(go.Heatmap(
z=lag_matrix, x=selected_rois, y=selected_rois,
colorscale="RdBu_r", zmid=0,
colorbar=dict(title="Lag (s)"),
text=np.round(lag_matrix, 1), texttemplate="%{text}",
))
fig_lagmat.update_layout(height=400, template="plotly_dark")
st.plotly_chart(fig_lagmat, use_container_width=True)
# --- Sustained vs Transient ---
st.divider()
st.subheader("Sustained vs Transient Decomposition")
st.markdown("Moving-average filter separates slow sustained responses from fast transient spikes.")
roi_for_decomp = st.selectbox("ROI for decomposition", selected_rois)
sustained, transient = decompose_response(predictions, roi_indices, roi_for_decomp, cutoff, tr_seconds)
original = sustained + transient
fig_decomp = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.06,
subplot_titles=("Original Signal", "Sustained Component", "Transient Component"))
fig_decomp.add_trace(go.Scatter(x=time_axis, y=original, line=dict(color="#888", width=1.5)), row=1, col=1)
fig_decomp.add_trace(go.Scatter(x=time_axis, y=sustained, line=dict(color="#6C5CE7", width=2)), row=2, col=1)
fig_decomp.add_trace(go.Scatter(x=time_axis, y=transient, line=dict(color="#FF6B6B", width=1.5)), row=3, col=1)
fig_decomp.update_xaxes(title_text="Time (seconds)", row=3, col=1)
fig_decomp.update_layout(height=550, template="plotly_dark", showlegend=False)
st.plotly_chart(fig_decomp, use_container_width=True)
# --- Methodology ---
with st.expander("Methodology", expanded=False):
st.markdown("""
**Peak Latency** is the time at which mean absolute activation reaches its maximum
within an ROI. In real fMRI, early sensory cortex (V1, A1) peaks at ~5-6s post-stimulus
due to the hemodynamic response, while association cortex (dlPFC, angular gyrus) peaks
~1-3s later reflecting higher-order processing.
**Temporal Correlation** computes Pearson correlation between the ROI timecourse and model
feature timecourse at each lag in ``[-max_lag, +max_lag]`` TRs. The lag at maximum absolute
correlation reveals the temporal offset at which model and brain are best aligned.
**Null significance band** is estimated by shuffling the model features 50 times and
computing the lag correlation each time. The 95% envelope of these null correlations
provides a significance threshold.
**Sustained vs Transient Decomposition** uses a moving-average filter with the specified
cutoff period. The sustained component captures slow, maintained responses (e.g., block
design activations), while the transient component captures fast, event-related responses.
**Cross-ROI Lag Matrix** shows the optimal temporal offset between every pair of ROIs,
revealing directional information flow (positive lag = row ROI leads column ROI).
**References**:
- Boynton et al., 1996, *J Neuroscience* (hemodynamic response function)
- Friston et al., 1998, *NeuroImage* (temporal basis functions in fMRI)
""")
|